/* * This file is a part of Alchemy OS project. * Copyright (C) 2011-2014, Sergey Basalaev <sbasalaev@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package alchemy.fs.rms; import alchemy.fs.FSDriver; import alchemy.fs.Filesystem; import alchemy.util.HashMap; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import javax.microedition.rms.RecordStore; import javax.microedition.rms.RecordStoreException; import javax.microedition.rms.RecordStoreFullException; import javax.microedition.rms.RecordStoreNotFoundException; /** * Filesystem implementation based on a record storage system. * This filesystem is suitable for all MIDP devices, but is * rather slow. * @author Sergey Basalaev */ public final class Driver extends FSDriver { // file attributes static private int A_DIR = 16; static private int A_READ = 4; static private int A_WRITE = 2; static private int A_EXEC = 1; /** <code>RecordStore</code> to use as filesystem. */ private RecordStore store; /** File descriptor for the root directory. */ private final FD rootFD; /** * Cache of file descriptors. * Maps file name to descriptor. */ private HashMap fdCache = new HashMap(); /** * Constructor to load through the reflection. * <code>init()</code> should be called before * filesystem can be used. */ public Driver() { // initializing root file descriptor FD fd = new FD(); fd.name = "/"; fd.record = 1; fd.attrs = A_READ | A_WRITE | A_DIR; rootFD = fd; } public void init(String name) throws IOException { // initializing record store RecordStore rs = null; try { try { rs = RecordStore.openRecordStore(name, false); } catch (RecordStoreNotFoundException rsnfe) { rs = RecordStore.openRecordStore(name, true); rs.addRecord(new byte[12], 0, 12); } } catch (RecordStoreException rse) { throw new RuntimeException(rse.toString()); } this.store = rs; } public void close() { try { store.closeRecordStore(); } catch (RecordStoreException rse) { } } public synchronized void create(String file) throws IOException { String parent = Filesystem.fileParent(file); if (parent == null) throw new IOException("File already exists: /"); FD parentfd = getFD(parent); if ((parentfd.attrs & A_DIR) == 0) throw new IOException("Not a directory: "+parent); Directory dir = new Directory(parentfd); int index = dir.getIndex(Filesystem.fileName(file)); if (index >= 0) throw new IOException("File already exists: "+file); FD fd = new FD(); fd.name = Filesystem.fileName(file); fd.attrs = A_READ | A_WRITE; fd.record = createFileNode(false); dir.nodes[dir.count] = fd; dir.count++; dir.flush(); fdCache.set(file, fd); } public synchronized void mkdir(String file) throws IOException { String parent = Filesystem.fileParent(file); if (parent == null) throw new IOException("File already exists: /"); FD parentfd = getFD(parent); if ((parentfd.attrs & A_DIR) == 0) throw new IOException("Not a directory: "+parent); Directory dir = new Directory(parentfd); int index = dir.getIndex(Filesystem.fileName(file)); if (index >= 0) throw new IOException("File already exists: "+file); FD fd = new FD(); fd.name = Filesystem.fileName(file); fd.attrs = A_READ | A_WRITE | A_DIR; fd.record = createFileNode(true); dir.nodes[dir.count] = fd; dir.count++; dir.flush(); fdCache.set(file, fd); } public synchronized InputStream read(String file) throws IOException { FD fd = getFD(file); if ((fd.attrs & A_DIR) != 0) throw new IOException("Is a directory: "+file); return new FileInputStream(fd); } public synchronized OutputStream write(String file) throws IOException { if (!exists(file)) create(file); FD fd = getFD(file); if ((fd.attrs & A_DIR) != 0) throw new IOException("Is a directory: "+file); return new FileOutputStream(fd, false); } public synchronized OutputStream append(String file) throws IOException { if (!exists(file)) create(file); FD fd = getFD(file); if ((fd.attrs & A_DIR) != 0) throw new IOException("Is a directory: "+file); return new FileOutputStream(fd, true); } public synchronized void remove(String file) throws IOException { String parent = Filesystem.fileParent(file); if (parent == null) throw new IOException("Cannot remove /"); FD parentfd = getFD(parent); if ((parentfd.attrs & A_DIR) == 0) throw new IOException("Not a directory: "+parent); Directory dir = new Directory(parentfd); int index = dir.getIndex(Filesystem.fileName(file)); if (index < 0) return; if ((dir.nodes[index].attrs & A_DIR) != 0) { Directory toRemove = new Directory(dir.nodes[index]); if (toRemove.count > 0) throw new IOException("Cannot remove non-empty directory: "+file); } try { store.deleteRecord(dir.nodes[index].record); } catch (RecordStoreException rse) { throw new IOException(rse.toString()); } dir.count--; dir.nodes[index] = dir.nodes[dir.count]; dir.flush(); fdCache.remove(file); } public synchronized String[] list(String file) throws IOException { FD fd = getFD(file); if ((fd.attrs & A_DIR) == 0) throw new IOException("Not a directory: "+file); Directory dir = new Directory(fd); String[] list = new String[dir.count]; for (int i=0; i<dir.count; i++) { FD node = dir.nodes[i]; list[i] = node.name; if ((node.attrs & A_DIR) != 0) list[i] = list[i].concat("/"); } return list; } public synchronized boolean exists(String file) { try { getFD(file); return true; } catch (IOException e) { return false; } } public synchronized boolean isDirectory(String file) { try { FD fd = getFD(file); return (fd.attrs & A_DIR) != 0; } catch (IOException e) { return false; } } public synchronized boolean canRead(String file) { try { FD fd = getFD(file); return (fd.attrs & A_READ) != 0; } catch (IOException e) { return false; } } public synchronized boolean canWrite(String file) { try { FD fd = getFD(file); return (fd.attrs & A_WRITE) != 0; } catch (IOException e) { return false; } } public synchronized boolean canExec(String file) { try { FD fd = getFD(file); return (fd.attrs & A_EXEC) != 0; } catch (IOException e) { return false; } } public synchronized void setRead(String file, boolean on) throws IOException { String parent = Filesystem.fileParent(file); if (parent == null) throw new IOException("Cannot change attrubutes of /"); FD parentfd = getFD(parent); if ((parentfd.attrs & A_DIR) == 0) throw new IOException("Not a directory: "+parent); Directory dir = new Directory(parentfd); int index = dir.getIndex(Filesystem.fileName(file)); if (index < 0) throw new IOException("File not found: "+file); FD fd = dir.nodes[index]; if (on) { if ((fd.attrs & A_READ) == 0) { fd.attrs |= A_READ; dir.flush(); fdCache.set(file, fd); } } else { if ((fd.attrs & A_READ) != 0) { fd.attrs &= ~A_READ; dir.flush(); fdCache.set(file, fd); } } } public synchronized void setWrite(String file, boolean on) throws IOException { String parent = Filesystem.fileParent(file); if (parent == null) throw new IOException("Cannot change attrubutes of /"); FD parentfd = getFD(parent); if ((parentfd.attrs & A_DIR) == 0) throw new IOException("Not a directory: "+parent); Directory dir = new Directory(parentfd); int index = dir.getIndex(Filesystem.fileName(file)); if (index < 0) throw new IOException("File not found: "+file); FD fd = dir.nodes[index]; if (on) { if ((fd.attrs & A_WRITE) == 0) { fd.attrs |= A_WRITE; dir.flush(); fdCache.set(file, fd); } } else { if ((fd.attrs & A_WRITE) != 0) { fd.attrs &= ~A_WRITE; dir.flush(); fdCache.set(file, fd); } } } public synchronized void setExec(String file, boolean on) throws IOException { String parent = Filesystem.fileParent(file); if (parent == null) throw new IOException("Cannot change attrubutes of /"); FD parentfd = getFD(parent); if ((parentfd.attrs & A_DIR) == 0) throw new IOException("Not a directory: "+parent); Directory dir = new Directory(parentfd); int index = dir.getIndex(Filesystem.fileName(file)); if (index < 0) throw new IOException("File not found: "+file); FD fd = dir.nodes[index]; if (on) { if ((fd.attrs & A_EXEC) == 0) { fd.attrs |= A_EXEC; dir.flush(); fdCache.set(file, fd); } } else { if ((fd.attrs & A_EXEC) != 0) { fd.attrs &= ~A_EXEC; dir.flush(); fdCache.set(file, fd); } } } public synchronized void move(String source, String dest) throws IOException { String srcparent = Filesystem.fileParent(source); if (srcparent == null) throw new IOException("Cannot move /"); if (exists(dest)) throw new IOException("Cannot move "+source+" to "+dest+", destination exists."); if (srcparent.equals(Filesystem.fileParent(dest))) { // rename within one directory FD parentfd = getFD(srcparent); Directory parentdir = new Directory(parentfd); int index = parentdir.getIndex(Filesystem.fileName(source)); if (index < 0) throw new IOException("File not found: "+source); FD destfd = parentdir.nodes[index]; destfd.name = Filesystem.fileName(dest); parentdir.flush(); // apply changes to cache fdCache.remove(source); fdCache.set(dest, destfd); } else { // read both dirs FD destdirfd = getFD(Filesystem.fileParent(dest)); Directory destdir = new Directory(destdirfd); FD srcdirfd = getFD(Filesystem.fileParent(source)); Directory srcdir = new Directory(srcdirfd); int index = srcdir.getIndex(Filesystem.fileName(source)); if (index < 0) throw new IOException("File not found: "+source); // move / rename FD destfd = srcdir.nodes[index]; destfd.name = Filesystem.fileName(dest); destdir.nodes[destdir.count] = destfd; destdir.count++; srcdir.count--; srcdir.nodes[index] = srcdir.nodes[srcdir.count]; destdir.flush(); srcdir.flush(); // apply changes to cache fdCache.remove(source); fdCache.set(dest, destfd); } } public long size(String file) throws IOException { FD fd = getFD(file); try { return store.getRecordSize(fd.record)-8; } catch (RecordStoreException rse) { throw new IOException(rse.toString()); } } public long lastModified(String file) throws IOException { FD fd = getFD(file); FileInputStream stream = new FileInputStream(fd); long stamp = stream.timeStamp(); stream.close(); return stamp; } public long spaceFree() { try { return store.getSizeAvailable(); } catch (RecordStoreException rse) { return 0L; } } public long spaceTotal() { return spaceFree() + spaceUsed(); } public long spaceUsed() { try { return store.getSize(); } catch (RecordStoreException rse) { return 0L; } } private FD getFD(String file) throws IOException { String parent = Filesystem.fileParent(file); if (parent == null) return rootFD; FD parentfd = getFD(parent); if ((parentfd.attrs & A_DIR) == 0) throw new IOException("Not a directory: "+parent); if ((parentfd.attrs & A_READ) == 0) throw new IOException("Access denied to "+parent); FD fd = (FD)fdCache.get(file); if (fd != null) { return fd; } else { Directory dir = new Directory(parentfd); int index = dir.getIndex(Filesystem.fileName(file)); if (index < 0) throw new IOException("File not found: "+file); fd = dir.nodes[index]; fdCache.set(file, fd); return fd; } } private int createFileNode(boolean isDir) throws IOException { byte[] buf = new byte[12]; long time = System.currentTimeMillis(); for (int i=7; i>=0; i--) { buf[i] = (byte)time; time >>>= 8; } try { return store.addRecord(buf, 0, isDir ? 12 : 8); } catch (RecordStoreFullException rsfe) { throw new IOException("Cannot create file, no more free space."); } catch (RecordStoreException rse) { throw new IOException(rse.toString()); } } /** File descriptor */ private static class FD { int record; int attrs; String name; } private class Directory { public FD fd; public int count; public FD[] nodes; public Directory(FD fd) throws IOException { this.fd = fd; DataInputStream stream = new DataInputStream(new FileInputStream(fd)); count = stream.readUnsignedShort(); nodes = new FD[count+1]; for (int i=0; i<count; i++) { FD node = new FD(); node.record = stream.readInt(); node.attrs = stream.readUnsignedByte(); node.name = stream.readUTF(); nodes[i] = node; } stream.close(); } public int getIndex(String name) { for (int i=count-1; i>=0; i--) { if (nodes[i].name.equals(name)) return i; } return -1; } public void flush() throws IOException { DataOutputStream stream = new DataOutputStream(new FileOutputStream(fd, false)); stream.writeShort(count); for (int i=0; i<count; i++) { FD node = nodes[i]; stream.writeInt(node.record); stream.writeByte(node.attrs); stream.writeUTF(node.name); } stream.close(); } } private class FileInputStream extends InputStream { private byte[] buf; private int mark = 8; private int pos = 8; public FileInputStream(FD fd) throws IOException { if ((fd.attrs & A_READ) == 0) throw new IOException("Access denied to "+fd.name); try { buf = store.getRecord(fd.record); } catch (RecordStoreException rse) { throw new IOException(rse.toString()); } } public synchronized int read() throws IOException { if (buf == null) throw new IOException("Stream is closed"); if (pos == buf.length) return -1; return buf[pos++] & 0xff; } public synchronized int read(byte[] b, int off, int len) throws IOException { if (buf == null) throw new IOException("Stream is closed"); if (off < 0 || len < 0 || off+len > b.length) throw new ArrayIndexOutOfBoundsException(); if (len == 0) return 0; if (pos == buf.length) return -1; int reallen = buf.length-pos; if (reallen == 0) return -1; if (reallen > len) reallen = len; System.arraycopy(buf, pos, b, off, reallen); pos += reallen; return reallen; } public int available() throws IOException { if (buf == null) throw new IOException("Stream is closed"); return buf.length - pos; } public synchronized void close() { buf = null; } public synchronized void mark(int readlimit) { mark = pos; } public boolean markSupported() { return true; } public synchronized void reset() { pos = mark; } public synchronized long skip(long n) throws IOException { if (buf == null) throw new IOException("Stream is closed"); if (n <= 0) return 0; long realskip = buf.length-pos; if (realskip > n) realskip = n; pos += realskip; return realskip; } public long timeStamp() { long time = 0; for (int i=0; i<8; i++) { time = (time << 8) | (buf[i] & 0xff); } return time; } } private class FileOutputStream extends OutputStream { private final FD fd; private byte[] buf; private int count; private boolean modified; public FileOutputStream(FD fd, boolean append) throws IOException { if ((fd.attrs & A_WRITE) == 0) throw new IOException("Access denied to "+fd.name); this.fd = fd; if (append) { try { count = store.getRecordSize(fd.record); buf = new byte[count+32]; store.getRecord(fd.record, buf, 0); modified = false; } catch (RecordStoreException rse) { throw new IOException(rse.toString()); } } else { buf = new byte[40]; count = 8; modified = true; } } public synchronized void write(int b) throws IOException { if (buf == null) throw new IOException("Stream is closed"); if (buf.length == count) grow(count << 1); buf[count] = (byte)b; count++; modified = true; } public synchronized void write(byte[] b, int off, int len) throws IOException { if (buf == null) throw new IOException("Stream is closed"); if (off < 0 || len < 0 || off+len > b.length) throw new ArrayIndexOutOfBoundsException(); if (buf.length < count+len) grow(buf.length+len); System.arraycopy(b, off, buf, count, len); count += len; modified = true; } public synchronized void flush() throws IOException { if (buf == null) throw new IOException("Stream is closed"); if ((fd.attrs & A_WRITE) == 0) throw new IOException("Access denied to "+fd.name); if (modified) { long stamp = System.currentTimeMillis(); for (int i=7; i >= 0; i--) { buf[i] = (byte)(stamp); stamp >>>= 8; } try { synchronized (Driver.this) { store.setRecord(fd.record, buf, 0, count); } } catch (RecordStoreException rse) { throw new IOException(rse.toString()); } modified = false; } } public synchronized void close() { if (buf == null) return; try { flush(); } catch (IOException ioe) { } buf = null; } private void grow(int len) { byte[] newbuf = new byte[len]; System.arraycopy(buf, 8, newbuf, 8, count-8); buf = newbuf; } } }