/*
    Реализация спецификаций CLDC версии 1.1 (JSR-139), MIDP версии 2.1 (JSR-118)
    и других спецификаций для функционирования компактных приложений на языке
    Java (мидлетов) в среде программного обеспечения Малик Эмулятор.

    Copyright © 2016–2017, 2019–2022 Малик Разработчик

    Это свободная программа: вы можете перераспространять ее и/или изменять
    ее на условиях Меньшей Стандартной общественной лицензии GNU в том виде,
    в каком она была опубликована Фондом свободного программного обеспечения;
    либо версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.

    Эта программа распространяется в надежде, что она будет полезной,
    но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА
    или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ. Подробнее см. в Меньшей Стандартной
    общественной лицензии GNU.

    Вы должны были получить копию Меньшей Стандартной общественной лицензии GNU
    вместе с этой программой. Если это не так, см.
    <https://www.gnu.org/licenses/>.
*/

package malik.emulator.microedition.system.protocol.file;

import java.io.*;
import java.util.*;
import javax.microedition.io.file.*;
import malik.emulator.io.*;
import malik.emulator.io.cloud.*;
import malik.emulator.io.vfs.*;
import malik.emulator.util.*;

public final class VFSConnection extends malik.emulator.microedition.io.CustomConnection implements FileConnection
{
    private static final String[] FORBIDDEN_EXTENSIONS;
    private static final String[] FORBIDDEN_DIRECTORIES;

    static {
        FORBIDDEN_EXTENSIONS = new String[] { "bindbg", "properties" };
        FORBIDDEN_DIRECTORIES = new String[] { "/meta-inf", "/res", "/rms", "/ui" };
    }

    public static boolean isForbiddenPath(VirtualFileSystemReadOnly place, String path) {
        if(place == null || place == CloudFileSystem.instance)
        {
            String[] rules;
            if((path = path != null ? path.toLowerCase() : "").lastIndexOf('/') <= 0) for(int i = (rules = FORBIDDEN_EXTENSIONS).length; i-- > 0; ) if(path.endsWith(".".concat(rules[i]))) return true;
            for(int i = (rules = FORBIDDEN_DIRECTORIES).length; i-- > 0; )
            {
                String rule;
                if(path.equals(rule = rules[i]) || path.startsWith(rule.concat("/"))) return true;
            }
        }
        return false;
    }

    private static boolean isObjectExists(VirtualFileSystemReadOnly place, String path) {
        try
        {
            (place == null ? CloudFileSystem.instance : place).readAttributes(path, null);
        }
        catch(IOException e)
        {
            return false;
        }
        return true;
    }

    private static boolean isMatches(String name, String[] matches) {
        int len;
        if(matches == null || (len = matches.length) <= 0) return false;
        if(len-- > 1)
        {
            for(int pos, i = pos = 0; i <= len; i++)
            {
                int fpos;
                String s = matches[i];
                if(i == len)
                {
                    if(name.length() - s.length() < pos || !name.endsWith(s)) return false;
                }
                else if(i == 0)
                {
                    if(!name.startsWith(s)) return false;
                    pos = s.length();
                }
                else
                {
                    if((fpos = name.indexOf(s, pos)) < 0) return false;
                    pos = fpos + s.length();
                }
            }
            return true;
        }
        return name.equals(matches[0]);
    }

    private static long directorySize(VirtualFileSystemReadOnly place, String path, boolean includeSubDirectories) throws IOException {
        long result = 0L;
        FileEnumeration enumr;
        if((enumr = place.findFirst(path)) != null)
        {
            try
            {
                do
                {
                    String name = enumr.getName();
                    if(isForbiddenPath(place, path.concat(name))) continue;
                    if(!enumr.isDirectory())
                    {
                        result += enumr.getSize();
                    }
                    else if(includeSubDirectories)
                    {
                        result += directorySize(place, (new StringBuilder()).append(path).append(name).append('/').toString(), true);
                    }
                } while(enumr.findNext());
            }
            finally
            {
                enumr.close();
            }
        }
        return result;
    }

    private static String[] getMatches(String filter) {
        int i;
        int j;
        int len;
        int index;
        String[] result;
        if(filter == null) return null;
        for(len = 1, j = 0; (j = filter.indexOf('*', j)) >= 0; j++) len++;
        for(result = new String[len], i = j = index = 0; (j = filter.indexOf('*', j)) >= 0; i = ++j) result[index++] = filter.substring(i, j);
        result[index] = filter.substring(i);
        return result;
    }

    private final int mode;
    private String path;
    private final String host;
    private final VirtualFileSystemReadOnly placero;
    private final VirtualFileSystemReadWrite placerw;

    public VFSConnection(String url, VirtualFileSystemReadOnly place, String path, int mode) {
        super(url);
        int len1 = url.length();
        int len2 = path.length();
        if(len1 < len2) len1 = len2;
        if(place == null) place = CloudFileSystem.instance;
        this.mode = mode;
        this.path = path;
        this.host = url.substring(0, len1 - len2);
        this.placero = place;
        this.placerw = place instanceof VirtualFileSystemReadWrite ? (VirtualFileSystemReadWrite) place : null;
    }

    public void setURL(String url) {
        throw new IllegalStateException("CustomConnection.setURL: невозможно изменить адрес соединения.");
    }

    public void mkdir() throws IOException {
        int lim;
        int pos;
        String path;
        VirtualFileSystemReadWrite place;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.mkdir: соединение закрыто.");
        }
        if((mode & WRITE) == 0)
        {
            throw new IllegalModeException("FileConnection.mkdir: требуется доступ для записи.");
        }
        if((lim = (path = this.path).length() - 1) > 0 && path.charAt(lim) == '/') path = path.substring(0, lim);
        if(isObjectExists(place = placerw, path))
        {
            throw new IOException("FileConnection.mkdir: объект существует.");
        }
        if((pos = path.lastIndexOf('/')) > 1 && !isObjectExists(place, path.substring(0, pos)))
        {
            throw new IOException("FileConnection.mkdir: промежуточные папки не существуют.");
        }
        place.createDirectory(path);
    }

    public void create() throws IOException {
        String path;
        VirtualFileSystemReadWrite place;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.create: соединение закрыто.");
        }
        if((mode & WRITE) == 0)
        {
            throw new IllegalModeException("FileConnection.create: требуется доступ для записи.");
        }
        if((path = this.path).endsWith("/"))
        {
            throw new IOException("FileConnection.create: адрес объекта заканчивается на «/».");
        }
        if(isObjectExists(place = placerw, path))
        {
            throw new IOException("FileConnection.create: объект существует.");
        }
        place.createFile(path).close();
    }

    public void delete() throws IOException {
        int lim;
        File file;
        String path;
        FileAttributes attrs;
        VirtualFileSystemReadWrite place;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.delete: соединение закрыто.");
        }
        if((mode & WRITE) == 0)
        {
            throw new IllegalModeException("FileConnection.delete: требуется доступ для записи.");
        }
        if((lim = (path = this.path).length() - 1) > 0 && path.charAt(lim) == '/') path = path.substring(0, lim);
        if((file = File.getOpened(place = placerw, path)) != null) file.close();
        place.readAttributes(path, attrs = new FileAttributes());
        if(attrs.isDirectory())
        {
            place.deleteDirectory(path);
            return;
        }
        place.deleteFile(path);
    }

    public void rename(String newName) throws IOException {
        boolean hasDir;
        int lim;
        File file;
        String oldPath;
        String newPath;
        VirtualFileSystemReadWrite place;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.rename: соединение закрыто.");
        }
        if((mode & WRITE) == 0)
        {
            throw new IllegalModeException("FileConnection.rename: требуется доступ для записи.");
        }
        if(newName == null)
        {
            throw new NullPointerException("FileConnection.rename: аргумент newName равен нулевой ссылке.");
        }
        if(newName.indexOf('/') >= 0)
        {
            throw new IllegalArgumentException("FileConnection.rename: аргумент newName содержит «/».");
        }
        if((lim = (oldPath = path).length() - 1) <= 0)
        {
            throw new IOException("FileConnection.rename: нельзя переименовать папку с приложением.");
        }
        if(hasDir = oldPath.charAt(lim) == '/') oldPath = oldPath.substring(0, lim);
        if(isForbiddenPath(place = placerw, newPath = oldPath.substring(0, oldPath.lastIndexOf('/') + 1).concat(newName)))
        {
            throw new SecurityException("FileConnection.rename: доступ к заданному объекту запрещён для протокола file.");
        }
        if((file = File.getOpened(place, oldPath)) != null) file.close();
        place.move(oldPath, newPath);
        if(hasDir) newPath = newPath.concat("/");
        super.setURL(host.concat(path = newPath));
    }

    public void truncate(long position) throws IOException {
        boolean closed;
        int lim;
        File file;
        String path;
        VirtualFileSystemReadOnly place;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.truncate: соединение закрыто.");
        }
        if((mode & WRITE) == 0)
        {
            throw new IllegalModeException("FileConnection.truncate: требуется доступ для записи.");
        }
        if(position < 0L)
        {
            throw new IllegalArgumentException("FileConnection.truncate: аргумент position не может быть отрицательным.");
        }
        if((lim = (path = this.path).length() - 1) > 0 && path.charAt(lim) == '/') path = path.substring(0, lim);
        if(closed = (file = File.getOpened(place = placero, path)) == null) file = File.open(place, path, WRITE);
        try
        {
            file.truncate(position);
        }
        finally
        {
            if(closed) file.close();
        }
    }

    public void setHidden(boolean hidden) throws IOException {
        int a;
        int lim;
        String path;
        FileAttributes attrs;
        VirtualFileSystemReadWrite place;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.setHidden: соединение закрыто.");
        }
        if((mode & WRITE) == 0)
        {
            throw new IllegalModeException("FileConnection.setHidden: требуется доступ для записи.");
        }
        if((lim = (path = this.path).length() - 1) > 0 && path.charAt(lim) == '/') path = path.substring(0, lim);
        (place = placerw).readAttributes(path, attrs = new FileAttributes());
        a = attrs.getAttributes();
        if(hidden)
        {
            a |= FileAttributes.HIDDEN;
        } else
        {
            a &= ~FileAttributes.HIDDEN;
        }
        attrs.setAttributes(a, attrs.getCreationTime(), attrs.getLastWriteTime(), attrs.getLastAccessTime());
        place.writeAttributes(path, attrs);
    }

    public void setReadable(boolean readable) throws IOException {
        /* Игнорируется, canRead всегда возвращает true. */
    }

    public void setWritable(boolean writable) throws IOException {
        int a;
        int lim;
        String path;
        FileAttributes attrs;
        VirtualFileSystemReadWrite place;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.setWritable: соединение закрыто.");
        }
        if((mode & WRITE) == 0)
        {
            throw new IllegalModeException("FileConnection.setWritable: требуется доступ для записи.");
        }
        if((lim = (path = this.path).length() - 1) > 0 && path.charAt(lim) == '/') path = path.substring(0, lim);
        (place = placerw).readAttributes(path, attrs = new FileAttributes());
        a = attrs.getAttributes();
        if(writable)
        {
            a &= ~FileAttributes.READ_ONLY;
        } else
        {
            a |= FileAttributes.READ_ONLY;
        }
        attrs.setAttributes(a, attrs.getCreationTime(), attrs.getLastWriteTime(), attrs.getLastAccessTime());
        place.writeAttributes(path, attrs);
    }

    public void setFileConnection(String objectName) throws IOException {
        int len;
        String path;
        VirtualFileSystemReadOnly place;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.setFileConnection: соединение закрыто.");
        }
        if(objectName == null)
        {
            throw new NullPointerException("FileConnection.setFileConnection: аргумент objectName равен нулевой ссылке.");
        }
        if(objectName.indexOf('/') >= 0)
        {
            throw new IllegalArgumentException("FileConnection.setFileConnection: аргумент objectName содержит «/».");
        }
        if((path = this.path).endsWith("/")) path = path.substring(0, path.length() - 1);
        place = placero;
        if((len = path.length()) > 1)
        {
            FileAttributes attrs;
            place.readAttributes(path, attrs = new FileAttributes());
            if(!attrs.isDirectory())
            {
                throw new IOException("FileConnection.setFileConnection: соединение связано не с папкой.");
            }
        }
        if(".".equals(objectName)) return;
        if("..".equals(objectName))
        {
            if(len <= 0)
            {
                throw new IllegalArgumentException("FileConnection.setFileConnection: целевой путь не существует.");
            }
            if((path = path.substring(0, path.lastIndexOf('/'))).length() <= 0) path = "/";
        } else
        {
            path = (new StringBuilder()).append(path).append('/').append(objectName).toString();
        }
        if(isForbiddenPath(place, path))
        {
            throw new SecurityException("FileConnection.setFileConnection: доступ к заданному объекту запрещён для протокола file.");
        }
        if(path.length() > 1)
        {
            try
            {
                place.readAttributes(path, null);
            }
            catch(IOException e)
            {
                throw new IllegalArgumentException("FileConnection.setFileConnection: целевой путь не существует.");
            }
        }
        super.setURL(host.concat(this.path = path));
    }

    public boolean exists() {
        String path;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.exists: соединение закрыто.");
        }
        if((mode & READ) == 0)
        {
            throw new IllegalModeException("FileConnection.exists: требуется доступ для чтения.");
        }
        if((path = this.path).endsWith("/")) path = path.substring(0, path.length() - 1);
        if(path.length() > 1)
        {
            try
            {
                placero.readAttributes(path, null);
            }
            catch(IOException e)
            {
                return false;
            }
        }
        return true;
    }

    public boolean canRead() {
        return true;
    }

    public boolean canWrite() {
        String path;
        FileAttributes attrs;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.canWrite: соединение закрыто.");
        }
        if((mode & READ) == 0)
        {
            throw new IllegalModeException("FileConnection.canWrite: требуется доступ для чтения.");
        }
        if((path = this.path).endsWith("/")) path = path.substring(0, path.length() - 1);
        if(path.length() <= 0) return true;
        try
        {
            placero.readAttributes(path, attrs = new FileAttributes());
        }
        catch(IOException e)
        {
            return false;
        }
        return !attrs.isReadOnly();
    }

    public boolean isOpen() {
        return !isConnectionClosed();
    }

    public boolean isHidden() {
        String path;
        FileAttributes attrs;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.isHidden: соединение закрыто.");
        }
        if((mode & READ) == 0)
        {
            throw new IllegalModeException("FileConnection.isHidden: требуется доступ для чтения.");
        }
        if((path = this.path).endsWith("/")) path = path.substring(0, path.length() - 1);
        if(path.length() <= 0) return false;
        try
        {
            placero.readAttributes(path, attrs = new FileAttributes());
        }
        catch(IOException e)
        {
            return false;
        }
        return attrs.isHidden();
    }

    public boolean isDirectory() {
        String path;
        FileAttributes attrs;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.isDirectory: соединение закрыто.");
        }
        if((mode & READ) == 0)
        {
            throw new IllegalModeException("FileConnection.isDirectory: требуется доступ для чтения.");
        }
        if((path = this.path).endsWith("/")) path = path.substring(0, path.length() - 1);
        if(path.length() <= 0) return true;
        try
        {
            placero.readAttributes(path, attrs = new FileAttributes());
        }
        catch(IOException e)
        {
            return false;
        }
        return attrs.isDirectory();
    }

    public long usedSize() {
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.usedSize: соединение закрыто.");
        }
        if((mode & READ) == 0)
        {
            throw new IllegalModeException("FileConnection.usedSize: требуется доступ для чтения.");
        }
        throw new SecurityException("FileConnection.usedSize: доступ к корневой папке запрещён для протокола file.");
    }

    public long totalSize() {
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.totalSize: соединение закрыто.");
        }
        if((mode & READ) == 0)
        {
            throw new IllegalModeException("FileConnection.totalSize: требуется доступ для чтения.");
        }
        throw new SecurityException("FileConnection.totalSize: доступ к корневой папке запрещён для протокола file.");
    }

    public long availableSize() {
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.availableSize: соединение закрыто.");
        }
        if((mode & READ) == 0)
        {
            throw new IllegalModeException("FileConnection.availableSize: требуется доступ для чтения.");
        }
        throw new SecurityException("FileConnection.availableSize: доступ к корневой папке запрещён для протокола file.");
    }

    public long lastModified() {
        String path;
        FileAttributes attrs;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.lastModified: соединение закрыто.");
        }
        if((mode & READ) == 0)
        {
            throw new IllegalModeException("FileConnection.lastModified: требуется доступ для чтения.");
        }
        if((path = this.path).endsWith("/")) path = path.substring(0, path.length() - 1);
        try
        {
            placero.readAttributes(path, attrs = new FileAttributes());
        }
        catch(IOException e)
        {
            return 0L;
        }
        return attrs.getLastWriteTime();
    }

    public long fileSize() throws IOException {
        long result;
        String path;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.fileSize: соединение закрыто.");
        }
        if((mode & READ) == 0)
        {
            throw new IllegalModeException("FileConnection.fileSize: требуется доступ для чтения.");
        }
        if((path = this.path).endsWith("/")) path = path.substring(0, path.length() - 1);
        if(path.length() <= 0)
        {
            throw new IOException("FileConnection.fileSize: соединение связано с папкой.");
        }
        try
        {
            FileEnumeration enumr;
            if((enumr = placero.findFirst(path)) != null)
            {
                try
                {
                    result = enumr.isDirectory() ? -2L : enumr.getSize();
                }
                finally
                {
                    enumr.close();
                }
            } else
            {
                result = -1L;
            }
        }
        catch(IOException e)
        {
            result = -1L;
        }
        if(result == -2L)
        {
            throw new IOException("FileConnection.fileSize: соединение связано с папкой.");
        }
        return result;
    }

    public long directorySize(boolean includeSubDirectories) throws IOException {
        long result;
        String path;
        String pathd;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.directorySize: соединение закрыто.");
        }
        if((mode & READ) == 0)
        {
            throw new IllegalModeException("FileConnection.directorySize: требуется доступ для чтения.");
        }
        if((pathd = this.path).endsWith("/"))
        {
            path = pathd.substring(0, pathd.length() - 1);
        } else
        {
            pathd = (path = pathd).concat("/");
        }
        try
        {
            if(path.length() > 0)
            {
                FileAttributes attrs;
                VirtualFileSystemReadOnly place;
                (place = placero).readAttributes(path, attrs = new FileAttributes());
                result = attrs.isDirectory() ? directorySize(place, pathd, includeSubDirectories) : -2L;
            } else
            {
                result = directorySize(placero, "/", includeSubDirectories);
            }
        }
        catch(IOException e)
        {
            result = -1L;
        }
        if(result == -2L)
        {
            throw new IOException("FileConnection.directorySize: соединение связано не с папкой.");
        }
        return result;
    }

    public InputStream openInputStream() throws IOException {
        int mode;
        File file;
        String path;
        FileAttributes attrs;
        VirtualFileSystemReadOnly place;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.openInputStream: соединение закрыто.");
        }
        if(((mode = this.mode) & READ) == 0)
        {
            throw new IllegalModeException("FileConnection.openInputStream: требуется доступ для чтения.");
        }
        if((path = this.path).endsWith("/")) path = path.substring(0, path.length() - 1);
        if(path.length() <= 0)
        {
            throw new IOException("FileConnection.openInputStream: соединение связано с папкой.");
        }
        (place = placero).readAttributes(path, attrs = new FileAttributes());
        if(attrs.isDirectory())
        {
            throw new IOException("FileConnection.openInputStream: соединение связано с папкой.");
        }
        return ((file = File.getOpened(place, path)) != null ? file : File.open(place, path, mode)).openInputStream();
    }

    public DataInputStream openDataInputStream() throws IOException {
        InputStream result = openInputStream();
        return new DataInputStream(result);
    }

    public OutputStream openOutputStream() throws IOException {
        int mode;
        File file;
        String path;
        FileAttributes attrs;
        VirtualFileSystemReadOnly place;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.openOutputStream: соединение закрыто.");
        }
        if(((mode = this.mode) & WRITE) == 0)
        {
            throw new IllegalModeException("FileConnection.openOutputStream: требуется доступ для записи.");
        }
        if((path = this.path).endsWith("/")) path = path.substring(0, path.length() - 1);
        if(path.length() <= 0)
        {
            throw new IOException("FileConnection.openOutputStream: соединение связано с папкой.");
        }
        (place = placero).readAttributes(path, attrs = new FileAttributes());
        if(attrs.isDirectory())
        {
            throw new IOException("FileConnection.openOutputStream: соединение связано с папкой.");
        }
        return ((file = File.getOpened(place, path)) != null ? file : File.open(place, path, mode)).openOutputStream();
    }

    public OutputStream openOutputStream(long position) throws IOException {
        int mode;
        File file;
        String path;
        FileAttributes attrs;
        VirtualFileSystemReadOnly place;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.openOutputStream: соединение закрыто.");
        }
        if(((mode = this.mode) & WRITE) == 0)
        {
            throw new IllegalModeException("FileConnection.openOutputStream: требуется доступ для записи.");
        }
        if((path = this.path).endsWith("/")) path = path.substring(0, path.length() - 1);
        if(path.length() <= 0)
        {
            throw new IOException("FileConnection.openOutputStream: соединение связано с папкой.");
        }
        (place = placero).readAttributes(path, attrs = new FileAttributes());
        if(attrs.isDirectory())
        {
            throw new IOException("FileConnection.openOutputStream: соединение связано с папкой.");
        }
        return ((file = File.getOpened(place, path)) != null ? file : File.open(place, path, mode)).openOutputStream(position);
    }

    public DataOutputStream openDataOutputStream() throws IOException {
        OutputStream result = openOutputStream();
        return new DataOutputStream(result);
    }

    public Enumeration list(String filter, boolean includeHidden) throws IOException {
        int len;
        String[] filter1matches;
        String[] filter2matches;
        String path;
        String pathd;
        Vector result;
        String filter2;
        FileAttributes attrs;
        FileEnumeration enumr;
        VirtualFileSystemReadOnly place;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.list: соединение закрыто.");
        }
        if((mode & READ) == 0)
        {
            throw new IllegalModeException("FileConnection.list: требуется доступ для чтения.");
        }
        if(filter == null)
        {
            throw new NullPointerException("FileConnection.list: аргумент filter равен нулевой ссылке.");
        }
        if(filter.indexOf('/') >= 0)
        {
            throw new IllegalArgumentException("FileConnection.list: аргумент filter содержит «/».");
        }
        if((len = filter.length()) <= 0) len = (filter = "*").length();
        filter2 = filter.endsWith(".*") ? filter.substring(0, len - ".*".length()) : null;
        if((pathd = this.path).endsWith("/"))
        {
            path = pathd.substring(0, pathd.length() - 1);
        } else
        {
            pathd = (path = pathd).concat("/");
        }
        place = placero;
        if(path.length() > 1)
        {
            place.readAttributes(path, attrs = new FileAttributes());
            if(!attrs.isDirectory())
            {
                throw new IOException("FileConnection.list: соединение связано не с папкой.");
            }
        }
        filter1matches = getMatches(filter);
        filter2matches = getMatches(filter2);
        result = new Vector();
        if((enumr = place.findFirst(pathd)) != null)
        {
            try
            {
                do
                {
                    if(includeHidden || !enumr.isHidden())
                    {
                        boolean dir = enumr.isDirectory();
                        String name = enumr.getName();
                        if(dir || isMatches(name, filter1matches) || isMatches(name, filter2matches)) result.addElement(dir ? name.concat("/") : name);
                    }
                } while(enumr.findNext());
            }
            finally
            {
                enumr.close();
            }
        }
        return result.elements();
    }

    public Enumeration list() throws IOException {
        String path;
        String pathd;
        Vector result;
        FileAttributes attrs;
        FileEnumeration enumr;
        VirtualFileSystemReadOnly place;
        if(isConnectionClosed())
        {
            throw new ConnectionClosedException("FileConnection.list: соединение закрыто.");
        }
        if((mode & READ) == 0)
        {
            throw new IllegalModeException("FileConnection.list: требуется доступ для чтения.");
        }
        if((pathd = this.path).endsWith("/"))
        {
            path = pathd.substring(0, pathd.length() - 1);
        } else
        {
            pathd = (path = pathd).concat("/");
        }
        place = placero;
        if(path.length() > 1)
        {
            place.readAttributes(path, attrs = new FileAttributes());
            if(!attrs.isDirectory())
            {
                throw new IOException("FileConnection.list: соединение связано не с папкой.");
            }
        }
        result = new Vector();
        if((enumr = place.findFirst(pathd)) != null)
        {
            try
            {
                do
                {
                    if(!enumr.isHidden())
                    {
                        String name = enumr.getName();
                        result.addElement(enumr.isDirectory() ? name.concat("/") : name);
                    }
                } while(enumr.findNext());
            }
            finally
            {
                enumr.close();
            }
        }
        return result.elements();
    }

    public String getName() {
        String path;
        return (path = this.path).substring(path.lastIndexOf('/', path.length() - (path.endsWith("/") ? 2 : 1)) + 1);
    }

    public String getPath() {
        String path;
        return (path = this.path).substring(0, path.lastIndexOf('/', path.length() - (path.endsWith("/") ? 2 : 1)) + 1);
    }
}

final class File extends Object
{
    private static final class PlacePathPair extends Object
    {
        public final Object place;
        public final String path;

        public PlacePathPair(Object place, String path) {
            this.place = place;
            this.path = path;
        }

        public boolean equals(Object anot) {
            PlacePathPair ppp;
            return anot instanceof PlacePathPair && place == (ppp = (PlacePathPair) anot).place && path.equals(ppp.path);
        }

        public int hashCode() {
            return System.identityHashCode(place) ^ path.hashCode();
        }
    }

    public static final int READ = 1;
    public static final int WRITE = 2;
    public static final int READ_WRITE = 3;

    private static final Hashtable OPENED;

    static {
        OPENED = new Hashtable();
    }

    public static File getOpened(VirtualFileSystemReadOnly place, String path) {
        return (File) OPENED.get(new PlacePathPair(place, path));
    }

    public static File open(VirtualFileSystemReadOnly place, String path, int mode) throws IOException {
        File result;
        PlacePathPair pair;
        IOStream openedIOStream;
        InputStream sourceInputStream;
        OutputStream destinationOutputStream;
        if(place == null) place = CloudFileSystem.instance;
        if(path == null)
        {
            throw new NullPointerException("File.open: аргумент path равен нулевой ссылке.");
        }
        if(mode < READ || mode > READ_WRITE)
        {
            throw new IllegalArgumentException("File.open: аргумент mode имеет недопустимое значение.");
        }
        if((mode & WRITE) != 0 && !(place instanceof VirtualFileSystemReadWrite))
        {
            throw new IllegalArgumentException("File.open: аргумент place имеет недопустимое значение.");
        }
        if(OPENED.get(pair = new PlacePathPair(place, path)) != null)
        {
            throw new FileNotFoundException((new StringBuilder()).append("File.open: файл ").append(path).append(" не найден или занят.").toString(), path);
        }
        if(mode == READ)
        {
            openedIOStream = null;
            sourceInputStream = place.openFileForRead(path);
            destinationOutputStream = null;
        } else
        {
            openedIOStream = ((VirtualFileSystemReadWrite) place).openFileForReadWrite(path);
            sourceInputStream = mode == READ_WRITE ? openedIOStream.getInputStream() : null;
            destinationOutputStream = openedIOStream.getOutputStream();
        }
        (result = new File(place, path, mode)).openedIOStream = openedIOStream;
        if(sourceInputStream != null) result.openedInputStream = result.new LocalInputStream(sourceInputStream);
        if(destinationOutputStream != null) result.openedOutputStream = result.new LocalOutputStream(destinationOutputStream);
        OPENED.put(pair, result);
        return result;
    }

    private final class LocalInputStream extends InputStream
    {
        final InputStream stream;

        public LocalInputStream(InputStream stream) {
            this.stream = stream;
        }

        public void close() throws IOException {
            File owner;
            synchronized((owner = File.this).monitor)
            {
                if(owner.hasInputStream)
                {
                    owner.hasInputStream = false;
                    if(!owner.hasOutputStream) owner.fastClose();
                }
            }
        }

        public void reset() throws IOException {
            if(!(File.this).hasInputStream)
            {
                throw new IOException("InputStream.reset: поток ввода закрыт.");
            }
            stream.reset();
        }

        public void mark(int readLimit) {
            stream.mark(readLimit);
        }

        public boolean markSupported() {
            return stream.markSupported();
        }

        public int available() throws IOException {
            if(!(File.this).hasInputStream)
            {
                throw new IOException("InputStream.available: поток ввода закрыт.");
            }
            return stream.available();
        }

        public int read() throws IOException {
            if(!(File.this).hasInputStream)
            {
                throw new IOException("InputStream.read: поток ввода закрыт.");
            }
            return stream.read();
        }

        public int read(byte[] dst) throws IOException {
            if(!(File.this).hasInputStream)
            {
                throw new IOException("InputStream.read: поток ввода закрыт.");
            }
            return stream.read(dst);
        }

        public int read(byte[] dst, int offset, int length) throws IOException {
            if(!(File.this).hasInputStream)
            {
                throw new IOException("InputStream.read: поток ввода закрыт.");
            }
            return stream.read(dst, offset, length);
        }

        public long skip(long quantity) throws IOException {
            if(!(File.this).hasInputStream)
            {
                throw new IOException("InputStream.skip: поток ввода закрыт.");
            }
            return stream.skip(quantity);
        }

        public String toString() {
            return stream.toString();
        }
    }

    private final class LocalOutputStream extends OutputStream
    {
        final OutputStream stream;

        public LocalOutputStream(OutputStream stream) {
            this.stream = stream;
        }

        public void close() throws IOException {
            File owner;
            synchronized((owner = File.this).monitor)
            {
                if(owner.hasOutputStream)
                {
                    owner.hasOutputStream = false;
                    if(!owner.hasInputStream) owner.fastClose();
                }
            }
        }

        public void flush() throws IOException {
            if(!(File.this).hasOutputStream)
            {
                throw new IOException("OutputStream.flush: поток вывода закрыт.");
            }
            stream.flush();
        }

        public void write(int byteData) throws IOException {
            if(!(File.this).hasOutputStream)
            {
                throw new IOException("OutputStream.write: поток вывода закрыт.");
            }
            stream.write(byteData);
        }

        public void write(byte[] src) throws IOException {
            if(!(File.this).hasOutputStream)
            {
                throw new IOException("OutputStream.write: поток вывода закрыт.");
            }
            stream.write(src);
        }

        public void write(byte[] src, int offset, int length) throws IOException {
            if(!(File.this).hasOutputStream)
            {
                throw new IOException("OutputStream.write: поток вывода закрыт.");
            }
            stream.write(src, offset, length);
        }

        public String toString() {
            return stream.toString();
        }
    }

    boolean closed;
    boolean hasInputStream;
    boolean hasOutputStream;
    private final int mode;
    private final String path;
    private IOStream openedIOStream;
    private LocalInputStream openedInputStream;
    private LocalOutputStream openedOutputStream;
    private final VirtualFileSystemReadOnly place;
    final Object monitor;

    private File(VirtualFileSystemReadOnly place, String path, int mode) {
        this.mode = mode;
        this.path = path;
        this.place = place;
        this.monitor = new Object();
    }

    public void close() throws IOException {
        synchronized(monitor)
        {
            if(!closed) fastClose();
        }
    }

    public void truncate(long position) throws IOException {
        int error = 0;
        long pos;
        IOStream stream = null;
        synchronized(monitor)
        {
            label0:
            {
                if(closed)
                {
                    error = 1;
                    break label0;
                }
                if((mode & WRITE) == 0)
                {
                    error = 2;
                    break label0;
                }
                stream = openedIOStream;
            }
        }
        switch(error)
        {
        case 1:
            throw new ClosedFileException("FileConnection.truncate: файл закрыт.");
        case 2:
            throw new IllegalModeException("FileConnection.truncate: требуется доступ для записи.");
        }
        pos = stream.position();
        stream.resetOutputStream();
        stream.position(position);
        stream.truncate();
        stream.resetOutputStream();
        stream.position(pos);
    }

    public int getMode() {
        return mode;
    }

    public InputStream openInputStream() throws IOException {
        int error = 0;
        InputStream result;
        synchronized(monitor)
        {
            label0:
            {
                IOStream stream;
                if(closed)
                {
                    error = 1;
                    result = null;
                    break label0;
                }
                if((mode & READ) == 0)
                {
                    error = 2;
                    result = null;
                    break label0;
                }
                if(hasInputStream)
                {
                    error = 3;
                    result = null;
                    break label0;
                }
                if((stream = openedIOStream) != null) stream.resetInputStream();
                hasInputStream = true;
                result = openedInputStream;
            }
        }
        switch(error)
        {
        case 1:
            throw new ClosedFileException("FileConnection.openInputStream: файл закрыт.");
        case 2:
            throw new IllegalModeException("FileConnection.openInputStream: требуется доступ для чтения.");
        case 3:
            throw new IOException("FileConnection.openInputStream: поток ввода уже был открыт раннее.");
        }
        return result;
    }

    public OutputStream openOutputStream() throws IOException {
        int error = 0;
        OutputStream result;
        synchronized(monitor)
        {
            label0:
            {
                if(closed)
                {
                    error = 1;
                    result = null;
                    break label0;
                }
                if((mode & WRITE) == 0)
                {
                    error = 2;
                    result = null;
                    break label0;
                }
                if(hasOutputStream)
                {
                    error = 3;
                    result = null;
                    break label0;
                }
                openedIOStream.resetOutputStream();
                hasOutputStream = true;
                result = openedOutputStream;
            }
        }
        switch(error)
        {
        case 1:
            throw new ClosedFileException("FileConnection.openOutputStream: файл закрыт.");
        case 2:
            throw new IllegalModeException("FileConnection.openOutputStream: требуется доступ для записи.");
        case 3:
            throw new IOException("FileConnection.openOutputStream: поток вывода уже был открыт раннее.");
        }
        return result;
    }

    public InputStream openInputStream(long position) throws IOException {
        int error = 0;
        InputStream result;
        synchronized(monitor)
        {
            label0:
            {
                IOStream stream;
                if(closed)
                {
                    error = 1;
                    result = null;
                    break label0;
                }
                if((mode & READ) == 0)
                {
                    error = 2;
                    result = null;
                    break label0;
                }
                if(hasInputStream)
                {
                    error = 3;
                    result = null;
                    break label0;
                }
                if((stream = openedIOStream) != null) stream.resetInputStream();
                hasInputStream = true;
                (result = openedInputStream).skip(position);
            }
        }
        switch(error)
        {
        case 1:
            throw new ClosedFileException("FileConnection.openInputStream: файл закрыт.");
        case 2:
            throw new IllegalModeException("FileConnection.openInputStream: требуется доступ для чтения.");
        case 3:
            throw new IOException("FileConnection.openInputStream: поток ввода уже был открыт раннее.");
        }
        return result;
    }

    public OutputStream openOutputStream(long position) throws IOException {
        int error = 0;
        OutputStream result;
        synchronized(monitor)
        {
            label0:
            {
                IOStream stream;
                if(closed)
                {
                    error = 1;
                    result = null;
                    break label0;
                }
                if((mode & WRITE) == 0)
                {
                    error = 2;
                    result = null;
                    break label0;
                }
                if(hasOutputStream)
                {
                    error = 3;
                    result = null;
                    break label0;
                }
                (stream = openedIOStream).resetOutputStream();
                stream.position(position);
                hasOutputStream = true;
                result = openedOutputStream;
            }
        }
        switch(error)
        {
        case 1:
            throw new ClosedFileException("FileConnection.openOutputStream: файл закрыт.");
        case 2:
            throw new IllegalModeException("FileConnection.openOutputStream: требуется доступ для записи.");
        case 3:
            throw new IOException("FileConnection.openOutputStream: поток вывода уже был открыт раннее.");
        }
        return result;
    }

    public String getPath() {
        return path;
    }

    public InputStream getInputStream() {
        return hasInputStream ? openedInputStream : null;
    }

    public OutputStream getOutputStream() {
        return hasOutputStream ? openedOutputStream : null;
    }

    public VirtualFileSystemReadOnly getPlace() {
        return place;
    }

    void fastClose() throws IOException {
        IOStream io = openedIOStream;
        LocalInputStream in = openedInputStream;
        OPENED.remove(new PlacePathPair(place, path));
        closed = true;
        hasInputStream = false;
        hasOutputStream = false;
        openedIOStream = null;
        openedInputStream = null;
        openedOutputStream = null;
        if(io != null)
        {
            io.close();
        } else
        {
            in.stream.close();
        }
    }
}
