diff options
author | Xavier Ducrohet <xav@android.com> | 2012-10-24 17:02:47 -0700 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2012-10-24 17:02:47 -0700 |
commit | 0a6d8750c4330ad793f3beae670eb21e0e1d401b (patch) | |
tree | 226729f6f6181b6c867fbf6d06e89ca8a1dfcf6d /src/main/java | |
parent | 6950a7fcfda2d03446514e315b6bf6bd5c476496 (diff) | |
parent | 67fe07b509b35defe3a8c80d65ad5ae859ac354a (diff) | |
download | fat32lib-0a6d8750c4330ad793f3beae670eb21e0e1d401b.tar.gz |
Merge changes I50294fcc,Ie59bda0ctools_r21
* changes:
Added LGPL 2.1 NOTICE file and MODULE_LICENSE
External library with modifications. fat32-lib. Signed-off-by: Dan Galpin <dgalpin@google.com>
Diffstat (limited to 'src/main/java')
39 files changed, 7232 insertions, 0 deletions
diff --git a/src/main/java/de/waldheinz/fs/AbstractFileSystem.java b/src/main/java/de/waldheinz/fs/AbstractFileSystem.java new file mode 100644 index 0000000..85be4db --- /dev/null +++ b/src/main/java/de/waldheinz/fs/AbstractFileSystem.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs; + +import java.io.IOException; + +/** + * Abstract class with common things in different FileSystem implementations. + * + * @author Fabien DUMINY + * @author Matthias Treydte <waldheinz at gmail.com> + */ +public abstract class AbstractFileSystem implements FileSystem { + private final boolean readOnly; + private boolean closed; + + /** + * Creates a new {@code AbstractFileSystem}. + * + * @param readOnly if the file system should be read-only + */ + public AbstractFileSystem(boolean readOnly) { + this.closed = false; + this.readOnly = readOnly; + } + + @Override + public void close() throws IOException { + if (!isClosed()) { + if (!isReadOnly()) { + flush(); + } + + closed = true; + } + } + + @Override + public final boolean isClosed() { + return closed; + } + + @Override + public final boolean isReadOnly() { + return readOnly; + } + + /** + * Checks if this {@code FileSystem} was already closed, and throws an + * exception if it was. + * + * @throws IllegalStateException if this {@code FileSystem} was + * already closed + * @see #isClosed() + * @see #close() + */ + protected final void checkClosed() throws IllegalStateException { + if (isClosed()) { + throw new IllegalStateException("file system was already closed"); + } + } + + /** + * Checks if this {@code FileSystem} is read-only, and throws an + * exception if it is. + * + * @throws ReadOnlyException if this {@code FileSystem} is read-only + * @see #isReadOnly() + */ + protected final void checkReadOnly() throws ReadOnlyException { + if (isReadOnly()) { + throw new ReadOnlyException(); + } + } +} diff --git a/src/main/java/de/waldheinz/fs/AbstractFsObject.java b/src/main/java/de/waldheinz/fs/AbstractFsObject.java new file mode 100644 index 0000000..fea022a --- /dev/null +++ b/src/main/java/de/waldheinz/fs/AbstractFsObject.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs; + +/** + * A base class that helps to implement the {@code FsObject} interface. + * + * @author Ewout Prangsma <epr at jnode.org> + * @author Matthias Treydte <waldheinz at gmail.com> + * @since 0.6 + */ +public class AbstractFsObject implements FsObject { + + /** + * Holds the read-only state of this object. + */ + private final boolean readOnly; + + /** + * Remembers if this object still valid. + */ + private boolean valid; + + /** + * Creates a new instance of {@code AbstractFsObject} which will be valid + * and have the specified read-only state. + * + * @param readOnly if the new object will be read-only + */ + protected AbstractFsObject(boolean readOnly) { + this.valid = true; + this.readOnly = readOnly; + } + + /** + * {@inheritDoc} + * + * @return {@inheritDoc} + * @see #checkValid() + * @see #invalidate() + */ + @Override + public final boolean isValid() { + return this.valid; + } + + /** + * Marks this object as invalid. + * + * @see #isValid() + * @see #checkValid() + */ + protected final void invalidate() { + this.valid = false; + } + + /** + * Convience method to check if this object is still valid and throw an + * {@code IllegalStateException} if it is not. + * + * @throws IllegalStateException if this object was invalidated + * @since 0.6 + * @see #isValid() + * @see #invalidate() + */ + protected final void checkValid() throws IllegalStateException { + if (!isValid()) throw new IllegalStateException( + this + " is not valid"); + } + + /** + * Convience method to check if this object is writable. An object is + * writable if it is both, valid and not read-only. + * + * @throws IllegalStateException if this object was invalidated + * @throws ReadOnlyException if this object was created with the read-only + * flag set + * @since 0.6 + */ + protected final void checkWritable() + throws IllegalStateException, ReadOnlyException { + + checkValid(); + + if (isReadOnly()) { + throw new ReadOnlyException(); + } + } + + @Override + public final boolean isReadOnly() { + return this.readOnly; + } + +} diff --git a/src/main/java/de/waldheinz/fs/BlockDevice.java b/src/main/java/de/waldheinz/fs/BlockDevice.java new file mode 100644 index 0000000..88f05c9 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/BlockDevice.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * This is the abstraction used for a device that can hold a {@link FileSystem}. + * + * @author Ewout Prangsma <epr at jnode.org> + * @author Matthias Treydte <waldheinz at gmail.com> + */ +public interface BlockDevice { + + /** + * Gets the total length of this device in bytes. + * + * @return the total number of bytes on this device + * @throws IOException on error getting the size of this device + */ + public abstract long getSize() throws IOException; + + /** + * Read a block of data from this device. + * + * @param devOffset the byte offset where to read the data from + * @param dest the destination buffer where to store the data read + * @throws IOException on read error + */ + public abstract void read(long devOffset, ByteBuffer dest) + throws IOException; + + /** + * Writes a block of data to this device. + * + * @param devOffset the byte offset where to store the data + * @param src the source {@code ByteBuffer} to write to the device + * @throws ReadOnlyException if this {@code BlockDevice} is read-only + * @throws IOException on write error + * @throws IllegalArgumentException if the {@code devOffset} is negative + * or the write would go beyond the end of the device + * @see #isReadOnly() + */ + public abstract void write(long devOffset, ByteBuffer src) + throws ReadOnlyException, IOException, + IllegalArgumentException; + + /** + * Flushes data in caches to the actual storage. + * + * @throws IOException on write error + */ + public abstract void flush() throws IOException; + + /** + * Returns the size of a sector on this device. + * + * @return the sector size in bytes + * @throws IOException on error determining the sector size + */ + public int getSectorSize() throws IOException; + + /** + * Closes this {@code BlockDevice}. No methods of this device may be + * accesses after this method was called. + * + * @throws IOException on error closing this device + * @see #isClosed() + */ + public void close() throws IOException; + + /** + * Checks if this device was already closed. No methods may be called + * on a closed device (except this method). + * + * @return if this device is closed + */ + public boolean isClosed(); + + /** + * Checks if this {@code BlockDevice} is read-only. + * + * @return if this {@code BlockDevice} is read-only + */ + public boolean isReadOnly(); + +} diff --git a/src/main/java/de/waldheinz/fs/FileSystem.java b/src/main/java/de/waldheinz/fs/FileSystem.java new file mode 100644 index 0000000..f84baae --- /dev/null +++ b/src/main/java/de/waldheinz/fs/FileSystem.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs; + +import java.io.IOException; + +/** + * The interface common to all file system implementations. + * + * @author Ewout Prangsma <epr at jnode.org> + * @author Matthias Treydte <waldheinz at gmail.com> + */ +public interface FileSystem { + + /** + * Gets the root entry of this filesystem. This is usually a directory, but + * this is not required. + * + * @return the file system's root entry + * @throws IOException on read error + */ + public FsDirectory getRoot() throws IOException; + + /** + * Returns if this {@code FileSystem} is in read-only mode. + * + * @return if this {@code FileSystem} is read-only + */ + public boolean isReadOnly(); + + /** + * Close this file system. After a close, all invocations of methods of + * this file system or objects created by this file system will throw an + * {@link IllegalStateException}. + * + * @throws IOException on error closing the file system + */ + public void close() throws IOException; + + /** + * Returns {@code true} if this file system is closed. If the file system + * is closed, no more operations may be performed on it. + * + * @return if this file system is closed + */ + public boolean isClosed(); + + /** + * The total size of this file system. + * + * @return if -1 this feature is unsupported + * @throws IOException if an I/O error occurs + */ + public long getTotalSpace() throws IOException; + + /** + * The free space of this file system. + * + * @return if -1 this feature is unsupported + * @throws IOException if an I/O error occurs + */ + public long getFreeSpace() throws IOException; + + /** + * The usable space of this file system. + * + * @return if -1 this feature is unsupported + * @throws IOException if an I/O error occurs + */ + public long getUsableSpace() throws IOException; + + /** + * Flushes any modified file system structures to the underlying storage. + * + * @throws IOException + */ + public void flush() throws IOException; +} diff --git a/src/main/java/de/waldheinz/fs/FileSystemFactory.java b/src/main/java/de/waldheinz/fs/FileSystemFactory.java new file mode 100644 index 0000000..47cc438 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/FileSystemFactory.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs; + +import de.waldheinz.fs.fat.FatFileSystem; +import java.io.IOException; + +/** + * Factory for {@link FileSystem} instances. + * + * @author Matthias Treydte <waldheinz at gmail.com> + */ +public class FileSystemFactory { + + private FileSystemFactory() { } + + /** + * <p> + * Creates a new {@link FileSystem} for the specified {@code device}. When + * using this method, care must be taken that there is only one + * {@code FileSystems} accessing the specified {@link BlockDevice}. + * Otherwise severe file system corruption may occur. + * </p> + * + * @param device the device to create the file system for + * @param readOnly if the file system should be openend read-only + * @return a new {@code FileSystem} instance for the specified device + * @throws UnknownFileSystemException if the file system type could + * not be determined + * @throws IOException on read error + */ + public static FileSystem create(BlockDevice device, boolean readOnly) + throws UnknownFileSystemException, IOException { + + return FatFileSystem.read(device, readOnly); + } +} diff --git a/src/main/java/de/waldheinz/fs/FsDirectory.java b/src/main/java/de/waldheinz/fs/FsDirectory.java new file mode 100644 index 0000000..c3d9b94 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/FsDirectory.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs; + +import java.io.IOException; +import java.util.Iterator; + +/** + * Base class for all {@link FileSystem} directories. + * + * @author Ewout Prangsma < epr at jnode.org> + * @author Matthias Treydte + */ +public interface FsDirectory extends Iterable<FsDirectoryEntry>, FsObject { + + /** + * Gets an iterator to iterate over the entries of this directory. + * + * @return the directory iterator + */ + @Override + public Iterator<FsDirectoryEntry> iterator(); + + /** + * Gets the entry with the given name. + * + * @param name the name of the entry to get + * @return the entry, if it existed + * @throws IOException on error retrieving the entry + */ + public FsDirectoryEntry getEntry(String name) throws IOException; + + /** + * Add a new file with a given name to this directory. + * + * @param name the name of the file to add + * @return the entry pointing to the new file + * @throws IOException on error creating the file + */ + public FsDirectoryEntry addFile(String name) throws IOException; + + /** + * Add a new (sub-)directory with a given name to this directory. + * + * @param name the name of the sub-directory to add + * @return the entry pointing to the new directory + * @throws IOException on error creating the directory + */ + public FsDirectoryEntry addDirectory(String name) throws IOException; + + /** + * Remove the entry with the given name from this directory. + * + * @param name name of the entry to remove + * @throws IOException on error deleting the entry + */ + public void remove(String name) throws IOException; + + /** + * Save all dirty (unsaved) data to the device. + * + * @throws IOException on write error + */ + public void flush() throws IOException; + +} diff --git a/src/main/java/de/waldheinz/fs/FsDirectoryEntry.java b/src/main/java/de/waldheinz/fs/FsDirectoryEntry.java new file mode 100644 index 0000000..c071bfe --- /dev/null +++ b/src/main/java/de/waldheinz/fs/FsDirectoryEntry.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs; + +import java.io.IOException; +import java.util.Comparator; + +/** + * Represents one entry in a {@link FsDirectory}. + * + * @author Ewout Prangsma <epr at jnode.org> + * @author Matthias Treydte <waldheinz at gmail.com> + */ +public interface FsDirectoryEntry extends FsObject { + + /** + * Compares directory entries alphabetically, with all directories coming + * before all files. + */ + public final static Comparator<FsDirectoryEntry> DIRECTORY_ENTRY_COMPARATOR = + new Comparator<FsDirectoryEntry>() { + + @Override + public int compare(FsDirectoryEntry e1, FsDirectoryEntry e2) { + if (e2.isDirectory() == e1.isDirectory()) { + /* compare names */ + return e1.getName().compareTo(e2.getName()); + } else { + if (e2.isDirectory()) return 1; + else return -1; + } + } + }; + + /** + * Gets the name of this entry. + * + * @return this entrys name + */ + public String getName(); + + /** + * Gets the last modification time of this entry. + * + * @return the last modification time of the entry as milliseconds + * since 1970, or {@code 0} if this filesystem does not support + * getting the last modification time + * @throws IOException if an error occurs retrieving the time stamp + */ + public long getLastModified() throws IOException; + + /** + * Returns the time when this entry was created as ms since 1970. + * + * @return the creation time, or 0 if this feature is not supported + * @throws IOException on error retrieving the time stamp + */ + public long getCreated() throws IOException; + + /** + * Returns the time when this entry was last accessed as ms since 1970. + * + * @return the last access time, or 0 if this feature is not supported + * @throws IOException on error retrieving the last access time + */ + public long getLastAccessed() throws IOException; + + /** + * Is this entry refering to a file? + * + * @return if this entry refers to a file + */ + public boolean isFile(); + + /** + * Is this entry refering to a (sub-)directory? + * + * @return if this entry refers to a directory + */ + public boolean isDirectory(); + + /** + * Sets the name of this entry. + * + * @param newName the new name of this entry + * @throws IOException on error setting the new name + */ + public void setName(String newName) throws IOException; + + /** + * Sets the last modification time of this entry. + * + * @param lastModified the new last modification time of this entry + * @throws IOException on write error + */ + public void setLastModified(long lastModified) throws IOException; + + /** + * Gets the file this entry refers to. This method can only be called if + * {@code isFile} returns {@code true}. + * + * @return the file described by this entry + * @throws IOException on error accessing the file + * @throws UnsupportedOperationException if this entry is a directory + */ + public FsFile getFile() + throws IOException, UnsupportedOperationException; + + /** + * Gets the directory this entry refers to. This method can only be called + * if <code>isDirectory</code> returns true. + * + * @return The directory described by this entry + * @throws IOException on read error + * @throws UnsupportedOperationException if this entry is a file + */ + public FsDirectory getDirectory() + throws IOException, UnsupportedOperationException; + + /** + * Indicate if the entry has been modified in memory (ie need to be saved) + * + * @return true if the entry needs to be saved + */ + public boolean isDirty(); +} diff --git a/src/main/java/de/waldheinz/fs/FsFile.java b/src/main/java/de/waldheinz/fs/FsFile.java new file mode 100644 index 0000000..dfaa998 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/FsFile.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A FsFile is a representation of a single block of bytes on a filesystem. It + * is comparable to an inode in Unix. + * + * An FsFile does not have any knowledge of who is using this file. It is also + * possible that the system uses a single FsFile instance to create two + * inputstream's for two different principals. + * + * @author Ewout Prangsma <epr at jnode.org> + */ +public interface FsFile extends FsObject { + + /** + * Gets the length (in bytes) of this file. + * + * @return the file size + */ + public long getLength(); + + /** + * Sets the length of this file. + * + * @param length the new length of this file + * @throws IOException on error updating the file size + */ + public void setLength(long length) throws IOException; + + /** + * Reads from this file into the specified {@code ByteBuffer}. The + * first byte read will be put into the buffer at it's + * {@link ByteBuffer#position() position}, and the number of bytes read + * will equal the buffer's {@link ByteBuffer#remaining() remaining} bytes. + * + * @param offset the offset into the file where to start reading + * @param dest the destination buffer where to put the bytes that were read + * @throws IOException on read error + */ + public void read(long offset, ByteBuffer dest) throws IOException; + + /** + * Writes to this file taking the data to write from the specified + * {@code ByteBuffer}. This method will read the buffer's + * {@link ByteBuffer#remaining() remaining} bytes starting at it's + * {@link ByteBuffer#position() position}. + * + * @param offset the offset into the file where the first byte will be + * written + * @param src the source buffer to read the data from + * @throws ReadOnlyException if the file is read-only + * @throws IOException on write error + */ + public void write(long offset, ByteBuffer src) + throws ReadOnlyException, IOException; + + /** + * Flush any possibly cached data to the disk. + * + * @throws IOException on error flushing + */ + public void flush() throws IOException; +} diff --git a/src/main/java/de/waldheinz/fs/FsObject.java b/src/main/java/de/waldheinz/fs/FsObject.java new file mode 100644 index 0000000..72d7d50 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/FsObject.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs; + +/** + * This interface is the base interface for objects that are part of a + * {@link FileSystem}. + * + * @author Ewout Prangsma <epr at jnode.org> + * @author Matthias Treydte <waldheinz at gmail.com> + */ +public interface FsObject { + + /** + * Checks if this {@code FsObject} is still valid. + * + * An object is not valid anymore if it has been removed from the + * filesystem. All invocations on methods (except this method and the + * methods inherited from {@link java.lang.Object}) of + * invalid objects must throw an {@link IllegalStateException}. + * + * @return if this {@code FsObject} is still valid + */ + public boolean isValid(); + + /** + * Checks if this {@code FsObject} is read-only. Any attempt to modify a + * read-only {@code FsObject} must result in a {@link ReadOnlyException} + * being thrown, and the modification must not be performed. + * + * @return if this {@code FsObject} is read-only + * @since 0.6 + */ + public boolean isReadOnly(); + +} diff --git a/src/main/java/de/waldheinz/fs/ReadOnlyException.java b/src/main/java/de/waldheinz/fs/ReadOnlyException.java new file mode 100644 index 0000000..f786d64 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/ReadOnlyException.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs; + +/** + * This exception is thrown when an attempt is made to write to a read-only + * {@link BlockDevice}, {@link FileSystem} or other file system object. This is + * an unchecked exception, as it should always be possible to query the object + * about it's read-only state using it's {@code isReadOnly()} method. + * + * @author Matthias Treydte <waldheinz at gmail.com> + * @see FileSystem#isReadOnly() + * @see BlockDevice#isReadOnly() + */ +public final class ReadOnlyException extends RuntimeException { + + private final static long serialVersionUID = 1; + + /** + * Creates a new instance of {@code ReadOnlyException}. + * + */ + public ReadOnlyException() { + super("read-only"); + } +} diff --git a/src/main/java/de/waldheinz/fs/UnknownFileSystemException.java b/src/main/java/de/waldheinz/fs/UnknownFileSystemException.java new file mode 100644 index 0000000..0e27baf --- /dev/null +++ b/src/main/java/de/waldheinz/fs/UnknownFileSystemException.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs; + +import java.io.IOException; + +/** + * Indicates that it was not possible to determine the type of the file + * system being used on a block device. + * + * @author Matthias Treydte <waldheinz at gmail.com> + */ +public final class UnknownFileSystemException extends IOException { + private final static long serialVersionUID = 1; + + private final BlockDevice device; + + /** + * Creates a new instance of {@code UnknownFileSystemException}. + * + * @param device the {@code BlockDevice} whose file system could not + * be determined + */ + public UnknownFileSystemException(BlockDevice device) { + super("can not determin file system type"); //NOI18N + this.device = device; + } + + /** + * Returns the {@code BlockDevice} whose file system could not be + * determined. + * + * @return the {@code BlockDevice} with an unknown file system + */ + public BlockDevice getDevice() { + return this.device; + } +} diff --git a/src/main/java/de/waldheinz/fs/fat/AbstractDirectory.java b/src/main/java/de/waldheinz/fs/fat/AbstractDirectory.java new file mode 100644 index 0000000..d3445b7 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/AbstractDirectory.java @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * This is the abstract base class for all directory implementations. + * + * @author Ewout Prangsma <epr at jnode.org> + * @author Matthias Treydte <waldheinz at gmail.com> + */ +abstract class AbstractDirectory { + + /** + * The maximum length of the volume label. + * + * @see #setLabel(java.lang.String) + */ + public static final int MAX_LABEL_LENGTH = 11; + + private final List<FatDirectoryEntry> entries; + private final boolean readOnly; + private final boolean isRoot; + + private boolean dirty; + private int capacity; + private String volumeLabel; + + /** + * Creates a new instance of {@code AbstractDirectory}. + * + * @param capacity the initial capacity of the new instance + * @param readOnly if the instance should be read-only + * @param isRoot if the new {@code AbstractDirectory} represents a root + * directory + */ + protected AbstractDirectory( + int capacity, boolean readOnly, boolean isRoot) { + + this.entries = new ArrayList<FatDirectoryEntry>(); + this.capacity = capacity; + this.readOnly = readOnly; + this.isRoot = isRoot; + } + + /** + * Gets called when the {@code AbstractDirectory} must read it's content + * off the backing storage. This method must always fill the buffer's + * remaining space with the bytes making up this directory, beginning with + * the first byte. + * + * @param data the {@code ByteBuffer} to fill + * @throws IOException on read error + */ + protected abstract void read(ByteBuffer data) throws IOException; + + /** + * Gets called when the {@code AbstractDirectory} wants to write it's + * contents to the backing storage. This method is expected to write the + * buffer's remaining data to the storage, beginning with the first byte. + * + * @param data the {@code ByteBuffer} to write + * @throws IOException on write error + */ + protected abstract void write(ByteBuffer data) throws IOException; + + /** + * Returns the number of the cluster where this directory is stored. This + * is important when creating the ".." entry in a sub-directory, as this + * entry must poing to the storage cluster of it's parent. + * + * @return this directory's storage cluster + */ + protected abstract long getStorageCluster(); + + /** + * Gets called by the {@code AbstractDirectory} when it has determined that + * it should resize because the number of entries has changed. + * + * @param entryCount the new number of entries this directory needs to store + * @throws IOException on write error + * @throws DirectoryFullException if the FAT12/16 root directory is full + * @see #sizeChanged(long) + * @see #checkEntryCount(int) + */ + protected abstract void changeSize(int entryCount) + throws DirectoryFullException, IOException; + + /** + * Replaces all entries in this directory. + * + * @param newEntries the new directory entries + */ + public void setEntries(List<FatDirectoryEntry> newEntries) { + if (newEntries.size() > capacity) + throw new IllegalArgumentException("too many entries"); + + this.entries.clear(); + this.entries.addAll(newEntries); + } + + /** + * + * + * @param newSize the new storage space for the directory in bytes + * @see #changeSize(int) + */ + protected final void sizeChanged(long newSize) throws IOException { + final long newCount = newSize / FatDirectoryEntry.SIZE; + if (newCount > Integer.MAX_VALUE) + throw new IOException("directory too large"); + + this.capacity = (int) newCount; + } + + public final FatDirectoryEntry getEntry(int idx) { + return this.entries.get(idx); + } + + /** + * Returns the current capacity of this {@code AbstractDirectory}. + * + * @return the number of entries this directory can hold in its current + * storage space + * @see #changeSize(int) + */ + public final int getCapacity() { + return this.capacity; + } + + /** + * The number of entries that are currently stored in this + * {@code AbstractDirectory}. + * + * @return the current number of directory entries + */ + public final int getEntryCount() { + return this.entries.size(); + } + + public boolean isReadOnly() { + return readOnly; + } + + public final boolean isRoot() { + return this.isRoot; + } + + /** + * Gets the number of directory entries in this directory. This is the + * number of "real" entries in this directory, possibly plus one if a + * volume label is set. + * + * @return the number of entries in this directory + */ + public int getSize() { + return entries.size() + ((this.volumeLabel != null) ? 1 : 0); + } + + /** + * Mark this directory as dirty. + */ + protected final void setDirty() { + this.dirty = true; + } + + /** + * Checks if this {@code AbstractDirectory} is a root directory. + * + * @throws UnsupportedOperationException if this is not a root directory + * @see #isRoot() + */ + private void checkRoot() throws UnsupportedOperationException { + if (!isRoot()) { + throw new UnsupportedOperationException( + "only supported on root directories"); + } + } + + /** + * Mark this directory as not dirty. + */ + private void resetDirty() { + this.dirty = false; + } + + /** + * Flush the contents of this directory to the persistent storage + */ + public void flush() throws IOException { + + final ByteBuffer data = ByteBuffer.allocate( + getCapacity() * FatDirectoryEntry.SIZE); + + for (int i=0; i < entries.size(); i++) { + final FatDirectoryEntry entry = entries.get(i); + + if (entry != null) { + entry.write(data); + } + } + + /* TODO: the label could be placed directly the dot entries */ + + if (this.volumeLabel != null) { + final FatDirectoryEntry labelEntry = + FatDirectoryEntry.createVolumeLabel(volumeLabel); + + labelEntry.write(data); + } + + if (data.hasRemaining()) { + FatDirectoryEntry.writeNullEntry(data); + } + + data.flip(); + + write(data); + resetDirty(); + } + + protected final void read() throws IOException { + final ByteBuffer data = ByteBuffer.allocate( + getCapacity() * FatDirectoryEntry.SIZE); + + read(data); + data.flip(); + + for (int i=0; i < getCapacity(); i++) { + final FatDirectoryEntry e = + FatDirectoryEntry.read(data, isReadOnly()); + + if (e == null) break; + + if (e.isVolumeLabel()) { + if (!this.isRoot) throw new IOException( + "volume label in non-root directory"); + + this.volumeLabel = e.getVolumeLabel(); + } else { + entries.add(e); + } + } + } + + public void addEntry(FatDirectoryEntry e) throws IOException { + assert (e != null); + + if (getSize() == getCapacity()) { + changeSize(getCapacity() + 1); + } + + entries.add(e); + } + + public void addEntries(FatDirectoryEntry[] entries) + throws IOException { + + if (getSize() + entries.length > getCapacity()) { + changeSize(getSize() + entries.length); + } + + this.entries.addAll(Arrays.asList(entries)); + } + + public void removeEntry(FatDirectoryEntry entry) throws IOException { + assert (entry != null); + + this.entries.remove(entry); + changeSize(getSize()); + } + + /** + * Returns the volume label that is stored in this directory. Reading the + * volume label is only supported for the root directory. + * + * @return the volume label stored in this directory, or {@code null} + * @throws UnsupportedOperationException if this is not a root directory + * @see #isRoot() + */ + public String getLabel() throws UnsupportedOperationException { + checkRoot(); + + return volumeLabel; + } + + public FatDirectoryEntry createSub(Fat fat) throws IOException { + final ClusterChain chain = new ClusterChain(fat, false); + chain.setChainLength(1); + + final FatDirectoryEntry entry = FatDirectoryEntry.create(true); + entry.setStartCluster(chain.getStartCluster()); + + final ClusterChainDirectory dir = + new ClusterChainDirectory(chain, false); + + /* add "." entry */ + + final FatDirectoryEntry dot = FatDirectoryEntry.create(true); + dot.setShortName(ShortName.DOT); + dot.setStartCluster(dir.getStorageCluster()); + copyDateTimeFields(entry, dot); + dir.addEntry(dot); + + /* add ".." entry */ + + final FatDirectoryEntry dotDot = FatDirectoryEntry.create(true); + dotDot.setShortName(ShortName.DOT_DOT); + dotDot.setStartCluster(getStorageCluster()); + copyDateTimeFields(entry, dotDot); + dir.addEntry(dotDot); + + dir.flush(); + + return entry; + } + + private static void copyDateTimeFields( + FatDirectoryEntry src, FatDirectoryEntry dst) { + + dst.setCreated(src.getCreated()); + dst.setLastAccessed(src.getLastAccessed()); + dst.setLastModified(src.getLastModified()); + } + + /** + * Sets the volume label that is stored in this directory. Setting the + * volume label is supported on the root directory only. + * + * @param label the new volume label + * @throws IllegalArgumentException if the label is too long + * @throws UnsupportedOperationException if this is not a root directory + * @see #isRoot() + */ + public void setLabel(String label) throws IllegalArgumentException, + UnsupportedOperationException, IOException { + + checkRoot(); + + if (label.length() > MAX_LABEL_LENGTH) throw new + IllegalArgumentException("label too long"); + + if (this.volumeLabel != null) { + if (label == null) { + changeSize(getSize() - 1); + this.volumeLabel = null; + } else { + ShortName.checkValidChars(label.toCharArray()); + this.volumeLabel = label; + } + } else { + if (label != null) { + changeSize(getSize() + 1); + ShortName.checkValidChars(label.toCharArray()); + this.volumeLabel = label; + } + } + + this.dirty = true; + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/BootSector.java b/src/main/java/de/waldheinz/fs/fat/BootSector.java new file mode 100644 index 0000000..d1aa398 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/BootSector.java @@ -0,0 +1,517 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import de.waldheinz.fs.BlockDevice; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * The boot sector. + * + * @author Ewout Prangsma <epr at jnode.org> + * @author Matthias Treydte <waldheinz at gmail.com> + */ +public abstract class BootSector extends Sector { + + /** + * Offset to the byte specifying the number of FATs. + * + * @see #getNrFats() + * @see #setNrFats(int) + */ + public static final int FAT_COUNT_OFFSET = 16; + public static final int RESERVED_SECTORS_OFFSET = 14; + + public static final int TOTAL_SECTORS_16_OFFSET = 19; + public static final int TOTAL_SECTORS_32_OFFSET = 32; + + /** + * The length of the file system type string. + * + * @see #getFileSystemType() + */ + public static final int FILE_SYSTEM_TYPE_LENGTH = 8; + + /** + * The offset to the sectors per cluster value stored in a boot sector. + * + * @see #getSectorsPerCluster() + * @see #setSectorsPerCluster(int) + */ + public static final int SECTORS_PER_CLUSTER_OFFSET = 0x0d; + + public static final int EXTENDED_BOOT_SIGNATURE = 0x29; + + /** + * The size of a boot sector in bytes. + */ + public final static int SIZE = 512; + + protected BootSector(BlockDevice device) { + super(device, 0, SIZE); + markDirty(); + } + + public static BootSector read(BlockDevice device) throws IOException { + final ByteBuffer bb = ByteBuffer.allocate(512); + bb.order(ByteOrder.LITTLE_ENDIAN); + device.read(0, bb); + + if ((bb.get(510) & 0xff) != 0x55 || + (bb.get(511) & 0xff) != 0xaa) throw new IOException( + "missing boot sector signature"); + + final byte sectorsPerCluster = bb.get(SECTORS_PER_CLUSTER_OFFSET); + + if (sectorsPerCluster <= 0) throw new IOException( + "suspicious sectors per cluster count " + sectorsPerCluster); + + final int rootDirEntries = bb.getShort( + Fat16BootSector.ROOT_DIR_ENTRIES_OFFSET); + final int rootDirSectors = ((rootDirEntries * 32) + + (device.getSectorSize() - 1)) / device.getSectorSize(); + + final int total16 = + bb.getShort(TOTAL_SECTORS_16_OFFSET) & 0xffff; + final long total32 = + bb.getInt(TOTAL_SECTORS_32_OFFSET) & 0xffffffffl; + + final long totalSectors = total16 == 0 ? total32 : total16; + + final int fatSz16 = + bb.getShort(Fat16BootSector.SECTORS_PER_FAT_OFFSET) & 0xffff; + final long fatSz32 = + bb.getInt(Fat32BootSector.SECTORS_PER_FAT_OFFSET) & 0xffffffffl; + + final long fatSz = fatSz16 == 0 ? fatSz32 : fatSz16; + final int reservedSectors = bb.getShort(RESERVED_SECTORS_OFFSET); + final int fatCount = bb.get(FAT_COUNT_OFFSET); + final long dataSectors = totalSectors - (reservedSectors + + (fatCount * fatSz) + rootDirSectors); + + final long clusterCount = dataSectors / sectorsPerCluster; + + final BootSector result = + (clusterCount > Fat16BootSector.MAX_FAT16_CLUSTERS) ? + new Fat32BootSector(device) : new Fat16BootSector(device); + + result.read(); + return result; + } + + public abstract FatType getFatType(); + + /** + * Gets the number of sectors per FAT. + * + * @return the sectors per FAT + */ + public abstract long getSectorsPerFat(); + + /** + * Sets the number of sectors/fat + * + * @param v the new number of sectors per fat + */ + public abstract void setSectorsPerFat(long v); + + public abstract void setSectorCount(long count); + + public abstract int getRootDirEntryCount(); + + public abstract long getSectorCount(); + + /** + * Returns the offset to the file system type label, as this differs + * between FAT12/16 and FAT32. + * + * @return the offset to the file system type label + */ + public abstract int getFileSystemTypeLabelOffset(); + + public abstract int getExtendedBootSignatureOffset(); + + public void init() throws IOException { + setBytesPerSector(getDevice().getSectorSize()); + setSectorCount(getDevice().getSize() / getDevice().getSectorSize()); + set8(getExtendedBootSignatureOffset(), EXTENDED_BOOT_SIGNATURE); + + /* magic bytes needed by some windows versions to recognize a boot + * sector. these are x86 jump instructions which lead into + * nirvana when executed, but we're currently unable to produce really + * bootable images anyway. So... */ + set8(0x00, 0xeb); + set8(0x01, 0x3c); + set8(0x02, 0x90); + + /* the boot sector signature */ + set8(0x1fe, 0x55); + set8(0x1ff, 0xaa); + } + + /** + * Returns the file system type label string. + * + * @return the file system type string + * @see #setFileSystemTypeLabel(java.lang.String) + * @see #getFileSystemTypeLabelOffset() + * @see #FILE_SYSTEM_TYPE_LENGTH + */ + public String getFileSystemTypeLabel() { + final StringBuilder sb = new StringBuilder(FILE_SYSTEM_TYPE_LENGTH); + + for (int i=0; i < FILE_SYSTEM_TYPE_LENGTH; i++) { + sb.append ((char) get8(getFileSystemTypeLabelOffset() + i)); + } + + return sb.toString(); + } + + /** + * + * + * @param fsType the + * @throws IllegalArgumentException if the length of the specified string + * does not equal {@link #FILE_SYSTEM_TYPE_LENGTH} + */ + public void setFileSystemTypeLabel(String fsType) + throws IllegalArgumentException { + + if (fsType.length() != FILE_SYSTEM_TYPE_LENGTH) { + throw new IllegalArgumentException(); + } + + for (int i=0; i < FILE_SYSTEM_TYPE_LENGTH; i++) { + set8(getFileSystemTypeLabelOffset() + i, fsType.charAt(i)); + } + } + + /** + * Returns the number of clusters that are really needed to cover the + * data-caontaining portion of the file system. + * + * @return the number of clusters usable for user data + * @see #getDataSize() + */ + public final long getDataClusterCount() { + return getDataSize() / getBytesPerCluster(); + } + + /** + * Returns the size of the data-containing portion of the file system. + * + * @return the number of bytes usable for storing user data + */ + private long getDataSize() { + return (getSectorCount() * getBytesPerSector()) - + FatUtils.getFilesOffset(this); + } + + /** + * Gets the OEM name + * + * @return String + */ + public String getOemName() { + StringBuilder b = new StringBuilder(8); + + for (int i = 0; i < 8; i++) { + int v = get8(0x3 + i); + if (v == 0) break; + b.append((char) v); + } + + return b.toString(); + } + + + /** + * Sets the OEM name, must be at most 8 characters long. + * + * @param name the new OEM name + */ + public void setOemName(String name) { + if (name.length() > 8) throw new IllegalArgumentException( + "only 8 characters are allowed"); + + for (int i = 0; i < 8; i++) { + char ch; + if (i < name.length()) { + ch = name.charAt(i); + } else { + ch = (char) 0; + } + + set8(0x3 + i, ch); + } + } + + /** + * Gets the number of bytes/sector + * + * @return int + */ + public int getBytesPerSector() { + return get16(0x0b); + } + + /** + * Sets the number of bytes/sector + * + * @param v the new value for bytes per sector + */ + public void setBytesPerSector(int v) { + if (v == getBytesPerSector()) return; + + switch (v) { + case 512: case 1024: case 2048: case 4096: + set16(0x0b, v); + break; + + default: + throw new IllegalArgumentException(); + } + } + + private static boolean isPowerOfTwo(int n) { + return ((n!=0) && (n&(n-1))==0); + } + + /** + * Returns the number of bytes per cluster, which is calculated from the + * {@link #getSectorsPerCluster() sectors per cluster} and the + * {@link #getBytesPerSector() bytes per sector}. + * + * @return the number of bytes per cluster + */ + public int getBytesPerCluster() { + return this.getSectorsPerCluster() * this.getBytesPerSector(); + } + + /** + * Gets the number of sectors/cluster + * + * @return int + */ + public int getSectorsPerCluster() { + return get8(SECTORS_PER_CLUSTER_OFFSET); + } + + /** + * Sets the number of sectors/cluster + * + * @param v the new number of sectors per cluster + */ + public void setSectorsPerCluster(int v) { + if (v == getSectorsPerCluster()) return; + if (!isPowerOfTwo(v)) throw new IllegalArgumentException( + "value must be a power of two"); + + set8(SECTORS_PER_CLUSTER_OFFSET, v); + } + + /** + * Gets the number of reserved (for bootrecord) sectors + * + * @return int + */ + public int getNrReservedSectors() { + return get16(RESERVED_SECTORS_OFFSET); + } + + /** + * Sets the number of reserved (for bootrecord) sectors + * + * @param v the new number of reserved sectors + */ + public void setNrReservedSectors(int v) { + if (v == getNrReservedSectors()) return; + if (v < 1) throw new IllegalArgumentException( + "there must be >= 1 reserved sectors"); + set16(RESERVED_SECTORS_OFFSET, v); + } + + /** + * Gets the number of fats + * + * @return int + */ + public final int getNrFats() { + return get8(FAT_COUNT_OFFSET); + } + + /** + * Sets the number of fats + * + * @param v the new number of fats + */ + public final void setNrFats(int v) { + if (v == getNrFats()) return; + + set8(FAT_COUNT_OFFSET, v); + } + + /** + * Gets the number of logical sectors + * + * @return int + */ + protected int getNrLogicalSectors() { + return get16(TOTAL_SECTORS_16_OFFSET); + } + + /** + * Sets the number of logical sectors + * + * @param v the new number of logical sectors + */ + protected void setNrLogicalSectors(int v) { + if (v == getNrLogicalSectors()) return; + + set16(TOTAL_SECTORS_16_OFFSET, v); + } + + protected void setNrTotalSectors(long v) { + set32(TOTAL_SECTORS_32_OFFSET, v); + } + + protected long getNrTotalSectors() { + return get32(TOTAL_SECTORS_32_OFFSET); + } + + /** + * Gets the medium descriptor byte + * + * @return int + */ + public int getMediumDescriptor() { + return get8(0x15); + } + + /** + * Sets the medium descriptor byte + * + * @param v the new medium descriptor + */ + public void setMediumDescriptor(int v) { + set8(0x15, v); + } + + /** + * Gets the number of sectors/track + * + * @return int + */ + public int getSectorsPerTrack() { + return get16(0x18); + } + + /** + * Sets the number of sectors/track + * + * @param v the new number of sectors per track + */ + public void setSectorsPerTrack(int v) { + if (v == getSectorsPerTrack()) return; + + set16(0x18, v); + } + + /** + * Gets the number of heads + * + * @return int + */ + public int getNrHeads() { + return get16(0x1a); + } + + /** + * Sets the number of heads + * + * @param v the new number of heads + */ + public void setNrHeads(int v) { + if (v == getNrHeads()) return; + + set16(0x1a, v); + } + + /** + * Gets the number of hidden sectors + * + * @return int + */ + public long getNrHiddenSectors() { + return get32(0x1c); + } + + /** + * Sets the number of hidden sectors + * + * @param v the new number of hidden sectors + */ + public void setNrHiddenSectors(long v) { + if (v == getNrHiddenSectors()) return; + + set32(0x1c, v); + } + + @Override + public String toString() { + StringBuilder res = new StringBuilder(1024); + res.append("Bootsector :\n"); + res.append("oemName="); + res.append(getOemName()); + res.append('\n'); + res.append("medium descriptor = "); + res.append(getMediumDescriptor()); + res.append('\n'); + res.append("Nr heads = "); + res.append(getNrHeads()); + res.append('\n'); + res.append("Sectors per track = "); + res.append(getSectorsPerTrack()); + res.append('\n'); + res.append("Sector per cluster = "); + res.append(getSectorsPerCluster()); + res.append('\n'); + res.append("byte per sector = "); + res.append(getBytesPerSector()); + res.append('\n'); + res.append("Nr fats = "); + res.append(getNrFats()); + res.append('\n'); + res.append("Nr hidden sectors = "); + res.append(getNrHiddenSectors()); + res.append('\n'); + res.append("Nr logical sectors = "); + res.append(getNrLogicalSectors()); + res.append('\n'); + res.append("Nr reserved sector = "); + res.append(getNrReservedSectors()); + res.append('\n'); + + return res.toString(); + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/ClusterChain.java b/src/main/java/de/waldheinz/fs/fat/ClusterChain.java new file mode 100644 index 0000000..e296092 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/ClusterChain.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import de.waldheinz.fs.AbstractFsObject; +import de.waldheinz.fs.BlockDevice; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A chain of clusters as stored in a {@link Fat}. + * + * @author Matthias Treydte <waldheinz at gmail.com> + */ +final class ClusterChain extends AbstractFsObject { + protected final Fat fat; + private final BlockDevice device; + private final int clusterSize; + protected final long dataOffset; + + private long startCluster; + + /** + * Creates a new {@code ClusterChain} that contains no clusters. + * + * @param fat the {@code Fat} that holds the new chain + * @param readOnly if the chain should be created read-only + */ + public ClusterChain(Fat fat, boolean readOnly) { + this(fat, 0, readOnly); + } + + public ClusterChain(Fat fat, long startCluster, boolean readOnly) { + super(readOnly); + + this.fat = fat; + + if (startCluster != 0) { + this.fat.testCluster(startCluster); + + if (this.fat.isFreeCluster(startCluster)) + throw new IllegalArgumentException( + "cluster " + startCluster + " is free"); + } + + this.device = fat.getDevice(); + this.dataOffset = FatUtils.getFilesOffset(fat.getBootSector()); + this.startCluster = startCluster; + this.clusterSize = fat.getBootSector().getBytesPerCluster(); + } + + public int getClusterSize() { + return clusterSize; + } + + public Fat getFat() { + return fat; + } + + public BlockDevice getDevice() { + return device; + } + + /** + * Returns the first cluster of this chain. + * + * @return the chain's first cluster, which may be 0 if this chain does + * not contain any clusters + */ + public long getStartCluster() { + return startCluster; + } + + /** + * Calculates the device offset (0-based) for the given cluster and offset + * within the cluster. + * + * @param cluster + * @param clusterOffset + * @return long + * @throws FileSystemException + */ + private long getDevOffset(long cluster, int clusterOffset) { + return dataOffset + clusterOffset + + ((cluster - Fat.FIRST_CLUSTER) * clusterSize); + } + + /** + * Returns the size this {@code ClusterChain} occupies on the device. + * + * @return the size this chain occupies on the device in bytes + */ + public long getLengthOnDisk() { + if (getStartCluster() == 0) return 0; + + return getChainLength() * clusterSize; + } + + /** + * Sets the length of this {@code ClusterChain} in bytes. Because a + * {@code ClusterChain} can only contain full clusters, the new size + * will always be a multiple of the cluster size. + * + * @param size the desired number of bytes the can be stored in + * this {@code ClusterChain} + * @return the true number of bytes this {@code ClusterChain} can contain + * @throws IOException on error setting the new size + * @see #setChainLength(int) + */ + public long setSize(long size) throws IOException { + final long nrClusters = ((size + clusterSize - 1) / clusterSize); + if (nrClusters > Integer.MAX_VALUE) + throw new IOException("too many clusters"); + + setChainLength((int) nrClusters); + + return clusterSize * nrClusters; + } + + /** + * Determines the length of this {@code ClusterChain} in clusters. + * + * @return the length of this chain + */ + public int getChainLength() { + if (getStartCluster() == 0) return 0; + + final long[] chain = getFat().getChain(getStartCluster()); + return chain.length; + } + + /** + * Sets the length of this cluster chain in clusters. + * + * @param nrClusters the new number of clusters this chain should contain, + * must be {@code >= 0} + * @throws IOException on error updating the chain length + * @see #setSize(long) + */ + public void setChainLength(int nrClusters) throws IOException { + if (nrClusters < 0) throw new IllegalArgumentException( + "negative cluster count"); //NOI18N + + if ((this.startCluster == 0) && (nrClusters == 0)) { + /* nothing to do */ + } else if ((this.startCluster == 0) && (nrClusters > 0)) { + final long[] chain = fat.allocNew(nrClusters); + this.startCluster = chain[0]; + } else { + final long[] chain = fat.getChain(startCluster); + + if (nrClusters != chain.length) { + if (nrClusters > chain.length) { + /* grow the chain */ + int count = nrClusters - chain.length; + + while (count > 0) { + fat.allocAppend(getStartCluster()); + count--; + } + } else { + /* shrink the chain */ + if (nrClusters > 0) { + fat.setEof(chain[nrClusters - 1]); + for (int i = nrClusters; i < chain.length; i++) { + fat.setFree(chain[i]); + } + } else { + for (int i=0; i < chain.length; i++) { + fat.setFree(chain[i]); + } + + this.startCluster = 0; + } + } + } + } + } + + public void readData(long offset, ByteBuffer dest) + throws IOException { + + int len = dest.remaining(); + + if ((startCluster == 0 && len > 0)) throw new EOFException(); + + final long[] chain = getFat().getChain(startCluster); + final BlockDevice dev = getDevice(); + + int chainIdx = (int) (offset / clusterSize); + if (offset % clusterSize != 0) { + int clusOfs = (int) (offset % clusterSize); + int size = Math.min(len, + (int) (clusterSize - (offset % clusterSize) - 1)); + dest.limit(dest.position() + size); + + dev.read(getDevOffset(chain[chainIdx], clusOfs), dest); + + offset += size; + len -= size; + chainIdx++; + } + + while (len > 0) { + int size = Math.min(clusterSize, len); + dest.limit(dest.position() + size); + + dev.read(getDevOffset(chain[chainIdx], 0), dest); + + len -= size; + chainIdx++; + } + } + + /** + * Writes data to this cluster chain, possibly growing the chain so it + * can store the additional data. When this method returns without throwing + * an exception, the buffer's {@link ByteBuffer#position() position} will + * equal it's {@link ByteBuffer#limit() limit}, and the limit will not + * have changed. This is not guaranteed if writing fails. + * + * @param offset the offset where to write the first byte from the buffer + * @param srcBuf the buffer to write to this {@code ClusterChain} + * @throws IOException on write error + */ + public void writeData(long offset, ByteBuffer srcBuf) throws IOException { + + int len = srcBuf.remaining(); + + if (len == 0) return; + + final long minSize = offset + len; + if (getLengthOnDisk() < minSize) { + setSize(minSize); + } + + final long[] chain = fat.getChain(getStartCluster()); + + int chainIdx = (int) (offset / clusterSize); + if (offset % clusterSize != 0) { + int clusOfs = (int) (offset % clusterSize); + int size = Math.min(len, + (int) (clusterSize - (offset % clusterSize))); + srcBuf.limit(srcBuf.position() + size); + + device.write(getDevOffset(chain[chainIdx], clusOfs), srcBuf); + + offset += size; + len -= size; + chainIdx++; + } + + while (len > 0) { + int size = Math.min(clusterSize, len); + srcBuf.limit(srcBuf.position() + size); + + device.write(getDevOffset(chain[chainIdx], 0), srcBuf); + + len -= size; + chainIdx++; + } + + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (!(obj instanceof ClusterChain)) return false; + + final ClusterChain other = (ClusterChain) obj; + + if (this.fat != other.fat && + (this.fat == null || !this.fat.equals(other.fat))) { + + return false; + } + + if (this.startCluster != other.startCluster) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 79 * hash + + (this.fat != null ? this.fat.hashCode() : 0); + hash = 79 * hash + + (int) (this.startCluster ^ (this.startCluster >>> 32)); + return hash; + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/ClusterChainDirectory.java b/src/main/java/de/waldheinz/fs/fat/ClusterChainDirectory.java new file mode 100644 index 0000000..fa3e9df --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/ClusterChainDirectory.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A directory that is stored in a cluster chain. + * + * @author Ewout Prangsma <epr at jnode.org> + * @author Matthias Treydte <waldheinz at gmail.com> + */ +class ClusterChainDirectory extends AbstractDirectory { + + /** + * According to the FAT specification, this is the maximum size a FAT + * directory may occupy on disk. The {@code ClusterChainDirectory} takes + * care not to grow beyond this limit. + * + * @see #changeSize(int) + */ + public final static int MAX_SIZE = 65536 * 32; + + /** + * The {@code ClusterChain} that stores this directory. Package-visible + * for testing. + */ + final ClusterChain chain; + + protected ClusterChainDirectory(ClusterChain chain, boolean isRoot) { + + super((int)(chain.getLengthOnDisk() / FatDirectoryEntry.SIZE), + chain.isReadOnly(), isRoot); + + this.chain = chain; + } + + public static ClusterChainDirectory readRoot( + ClusterChain chain) throws IOException { + + final ClusterChainDirectory result = + new ClusterChainDirectory(chain, true); + + result.read(); + return result; + } + + public static ClusterChainDirectory createRoot(Fat fat) throws IOException { + + if (fat.getFatType() != FatType.FAT32) { + throw new IllegalArgumentException( + "only FAT32 stores root directory in a cluster chain"); + } + + final Fat32BootSector bs = (Fat32BootSector) fat.getBootSector(); + final ClusterChain cc = new ClusterChain(fat, false); + cc.setChainLength(1); + + bs.setRootDirFirstCluster(cc.getStartCluster()); + + final ClusterChainDirectory result = + new ClusterChainDirectory(cc, true); + + result.flush(); + return result; + } + + @Override + protected final void read(ByteBuffer data) throws IOException { + this.chain.readData(0, data); + } + + @Override + protected final void write(ByteBuffer data) throws IOException { + final int toWrite = data.remaining(); + chain.writeData(0, data); + final long trueSize = chain.getLengthOnDisk(); + + /* TODO: check if the code below is really needed */ + if (trueSize > toWrite) { + final int rest = (int) (trueSize - toWrite); + final ByteBuffer fill = ByteBuffer.allocate(rest); + chain.writeData(toWrite, fill); + } + } + + /** + * Returns the first cluster of the chain that stores this directory for + * non-root instances or 0 if this is the root directory. + * + * @return the first storage cluster of this directory + * @see #isRoot() + */ + @Override + protected final long getStorageCluster() { + return isRoot() ? 0 : chain.getStartCluster(); + } + + public final void delete() throws IOException { + chain.setChainLength(0); + } + + @Override + protected final void changeSize(int entryCount) + throws IOException, IllegalArgumentException { + + assert (entryCount >= 0); + + final int size = entryCount * FatDirectoryEntry.SIZE; + + if (size > MAX_SIZE) throw new DirectoryFullException( + "directory would grow beyond " + MAX_SIZE + " bytes", + getCapacity(), entryCount); + + sizeChanged(chain.setSize(Math.max(size, chain.getClusterSize()))); + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/DirectoryFullException.java b/src/main/java/de/waldheinz/fs/fat/DirectoryFullException.java new file mode 100644 index 0000000..5f9770c --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/DirectoryFullException.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import java.io.IOException; + +/** + * Gets thrown when either + * <ul> + * <li>a {@link Fat16RootDirectory} becomes full or</li> + * <li>a {@link ClusterChainDirectory} grows beyond it's + * {@link ClusterChainDirectory#MAX_SIZE maximum size} + * </ul> + * + * @author Matthias Treydte <waldheinz at gmail.com> + */ +public final class DirectoryFullException extends IOException { + private final static long serialVersionUID = 2; + private final int currentCapacity; + private final int requestedCapacity; + + DirectoryFullException(int currentCapacity, int requestedCapacity) { + this("directory is full", currentCapacity, requestedCapacity); + } + + DirectoryFullException(String message, + int currentCapacity, int requestedCapacity) { + + super(message); + + this.currentCapacity = currentCapacity; + this.requestedCapacity = requestedCapacity; + } + + /** + * Returns the current capacity of the directory. + * + * @return the current capacity + */ + public int getCurrentCapacity() { + return currentCapacity; + } + + /** + * Returns the capacity the directory tried to grow, which did not succeed. + * + * @return the requested capacity + */ + public int getRequestedCapacity() { + return requestedCapacity; + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/DosUtils.java b/src/main/java/de/waldheinz/fs/fat/DosUtils.java new file mode 100644 index 0000000..09e1ebb --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/DosUtils.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import java.util.Calendar; + +/** + * This class contains some methods for date and time conversions between Java + * and the format known from DOS filesystems (e.g. fat) + * + * @author Ewout Prangsma < epr at jnode.org> + */ +final class DosUtils { + + private DosUtils() { /* no instances */ } + + /** + * Decode a 16-bit encoded DOS date/time into a java date/time. + * + * @param dosDate + * @param dosTime + * @return long + */ + public static long decodeDateTime(int dosDate, int dosTime) { + final Calendar cal = Calendar.getInstance(); + + cal.set(Calendar.MILLISECOND, 0); + cal.set(Calendar.SECOND, (dosTime & 0x1f) * 2); + cal.set(Calendar.MINUTE, (dosTime >> 5) & 0x3f); + cal.set(Calendar.HOUR_OF_DAY, dosTime >> 11); + + cal.set(Calendar.DATE, dosDate & 0x1f); + cal.set(Calendar.MONTH, ((dosDate >> 5) & 0x0f) - 1); + cal.set(Calendar.YEAR, 1980 + (dosDate >> 9)); + + return cal.getTimeInMillis(); + } + + /** + * Encode a java date/time into a 16-bit encoded DOS time + * + * @param javaDateTime + * @return long + */ + public static int encodeTime(long javaDateTime) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(javaDateTime); + return 2048 * cal.get(Calendar.HOUR_OF_DAY) + 32 * cal.get(Calendar.MINUTE) + + cal.get(Calendar.SECOND) / 2; + } + + /** + * Encode a java date/time into a 16-bit encoded DOS date + * + * @param javaDateTime + * @return long + */ + public static int encodeDate(long javaDateTime) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(javaDateTime); + return 512 * (cal.get(Calendar.YEAR) - 1980) + 32 * (cal.get(Calendar.MONTH) + 1) + + cal.get(Calendar.DATE); + } +} diff --git a/src/main/java/de/waldheinz/fs/fat/Dummy83BufferGenerator.java b/src/main/java/de/waldheinz/fs/fat/Dummy83BufferGenerator.java new file mode 100644 index 0000000..d9848cc --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/Dummy83BufferGenerator.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2012 Google, Inc. + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import java.security.SecureRandom; + +/** + * Generates dummy 8.3 buffers that are associated with the long names. + * + * @author Daniel Galpin <dgalpin@google.com> based upon the work of + * Andrew Tridgell <tridge@samba.org> + */ +final class Dummy83BufferGenerator { + final SecureRandom mRandom; + + /** + * Creates a new instance of {@code Dummy83BufferGenerator} that uses + * randomness only to avoid short name collisions. + */ + public Dummy83BufferGenerator() { + mRandom = new SecureRandom(); + } + + /* + * Its in the DOS manual!(DOS 5: page 72) Valid: A..Z 0..9 _ ^ $ ~ ! # % & - {} () @ ' ` + * + * Invalid: spaces/periods, + */ + public static boolean validChar(char toTest) { + if (toTest >= 'A' && toTest <= 'Z') return true; + if (toTest >= 'a' && toTest <= 'z') return true; + if (toTest >= '0' && toTest <= '9') return true; + if (toTest == '_' || toTest == '^' || toTest == '$' || toTest == '~' || + toTest == '!' || toTest == '#' || toTest == '%' || toTest == '&' || + toTest == '-' || toTest == '{' || toTest == '}' || toTest == '(' || + toTest == ')' || toTest == '@' || toTest == '\'' || toTest == '`') + return true; + + return false; + } + + public static boolean isSkipChar(char c) { + return (c == '.') || (c == ' '); + } + + public static String tidyString(String dirty) { + final StringBuilder result = new StringBuilder(); + + /* epurate it from alien characters */ + for (int src=0; src < dirty.length(); src++) { + final char toTest = Character.toUpperCase(dirty.charAt(src)); + if (isSkipChar(toTest)) continue; + + if (validChar(toTest)) { + result.append(toTest); + } else { + result.append('_'); + } + } + + return result.toString(); + } + + public static boolean cleanString(String s) { + for (int i=0; i < s.length(); i++) { + if (isSkipChar(s.charAt(i))) return false; + if (!validChar(s.charAt(i))) return false; + } + + return true; + } + + public static String stripLeadingPeriods(String str) { + final StringBuilder sb = new StringBuilder(str.length()); + + for (int i=0; i < str.length(); i++) { + if (str.charAt(i) != '.') { //NOI18N + sb.append(str.substring(i)); + break; + } + } + + return sb.toString(); + } /* + * These characters are all invalid in 8.3 names, plus have been shown to be + * harmless on all tested devices + */ + static final private char[] invalidchar = { + (char) 0x01, (char) 0x02, (char) 0x03, (char) 0x04, (char) 0x05, (char) 0x06, + (char) 0x0B, + (char) 0x0C, (char) 0x0E, (char) 0x0F, (char) 0x10, (char) 0x11, (char) 0x12, + (char) 0x13, + (char) 0x14, (char) 0x15, (char) 0x16, (char) 0x17, (char) 0x18, (char) 0x19, + (char) 0x1A, + (char) 0x1B, (char) 0x1C, (char) 0x1D, (char) 0x1E, (char) 0x1F, (char) 0x22, + (char) 0x2a, + (char) 0x3a, (char) 0x3c, (char) 0x3e, (char) 0x3f, (char) 0x5b, (char) 0x5d, + (char) 0x7c + }; + + /** + * See original C Linux patch by Andrew Tridgell <tridge@samba.org> + * build a 11 byte 8.3 buffer which is not a short filename. We want 11 + * bytes which: - will be seen as a constant string to all APIs on Linux and + * Windows - cannot be matched with wildcard patterns - cannot be used to + * access the file - has a low probability of collision within a directory - + * has an invalid 3 byte extension - contains at least one non-space and + * non-nul byte + * + * @param longFullName the long file name to generate the buffer for + * @return the generated 8.3 buffer + */ + public ShortName generate83BufferNew(String longFullName) + throws IllegalStateException { + + char[] retBuffer = new char[11]; + + boolean hasRealShortName = false;// getRealShortNameInstead(longFullName, + // retBuffer); + if (!hasRealShortName) { + int i, tilde_pos, slash_pos; + int randomNumber = Math.abs(mRandom.nextInt()); + + /* + * the '/' makes sure that even unpatched Linux systems can't get at + * files by the 8.3 entry. + */ + + slash_pos = randomNumber % 8; + randomNumber >>= 3; + + /* + * fill in the first 8 bytes with invalid characters. Note that we + * need to be careful not to run out of randomness. We use the same + * extension for all buffers. + */ + for (i = 0; i < 8; i++) { + if (i == slash_pos) + retBuffer[i] = '/'; + else { + retBuffer[i] = + invalidchar[randomNumber % invalidchar.length]; + randomNumber /= invalidchar.length; + if (randomNumber < invalidchar.length) + randomNumber = Math.abs(mRandom.nextInt()); + } + } + + for ( i = 0; i < 8; i ++ ) { + if (retBuffer[i] == 0xe5) { + throw new RuntimeException(); + } + } + + retBuffer[8] = 'i'; + retBuffer[9] = 'f'; + retBuffer[10] = 'l'; + } + ShortName retName = new ShortName(retBuffer); + retName.setHasShortNameOnly(hasRealShortName); + return retName; + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/Fat.java b/src/main/java/de/waldheinz/fs/fat/Fat.java new file mode 100644 index 0000000..3f3ab43 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/Fat.java @@ -0,0 +1,477 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import de.waldheinz.fs.BlockDevice; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * + * + * @author Ewout Prangsma <epr at jnode.org> + * @author Matthias Treydte <waldheinz at gmail.com> + */ +public final class Fat { + + /** + * The first cluster that really holds user data in a FAT. + */ + public final static int FIRST_CLUSTER = 2; + + private final long[] entries; + private final FatType fatType; + private final int sectorCount; + private final int sectorSize; + private final BlockDevice device; + private final BootSector bs; + private final long offset; + private final int lastClusterIndex; + + private int lastAllocatedCluster; + + /** + * Reads a {@code Fat} as specified by a {@code BootSector}. + * + * @param bs the boot sector specifying the {@code Fat} layout + * @param fatNr the number of the {@code Fat} to read + * @return the {@code Fat} that was read + * @throws IOException on read error + * @throws IllegalArgumentException if {@code fatNr} is greater than + * {@link BootSector#getNrFats()} + */ + public static Fat read(BootSector bs, int fatNr) + throws IOException, IllegalArgumentException { + + if (fatNr > bs.getNrFats()) { + throw new IllegalArgumentException( + "boot sector says there are only " + bs.getNrFats() + + " FATs when reading FAT #" + fatNr); + } + + final long fatOffset = FatUtils.getFatOffset(bs, fatNr); + final Fat result = new Fat(bs, fatOffset); + result.read(); + return result; + } + + /** + * Creates a new {@code Fat} as specified by a {@code BootSector}. + * + * @param bs the boot sector specifying the {@code Fat} layout + * @param fatNr the number of the {@code Fat} to create + * @return the {@code Fat} that was created + * @throws IOException on write error + * @throws IllegalArgumentException if {@code fatNr} is greater than + * {@link BootSector#getNrFats()} + */ + public static Fat create(BootSector bs, int fatNr) + throws IOException, IllegalArgumentException { + + if (fatNr > bs.getNrFats()) { + throw new IllegalArgumentException( + "boot sector says there are only " + bs.getNrFats() + + " FATs when creating FAT #" + fatNr); + } + + final long fatOffset = FatUtils.getFatOffset(bs, fatNr); + final Fat result = new Fat(bs, fatOffset); + + if (bs.getDataClusterCount() > result.entries.length) + throw new IOException("FAT too small for device"); + + result.init(bs.getMediumDescriptor()); + result.write(); + return result; + } + + private Fat(BootSector bs, long offset) throws IOException { + this.bs = bs; + this.fatType = bs.getFatType(); + if (bs.getSectorsPerFat() > Integer.MAX_VALUE) + throw new IllegalArgumentException("FAT too large"); + + if (bs.getSectorsPerFat() <= 0) throw new IOException( + "boot sector says there are " + bs.getSectorsPerFat() + + " sectors per FAT"); + + if (bs.getBytesPerSector() <= 0) throw new IOException( + "boot sector says there are " + bs.getBytesPerSector() + + " bytes per sector"); + + this.sectorCount = (int) bs.getSectorsPerFat(); + this.sectorSize = bs.getBytesPerSector(); + this.device = bs.getDevice(); + this.offset = offset; + this.lastAllocatedCluster = FIRST_CLUSTER; + + if (bs.getDataClusterCount() > Integer.MAX_VALUE) throw + new IOException("too many data clusters"); + + if (bs.getDataClusterCount() == 0) throw + new IOException("no data clusters"); + + this.lastClusterIndex = (int) bs.getDataClusterCount() + FIRST_CLUSTER; + + entries = new long[(int) ((sectorCount * sectorSize) / + fatType.getEntrySize())]; + + if (lastClusterIndex > entries.length) throw new IOException( + "file system has " + lastClusterIndex + + "clusters but only " + entries.length + " FAT entries"); + } + + public FatType getFatType() { + return fatType; + } + + /** + * Returns the {@code BootSector} that specifies this {@code Fat}. + * + * @return this {@code Fat}'s {@code BootSector} + */ + public BootSector getBootSector() { + return this.bs; + } + + /** + * Returns the {@code BlockDevice} where this {@code Fat} is stored. + * + * @return the device holding this FAT + */ + public BlockDevice getDevice() { + return device; + } + + private void init(int mediumDescriptor) { + entries[0] = + (mediumDescriptor & 0xFF) | + (0xFFFFF00L & fatType.getBitMask()); + entries[1] = fatType.getEofMarker(); + } + + /** + * Read the contents of this FAT from the given device at the given offset. + * + * @param offset the byte offset where to read the FAT from the device + * @throws IOException on read error + */ + private void read() throws IOException { + final byte[] data = new byte[sectorCount * sectorSize]; + device.read(offset, ByteBuffer.wrap(data)); + + for (int i = 0; i < entries.length; i++) + entries[i] = fatType.readEntry(data, i); + } + + public void write() throws IOException { + this.writeCopy(offset); + } + + /** + * Write the contents of this FAT to the given device at the given offset. + * + * @param offset the device offset where to write the FAT copy + * @throws IOException on write error + */ + public void writeCopy(long offset) throws IOException { + final byte[] data = new byte[sectorCount * sectorSize]; + + for (int index = 0; index < entries.length; index++) { + fatType.writeEntry(data, index, entries[index]); + } + + device.write(offset, ByteBuffer.wrap(data)); + } + + /** + * Gets the medium descriptor byte + * + * @return int + */ + public int getMediumDescriptor() { + return (int) (entries[0] & 0xFF); + } + + /** + * Gets the entry at a given offset + * + * @param index + * @return long + */ + public long getEntry(int index) { + return entries[index]; + } + + /** + * Returns the last free cluster that was accessed in this FAT. + * + * @return the last seen free cluster + */ + public int getLastFreeCluster() { + return this.lastAllocatedCluster; + } + + public long[] getChain(long startCluster) { + testCluster(startCluster); + // Count the chain first + int count = 1; + long cluster = startCluster; + while (!isEofCluster(entries[(int) cluster])) { + count++; + cluster = entries[(int) cluster]; + } + // Now create the chain + long[] chain = new long[count]; + chain[0] = startCluster; + cluster = startCluster; + int i = 0; + while (!isEofCluster(entries[(int) cluster])) { + cluster = entries[(int) cluster]; + chain[++i] = cluster; + } + return chain; + } + + /** + * Gets the cluster after the given cluster + * + * @param cluster + * @return long The next cluster number or -1 which means eof. + */ + public long getNextCluster(long cluster) { + testCluster(cluster); + long entry = entries[(int) cluster]; + if (isEofCluster(entry)) { + return -1; + } else { + return entry; + } + } + + /** + * Allocate a cluster for a new file + * + * @return long the number of the newly allocated cluster + * @throws IOException if there are no free clusters + */ + public long allocNew() throws IOException { + + int i; + int entryIndex = -1; + + for (i = lastAllocatedCluster; i < lastClusterIndex; i++) { + if (isFreeCluster(i)) { + entryIndex = i; + break; + } + } + + if (entryIndex < 0) { + for (i = FIRST_CLUSTER; i < lastAllocatedCluster; i++) { + if (isFreeCluster(i)) { + entryIndex = i; + break; + } + } + } + + if (entryIndex < 0) { + throw new IOException( + "FAT Full (" + (lastClusterIndex - FIRST_CLUSTER) + + ", " + i + ")"); //NOI18N + } + + entries[entryIndex] = fatType.getEofMarker(); + lastAllocatedCluster = entryIndex % lastClusterIndex; + if (lastAllocatedCluster < FIRST_CLUSTER) + lastAllocatedCluster = FIRST_CLUSTER; + + return entryIndex; + } + + /** + * Returns the number of clusters that are currently not in use by this FAT. + * This estimate does only account for clusters that are really available in + * the data portion of the file system, not for clusters that might only + * theoretically be stored in the {@code Fat}. + * + * @return the free cluster count + * @see FsInfoSector#setFreeClusterCount(long) + * @see FsInfoSector#getFreeClusterCount() + * @see BootSector#getDataClusterCount() + */ + public int getFreeClusterCount() { + int result = 0; + + for (int i=FIRST_CLUSTER; i < lastClusterIndex; i++) { + if (isFreeCluster(i)) result++; + } + + return result; + } + + /** + * Returns the cluster number that was last allocated in this fat. + * + * @return + */ + public int getLastAllocatedCluster() { + return this.lastAllocatedCluster; + } + + /** + * Allocate a series of clusters for a new file. + * + * @param nrClusters when number of clusters to allocate + * @return long + * @throws IOException if there are no free clusters + */ + public long[] allocNew(int nrClusters) throws IOException { + final long rc[] = new long[nrClusters]; + + rc[0] = allocNew(); + for (int i = 1; i < nrClusters; i++) { + rc[i] = allocAppend(rc[i - 1]); + } + + return rc; + } + + /** + * Allocate a cluster to append to a new file + * + * @param cluster a cluster from a chain where the new cluster should be + * appended + * @return long the newly allocated and appended cluster number + * @throws IOException if there are no free clusters + */ + public long allocAppend(long cluster) + throws IOException { + + testCluster(cluster); + + while (!isEofCluster(entries[(int) cluster])) { + cluster = entries[(int) cluster]; + } + + long newCluster = allocNew(); + entries[(int) cluster] = newCluster; + + return newCluster; + } + + public void setEof(long cluster) { + testCluster(cluster); + entries[(int) cluster] = fatType.getEofMarker(); + } + + public void setFree(long cluster) { + testCluster(cluster); + entries[(int) cluster] = 0; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Fat)) return false; + + final Fat other = (Fat) obj; + if (this.fatType != other.fatType) return false; + if (this.sectorCount != other.sectorCount) return false; + if (this.sectorSize != other.sectorSize) return false; + if (this.lastClusterIndex != other.lastClusterIndex) return false; + if (!Arrays.equals(this.entries, other.entries)) return false; + if (this.getMediumDescriptor() != other.getMediumDescriptor()) + return false; + + return true; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 23 * hash + Arrays.hashCode(this.entries); + hash = 23 * hash + this.fatType.hashCode(); + hash = 23 * hash + this.sectorCount; + hash = 23 * hash + this.sectorSize; + hash = 23 * hash + this.lastClusterIndex; + return hash; + } + + /** + * Is the given entry a free cluster? + * + * @param entry + * @return boolean + */ + protected boolean isFreeCluster(long entry) { + if (entry > Integer.MAX_VALUE) throw new IllegalArgumentException(); + return (entries[(int) entry] == 0); + } + + /** + * Is the given entry a reserved cluster? + * + * @param entry + * @return boolean + */ + protected boolean isReservedCluster(long entry) { + return fatType.isReservedCluster(entry); + } + + /** + * Is the given entry an EOF marker + * + * @param entry + * @return boolean + */ + protected boolean isEofCluster(long entry) { + return fatType.isEofCluster(entry); + } + + protected void testCluster(long cluster) throws IllegalArgumentException { + if ((cluster < FIRST_CLUSTER) || (cluster >= entries.length)) { + throw new IllegalArgumentException( + "invalid cluster value " + cluster); + } + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + + sb.append(this.getClass().getSimpleName()); + sb.append("[type="); + sb.append(fatType); + sb.append(", mediumDescriptor=0x"); + sb.append(Integer.toHexString(getMediumDescriptor())); + sb.append(", sectorCount="); + sb.append(sectorCount); + sb.append(", sectorSize="); + sb.append(sectorSize); + sb.append(", freeClusters="); + sb.append(getFreeClusterCount()); + sb.append("]"); + + return sb.toString(); + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/Fat16BootSector.java b/src/main/java/de/waldheinz/fs/fat/Fat16BootSector.java new file mode 100644 index 0000000..38bdc2e --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/Fat16BootSector.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import de.waldheinz.fs.BlockDevice; +import java.io.IOException; + +/** + * The boot sector layout as used by the FAT12 / FAT16 variants. + * + * @author Matthias Treydte <matthias.treydte at meetwise.com> + */ +final class Fat16BootSector extends BootSector { + + /** + * The default number of entries for the root directory. + * + * @see #getRootDirEntryCount() + * @see #setRootDirEntryCount(int) + */ + public static final int DEFAULT_ROOT_DIR_ENTRY_COUNT = 512; + + /** + * The default volume label. + */ + public static final String DEFAULT_VOLUME_LABEL = "NO NAME"; //NOI18N + + /** + * The maximum number of clusters for a FAT12 file system. This is actually + * the number of clusters where mkdosfs stop complaining about a FAT16 + * partition having not enough sectors, so it would be misinterpreted + * as FAT12 without special handling. + * + * @see #getNrLogicalSectors() + */ + public static final int MAX_FAT12_CLUSTERS = 4084; + + public static final int MAX_FAT16_CLUSTERS = 65524; + + /** + * The offset to the sectors per FAT value. + */ + public static final int SECTORS_PER_FAT_OFFSET = 0x16; + + /** + * The offset to the root directory entry count value. + * + * @see #getRootDirEntryCount() + * @see #setRootDirEntryCount(int) + */ + public static final int ROOT_DIR_ENTRIES_OFFSET = 0x11; + + /** + * The offset to the first byte of the volume label. + */ + public static final int VOLUME_LABEL_OFFSET = 0x2b; + + /** + * Offset to the FAT file system type string. + * + * @see #getFileSystemType() + */ + public static final int FILE_SYSTEM_TYPE_OFFSET = 0x36; + + /** + * The maximum length of the volume label. + */ + public static final int MAX_VOLUME_LABEL_LENGTH = 11; + + public static final int EXTENDED_BOOT_SIGNATURE_OFFSET = 0x26; + + /** + * Creates a new {@code Fat16BootSector} for the specified device. + * + * @param device the {@code BlockDevice} holding the boot sector + */ + public Fat16BootSector(BlockDevice device) { + super(device); + } + + /** + * Returns the volume label that is stored in this boot sector. + * + * @return the volume label + */ + public String getVolumeLabel() { + final StringBuilder sb = new StringBuilder(); + + for (int i=0; i < MAX_VOLUME_LABEL_LENGTH; i++) { + final char c = (char) get8(VOLUME_LABEL_OFFSET + i); + + if (c != 0) { + sb.append(c); + } else { + break; + } + } + + return sb.toString(); + } + + /** + * Sets the volume label that is stored in this boot sector. + * + * @param label the new volume label + * @throws IllegalArgumentException if the specified label is longer + * than {@link #MAX_VOLUME_LABEL_LENGTH} + */ + public void setVolumeLabel(String label) throws IllegalArgumentException { + if (label.length() > MAX_VOLUME_LABEL_LENGTH) + throw new IllegalArgumentException("volume label too long"); + + for (int i = 0; i < MAX_VOLUME_LABEL_LENGTH; i++) { + set8(VOLUME_LABEL_OFFSET + i, + i < label.length() ? label.charAt(i) : 0); + } + } + + /** + * Gets the number of sectors/fat for FAT 12/16. + * + * @return int + */ + @Override + public long getSectorsPerFat() { + return get16(SECTORS_PER_FAT_OFFSET); + } + + /** + * Sets the number of sectors/fat + * + * @param v the new number of sectors per fat + */ + @Override + public void setSectorsPerFat(long v) { + if (v == getSectorsPerFat()) return; + if (v > 0x7FFF) throw new IllegalArgumentException( + "too many sectors for a FAT12/16"); + + set16(SECTORS_PER_FAT_OFFSET, (int)v); + } + + @Override + public FatType getFatType() { + final long rootDirSectors = ((getRootDirEntryCount() * 32) + + (getBytesPerSector() - 1)) / getBytesPerSector(); + final long dataSectors = getSectorCount() - + (getNrReservedSectors() + (getNrFats() * getSectorsPerFat()) + + rootDirSectors); + final long clusterCount = dataSectors / getSectorsPerCluster(); + + if (clusterCount > MAX_FAT16_CLUSTERS) throw new IllegalStateException( + "too many clusters for FAT12/16: " + clusterCount); + + return clusterCount > MAX_FAT12_CLUSTERS ? + FatType.FAT16 : FatType.FAT12; + } + + @Override + public void setSectorCount(long count) { + if (count > 65535) { + setNrLogicalSectors(0); + setNrTotalSectors(count); + } else { + setNrLogicalSectors((int) count); + setNrTotalSectors(count); + } + } + + @Override + public long getSectorCount() { + if (getNrLogicalSectors() == 0) return getNrTotalSectors(); + else return getNrLogicalSectors(); + } + + /** + * Gets the number of entries in the root directory. + * + * @return int the root directory entry count + */ + @Override + public int getRootDirEntryCount() { + return get16(ROOT_DIR_ENTRIES_OFFSET); + } + + /** + * Sets the number of entries in the root directory + * + * @param v the new number of entries in the root directory + * @throws IllegalArgumentException for negative values + */ + public void setRootDirEntryCount(int v) throws IllegalArgumentException { + if (v < 0) throw new IllegalArgumentException(); + if (v == getRootDirEntryCount()) return; + + set16(ROOT_DIR_ENTRIES_OFFSET, v); + } + + @Override + public void init() throws IOException { + super.init(); + + setRootDirEntryCount(DEFAULT_ROOT_DIR_ENTRY_COUNT); + setVolumeLabel(DEFAULT_VOLUME_LABEL); + } + + @Override + public int getFileSystemTypeLabelOffset() { + return FILE_SYSTEM_TYPE_OFFSET; + } + + @Override + public int getExtendedBootSignatureOffset() { + return EXTENDED_BOOT_SIGNATURE_OFFSET; + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/Fat16RootDirectory.java b/src/main/java/de/waldheinz/fs/fat/Fat16RootDirectory.java new file mode 100644 index 0000000..958e8a0 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/Fat16RootDirectory.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import de.waldheinz.fs.BlockDevice; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * The root directory of a FAT12/16 partition. + * + * @author Matthias Treydte <waldheinz at gmail.com> + */ +final class Fat16RootDirectory extends AbstractDirectory { + private final BlockDevice device; + private final long deviceOffset; + + private Fat16RootDirectory(Fat16BootSector bs, boolean readOnly) { + super(bs.getRootDirEntryCount(), readOnly, true); + + if (bs.getRootDirEntryCount() <= 0) throw new IllegalArgumentException( + "root directory size is " + bs.getRootDirEntryCount()); + + this.deviceOffset = FatUtils.getRootDirOffset(bs); + this.device = bs.getDevice(); + } + + /** + * Reads a {@code Fat16RootDirectory} as indicated by the specified + * {@code Fat16BootSector}. + * + * @param bs the boot sector that describes the root directory to read + * @param readOnly if the directory shold be created read-only + * @return the directory that was read + * @throws IOException on read error + */ + public static Fat16RootDirectory read( + Fat16BootSector bs, boolean readOnly) throws IOException { + + final Fat16RootDirectory result = new Fat16RootDirectory(bs, readOnly); + result.read(); + return result; + } + + /** + * Creates a new {@code Fat16RootDirectory} as indicated by the specified + * {@code Fat16BootSector}. The directory will always be created in + * read-write mode. + * + * @param bs the boot sector that describes the root directory to create + * @return the directory that was created + * @throws IOException on write error + */ + public static Fat16RootDirectory create( + Fat16BootSector bs) throws IOException { + + final Fat16RootDirectory result = new Fat16RootDirectory(bs, false); + result.flush(); + return result; + } + + @Override + protected void read(ByteBuffer data) throws IOException { + this.device.read(deviceOffset, data); + } + + @Override + protected void write(ByteBuffer data) throws IOException { + this.device.write(deviceOffset, data); + } + + /** + * By convention always returns 0, as the FAT12/16 root directory is not + * stored in a cluster chain. + * + * @return always 0 + */ + @Override + protected long getStorageCluster() { + return 0; + } + + /** + * As a FAT12/16 root directory can not change it's size, this method + * throws a {@code DirectoryFullException} if the requested size is + * larger than {@link #getCapacity()} and does nothing else. + * + * @param entryCount {@inheritDoc} + */ + @Override + protected void changeSize(int entryCount) throws DirectoryFullException { + if (getCapacity() < entryCount) { + throw new DirectoryFullException(getCapacity(), entryCount); + } + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/Fat32BootSector.java b/src/main/java/de/waldheinz/fs/fat/Fat32BootSector.java new file mode 100644 index 0000000..eded7ee --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/Fat32BootSector.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import de.waldheinz.fs.BlockDevice; +import java.io.IOException; + +/** + * Contains the FAT32 specific parts of the boot sector. + * + * @author Matthias Treydte <matthias.treydte at meetwise.com> + */ +final class Fat32BootSector extends BootSector { + + /** + * The offset to the entry specifying the first cluster of the FAT32 + * root directory. + */ + public final static int ROOT_DIR_FIRST_CLUSTER_OFFSET = 0x2c; + + /** + * The offset to the 4 bytes specifying the sectors per FAT value. + */ + public static final int SECTORS_PER_FAT_OFFSET = 0x24; + + /** + * Offset to the file system type label. + */ + public static final int FILE_SYSTEM_TYPE_OFFSET = 0x52; + + public static final int VERSION_OFFSET = 0x2a; + public static final int VERSION = 0; + + public static final int FS_INFO_SECTOR_OFFSET = 0x30; + public static final int BOOT_SECTOR_COPY_OFFSET = 0x32; + public static final int EXTENDED_BOOT_SIGNATURE_OFFSET = 0x42; + + /* + * TODO: make this constructor private + */ + public Fat32BootSector(BlockDevice device) throws IOException { + super(device); + } + + @Override + public void init() throws IOException { + super.init(); + + set16(VERSION_OFFSET, VERSION); + + setBootSectorCopySector(6); /* as suggested by M$ */ + } + + /** + * Returns the first cluster in the FAT that contains the root directory. + * + * @return the root directory's first cluster + */ + public long getRootDirFirstCluster() { + return get32(ROOT_DIR_FIRST_CLUSTER_OFFSET); + } + + /** + * Sets the first cluster of the root directory. + * + * @param value the root directory's first cluster + */ + public void setRootDirFirstCluster(long value) { + if (getRootDirFirstCluster() == value) return; + + set32(ROOT_DIR_FIRST_CLUSTER_OFFSET, value); + } + + /** + * Sets the sectur number that contains a copy of the boot sector. + * + * @param sectNr the sector that contains a boot sector copy + */ + public void setBootSectorCopySector(int sectNr) { + if (getBootSectorCopySector() == sectNr) return; + if (sectNr < 0) throw new IllegalArgumentException( + "boot sector copy sector must be >= 0"); + + set16(BOOT_SECTOR_COPY_OFFSET, sectNr); + } + + /** + * Returns the sector that contains a copy of the boot sector, or 0 if + * there is no copy. + * + * @return the sector number of the boot sector copy + */ + public int getBootSectorCopySector() { + return get16(BOOT_SECTOR_COPY_OFFSET); + } + + /** + * Sets the 11-byte volume label stored at offset 0x47. + * + * @param label the new volume label, may be {@code null} + */ + public void setVolumeLabel(String label) { + for (int i=0; i < 11; i++) { + final byte c = + (label == null) ? 0 : + (label.length() > i) ? (byte) label.charAt(i) : 0x20; + + set8(0x47 + i, c); + } + } + + public int getFsInfoSectorNr() { + return get16(FS_INFO_SECTOR_OFFSET); + } + + public void setFsInfoSectorNr(int offset) { + if (getFsInfoSectorNr() == offset) return; + + set16(FS_INFO_SECTOR_OFFSET, offset); + } + + @Override + public void setSectorsPerFat(long v) { + if (getSectorsPerFat() == v) return; + + set32(SECTORS_PER_FAT_OFFSET, v); + } + + @Override + public long getSectorsPerFat() { + return get32(SECTORS_PER_FAT_OFFSET); + } + + @Override + public FatType getFatType() { + return FatType.FAT32; + } + + @Override + public void setSectorCount(long count) { + super.setNrTotalSectors(count); + } + + @Override + public long getSectorCount() { + return super.getNrTotalSectors(); + } + + /** + * This is always 0 for FAT32. + * + * @return always 0 + */ + @Override + public int getRootDirEntryCount() { + return 0; + } + + public void setFileSystemId(int id) { + super.set32(0x43, id); + } + + public int getFileSystemId() { + return (int) super.get32(0x43); + } + + /** + * Writes a copy of this boot sector to the specified device, if a copy + * is requested. + * + * @param device the device to write the boot sector copy to + * @throws IOException on write error + * @see #getBootSectorCopySector() + */ + public void writeCopy(BlockDevice device) throws IOException { + if (getBootSectorCopySector() > 0) { + final long offset = getBootSectorCopySector() * SIZE; + buffer.rewind(); + buffer.limit(buffer.capacity()); + device.write(offset, buffer); + } + } + + @Override + public int getFileSystemTypeLabelOffset() { + return FILE_SYSTEM_TYPE_OFFSET; + } + + @Override + public int getExtendedBootSignatureOffset() { + return EXTENDED_BOOT_SIGNATURE_OFFSET; + } +} diff --git a/src/main/java/de/waldheinz/fs/fat/FatDirectoryEntry.java b/src/main/java/de/waldheinz/fs/fat/FatDirectoryEntry.java new file mode 100644 index 0000000..b7a3611 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/FatDirectoryEntry.java @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import de.waldheinz.fs.AbstractFsObject; +import java.nio.ByteBuffer; + +/** + * + * + * @author Ewout Prangsma <epr at jnode.org> + * @author Matthias Treydte <waldheinz at gmail.com> + */ +final class FatDirectoryEntry extends AbstractFsObject { + + /** + * The size in bytes of an FAT directory entry. + */ + public final static int SIZE = 32; + + /** + * The offset to the attributes byte. + */ + private static final int OFFSET_ATTRIBUTES = 0x0b; + + /** + * The offset to the file size dword. + */ + private static final int OFFSET_FILE_SIZE = 0x1c; + + private static final int F_READONLY = 0x01; + private static final int F_HIDDEN = 0x02; + private static final int F_SYSTEM = 0x04; + private static final int F_VOLUME_ID = 0x08; + private static final int F_DIRECTORY = 0x10; + private static final int F_ARCHIVE = 0x20; + + private static final int MAX_CLUSTER = 0xFFFF; + + /** + * The magic byte denoting that this entry was deleted and is free + * for reuse. + * + * @see #isDeleted() + */ + public static final int ENTRY_DELETED_MAGIC = 0xe5; + + private final byte[] data; + private boolean dirty; + boolean hasShortNameOnly; + + FatDirectoryEntry(byte[] data, boolean readOnly) { + super(readOnly); + + this.data = data; + } + + private FatDirectoryEntry() { + this(new byte[SIZE], false); + + } + + /** + * Reads a {@code FatDirectoryEntry} from the specified {@code ByteBuffer}. + * The buffer must have at least {@link #SIZE} bytes remaining. The entry + * is read from the buffer's current position, and if this method returns + * non-null the position will have advanced by {@link #SIZE} bytes, + * otherwise the position will remain unchanged. + * + * @param buff the buffer to read the entry from + * @param readOnly if the resulting {@code FatDirecoryEntry} should be + * read-only + * @return the directory entry that was read from the buffer or {@code null} + * if there was no entry to read from the specified position (first + * byte was 0) + */ + public static FatDirectoryEntry read(ByteBuffer buff, boolean readOnly) { + assert (buff.remaining() >= SIZE); + + /* peek into the buffer to see if we're done with reading */ + + if (buff.get(buff.position()) == 0) return null; + + /* read the directory entry */ + + final byte[] data = new byte[SIZE]; + buff.get(data); + return new FatDirectoryEntry(data, readOnly); + } + + public static void writeNullEntry(ByteBuffer buff) { + for (int i=0; i < SIZE; i++) { + buff.put((byte) 0); + } + } + + /** + * Decides if this entry is a "volume label" entry according to the FAT + * specification. + * + * @return if this is a volume label entry + */ + public boolean isVolumeLabel() { + if (isLfnEntry()) return false; + else return ((getFlags() & (F_DIRECTORY | F_VOLUME_ID)) == F_VOLUME_ID); + } + + private void setFlag(int mask, boolean set) { + final int oldFlags = getFlags(); + + if (((oldFlags & mask) != 0) == set) return; + + if (set) { + setFlags(oldFlags | mask); + } else { + setFlags(oldFlags & ~mask); + } + + this.dirty = true; + } + + public boolean isSystemFlag() { + return ((getFlags() & F_SYSTEM) != 0); + } + + public void setSystemFlag(boolean isSystem) { + setFlag(F_SYSTEM, isSystem); + } + + public boolean isArchiveFlag() { + return ((getFlags() & F_ARCHIVE) != 0); + } + + public void setArchiveFlag(boolean isArchive) { + setFlag(F_ARCHIVE, isArchive); + } + + public boolean isHiddenFlag() { + return ((getFlags() & F_HIDDEN) != 0); + } + + public void setHiddenFlag(boolean isHidden) { + setFlag(F_HIDDEN, isHidden); + } + + public boolean isVolumeIdFlag() { + return ((getFlags() & F_VOLUME_ID) != 0); + } + + public boolean isLfnEntry() { + return isReadonlyFlag() && isSystemFlag() && + isHiddenFlag() && isVolumeIdFlag(); + } + + public boolean isDirty() { + return dirty; + } + + private int getFlags() { + return LittleEndian.getUInt8(data, OFFSET_ATTRIBUTES); + } + + private void setFlags(int flags) { + LittleEndian.setInt8(data, OFFSET_ATTRIBUTES, flags); + } + + public boolean isDirectory() { + return ((getFlags() & (F_DIRECTORY | F_VOLUME_ID)) == F_DIRECTORY); + } + + public static FatDirectoryEntry create(boolean directory) { + final FatDirectoryEntry result = new FatDirectoryEntry(); + + if (directory) { + result.setFlags(F_DIRECTORY); + } + + /* initialize date and time fields */ + + final long now = System.currentTimeMillis(); + result.setCreated(now); + result.setLastAccessed(now); + result.setLastModified(now); + + return result; + } + + public static FatDirectoryEntry createVolumeLabel(String volumeLabel) { + assert(volumeLabel != null); + + final byte[] data = new byte[SIZE]; + + System.arraycopy( + volumeLabel.getBytes(), 0, + data, 0, + volumeLabel.length()); + + final FatDirectoryEntry result = new FatDirectoryEntry(data, false); + result.setFlags(FatDirectoryEntry.F_VOLUME_ID); + return result; + } + + public String getVolumeLabel() { + if (!isVolumeLabel()) + throw new UnsupportedOperationException("not a volume label"); + + final StringBuilder sb = new StringBuilder(); + + for (int i=0; i < AbstractDirectory.MAX_LABEL_LENGTH; i++) { + final byte b = this.data[i]; + + if (b != 0) { + sb.append((char) b); + } else { + break; + } + } + + return sb.toString(); + } + + public long getCreated() { + return DosUtils.decodeDateTime( + LittleEndian.getUInt16(data, 0x10), + LittleEndian.getUInt16(data, 0x0e)); + } + + public void setCreated(long created) { + LittleEndian.setInt16(data, 0x0e, + DosUtils.encodeTime(created)); + LittleEndian.setInt16(data, 0x10, + DosUtils.encodeDate(created)); + + this.dirty = true; + } + + public long getLastModified() { + return DosUtils.decodeDateTime( + LittleEndian.getUInt16(data, 0x18), + LittleEndian.getUInt16(data, 0x16)); + } + + public void setLastModified(long lastModified) { + LittleEndian.setInt16(data, 0x16, + DosUtils.encodeTime(lastModified)); + LittleEndian.setInt16(data, 0x18, + DosUtils.encodeDate(lastModified)); + + this.dirty = true; + } + + public long getLastAccessed() { + return DosUtils.decodeDateTime( + LittleEndian.getUInt16(data, 0x12), + 0); /* time is not recorded */ + } + + public void setLastAccessed(long lastAccessed) { + LittleEndian.setInt16(data, 0x12, + DosUtils.encodeDate(lastAccessed)); + + this.dirty = true; + } + + /** + * Returns if this entry has been marked as deleted. A deleted entry has + * its first byte set to the magic {@link #ENTRY_DELETED_MAGIC} value. + * + * @return if this entry is marked as deleted + */ + public boolean isDeleted() { + return (LittleEndian.getUInt8(data, 0) == ENTRY_DELETED_MAGIC); + } + + /** + * Returns the size of this entry as stored at {@link #OFFSET_FILE_SIZE}. + * + * @return the size of the file represented by this entry + */ + public long getLength() { + return LittleEndian.getUInt32(data, OFFSET_FILE_SIZE); + } + + /** + * Sets the size of this entry stored at {@link #OFFSET_FILE_SIZE}. + * + * @param length the new size of the file represented by this entry + * @throws IllegalArgumentException if {@code length} is out of range + */ + public void setLength(long length) throws IllegalArgumentException { + LittleEndian.setInt32(data, OFFSET_FILE_SIZE, length); + } + + /** + * Returns the {@code ShortName} that is stored in this directory entry or + * {@code null} if this entry has not been initialized. + * + * @return the {@code ShortName} stored in this entry or {@code null} + */ + public ShortName getShortName() { + if (this.data[0] == 0) { + return null; + } else { + return ShortName.parse(this.data); + } + } + + /** + * Does this entry refer to a file? + * + * @return + * @see org.jnode.fs.FSDirectoryEntry#isFile() + */ + public boolean isFile() { + return ((getFlags() & (F_DIRECTORY | F_VOLUME_ID)) == 0); + } + + public void setShortName(ShortName sn) { + if (sn.equals(this.getShortName())) return; + + sn.write(this.data); + this.hasShortNameOnly = sn.hasShortNameOnly(); + this.dirty = true; + } + + /** + * Returns the startCluster. + * + * @return int + */ + public long getStartCluster() { + int lowBytes = LittleEndian.getUInt16(data, 0x1a); + long highBytes = LittleEndian.getUInt16(data, 0x14); + return ( highBytes << 16 | lowBytes ); + } + + /** + * Sets the startCluster. + * + * @param startCluster The startCluster to set + */ + void setStartCluster(long startCluster) { + if ( startCluster > Integer.MAX_VALUE ) throw new AssertionError(); + + LittleEndian.setInt16(data, 0x1a, (int) startCluster); + LittleEndian.setInt16(data, 0x14, (int)(startCluster >>> 16)); + } + + @Override + public String toString() { + return getClass().getSimpleName() + + " [name=" + getShortName() + "]"; //NOI18N + } + + /** + * Writes this directory entry into the specified buffer. + * + * @param buff the buffer to write this entry to + */ + void write(ByteBuffer buff) { + buff.put(data); + this.dirty = false; + } + + /** + * Returns if the read-only flag is set for this entry. Do not confuse + * this with {@link #isReadOnly()}. + * + * @return if the read only file system flag is set on this entry + * @see #F_READONLY + * @see #setReadonlyFlag(boolean) + */ + public boolean isReadonlyFlag() { + return ((getFlags() & F_READONLY) != 0); + } + + /** + * Updates the read-only file system flag for this entry. + * + * @param isReadonly the new value for the read-only flag + * @see #F_READONLY + * @see #isReadonlyFlag() + */ + public void setReadonlyFlag(boolean isReadonly) { + setFlag(F_READONLY, isReadonly); + } + + String getLfnPart() { + final char[] unicodechar = new char[13]; + + unicodechar[0] = (char) LittleEndian.getUInt16(data, 1); + unicodechar[1] = (char) LittleEndian.getUInt16(data, 3); + unicodechar[2] = (char) LittleEndian.getUInt16(data, 5); + unicodechar[3] = (char) LittleEndian.getUInt16(data, 7); + unicodechar[4] = (char) LittleEndian.getUInt16(data, 9); + unicodechar[5] = (char) LittleEndian.getUInt16(data, 14); + unicodechar[6] = (char) LittleEndian.getUInt16(data, 16); + unicodechar[7] = (char) LittleEndian.getUInt16(data, 18); + unicodechar[8] = (char) LittleEndian.getUInt16(data, 20); + unicodechar[9] = (char) LittleEndian.getUInt16(data, 22); + unicodechar[10] = (char) LittleEndian.getUInt16(data, 24); + unicodechar[11] = (char) LittleEndian.getUInt16(data, 28); + unicodechar[12] = (char) LittleEndian.getUInt16(data, 30); + + int end = 0; + + while ((end < 13) && (unicodechar[end] != '\0')) { + end++; + } + + return new String(unicodechar).substring(0, end); + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/FatFile.java b/src/main/java/de/waldheinz/fs/fat/FatFile.java new file mode 100644 index 0000000..95942e0 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/FatFile.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import de.waldheinz.fs.AbstractFsObject; +import java.io.IOException; +import de.waldheinz.fs.FsFile; +import de.waldheinz.fs.ReadOnlyException; +import java.io.EOFException; +import java.nio.ByteBuffer; + +/** + * The in-memory representation of a single file (chain of clusters) on a + * FAT file system. + * + * @author Matthias Treydte <waldheinz at gmail.com> + * @since 0.6 + */ +public final class FatFile extends AbstractFsObject implements FsFile { + private final FatDirectoryEntry entry; + private final ClusterChain chain; + + private FatFile(FatDirectoryEntry myEntry, ClusterChain chain) { + super(myEntry.isReadOnly()); + + this.entry = myEntry; + this.chain = chain; + } + + static FatFile get(Fat fat, FatDirectoryEntry entry) + throws IOException { + + if (entry.isDirectory()) + throw new IllegalArgumentException(entry + " is a directory"); + + final ClusterChain cc = new ClusterChain( + fat, entry.getStartCluster(), entry.isReadonlyFlag()); + + if (entry.getLength() > cc.getLengthOnDisk()) throw new IOException( + "entry is larger than associated cluster chain"); + + return new FatFile(entry, cc); + } + + /** + * Returns the length of this file in bytes. This is the length that + * is stored in the directory entry that is associated with this file. + * + * @return long the length that is recorded for this file + */ + @Override + public long getLength() { + checkValid(); + + return entry.getLength(); + } + + /** + * Sets the size (in bytes) of this file. Because + * {@link #write(long, java.nio.ByteBuffer) writing} to the file will grow + * it automatically if needed, this method is mainly usefull for truncating + * a file. + * + * @param length the new length of the file in bytes + * @throws ReadOnlyException if this file is read-only + * @throws IOException on error updating the file size + */ + @Override + public void setLength(long length) throws ReadOnlyException, IOException { + checkWritable(); + + if (getLength() == length) return; + + updateTimeStamps(true); + chain.setSize(length); + + this.entry.setStartCluster(chain.getStartCluster()); + this.entry.setLength(length); + } + + /** + * <p> + * {@inheritDoc} + * </p><p> + * Unless this file is {@link #isReadOnly() read-ony}, this method also + * updates the "last accessed" field in the directory entry that is + * associated with this file. + * </p> + * + * @param offset {@inheritDoc} + * @param dest {@inheritDoc} + * @see FatDirectoryEntry#setLastAccessed(long) + */ + @Override + public void read(long offset, ByteBuffer dest) throws IOException { + checkValid(); + + final int len = dest.remaining(); + + if (len == 0) return; + + if (offset + len > getLength()) { + throw new EOFException(); + } + + if (!isReadOnly()) { + updateTimeStamps(false); + } + + chain.readData(offset, dest); + } + + /** + * <p> + * {@inheritDoc} + * </p><p> + * If the data to be written extends beyond the current + * {@link #getLength() length} of this file, an attempt is made to + * {@link #setLength(long) grow} the file so that the data will fit. + * Additionally, this method updates the "last accessed" and "last modified" + * fields on the directory entry that is associated with this file. + * </p> + * + * @param offset {@inheritDoc} + * @param srcBuf {@inheritDoc} + */ + @Override + public void write(long offset, ByteBuffer srcBuf) + throws ReadOnlyException, IOException { + + checkWritable(); + + updateTimeStamps(true); + + final long lastByte = offset + srcBuf.remaining(); + + if (lastByte > getLength()) { + setLength(lastByte); + } + + chain.writeData(offset, srcBuf); + } + + private void updateTimeStamps(boolean write) { + final long now = System.currentTimeMillis(); + entry.setLastAccessed(now); + + if (write) { + entry.setLastModified(now); + } + } + + /** + * Has no effect besides possibly throwing an {@code ReadOnlyException}. To + * make sure that all data is written out to disk use the + * {@link FatFileSystem#flush()} method. + * + * @throws ReadOnlyException if this {@code FatFile} is read-only + */ + @Override + public void flush() throws ReadOnlyException { + checkWritable(); + + /* nothing else to do */ + } + + /** + * Returns the {@code ClusterChain} that holds the contents of + * this {@code FatFile}. + * + * @return the file's {@code ClusterChain} + */ + ClusterChain getChain() { + checkValid(); + + return chain; + } + + /** + * Returns a human-readable string representation of this {@code FatFile}, + * mainly for debugging purposes. + * + * @return a string describing this {@code FatFile} + */ + @Override + public String toString() { + return getClass().getSimpleName() + " [length=" + getLength() + + ", first cluster=" + chain.getStartCluster() + "]"; + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/FatFileSystem.java b/src/main/java/de/waldheinz/fs/fat/FatFileSystem.java new file mode 100644 index 0000000..db4d3e2 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/FatFileSystem.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import de.waldheinz.fs.AbstractFileSystem; +import de.waldheinz.fs.BlockDevice; +import java.io.IOException; +import de.waldheinz.fs.ReadOnlyException; + +/** + * <p> + * Implements the {@code FileSystem} interface for the FAT family of file + * systems. This class always uses the "long file name" specification when + * writing directory entries. + * </p><p> + * For creating (aka "formatting") FAT file systems please refer to the + * {@link SuperFloppyFormatter} class. + * </p> + * + * @author Ewout Prangsma <epr at jnode.org> + * @author Matthias Treydte <waldheinz at gmail.com> + */ +public final class FatFileSystem extends AbstractFileSystem { + + private final Fat fat; + private final FsInfoSector fsiSector; + private final BootSector bs; + private final FatLfnDirectory rootDir; + private final AbstractDirectory rootDirStore; + private final FatType fatType; + private final long filesOffset; + + FatFileSystem(BlockDevice api, boolean readOnly) throws IOException { + + this(api, readOnly, false); + } + + /** + * Constructor for FatFileSystem in specified readOnly mode + * + * @param device the {@code BlockDevice} holding the file system + * @param readOnly if this FS should be read-lonly + * @param ignoreFatDifferences + * @throws IOException on read error + */ + private FatFileSystem(BlockDevice device, boolean readOnly, + boolean ignoreFatDifferences) + throws IOException { + + super(readOnly); + + this.bs = BootSector.read(device); + + if (bs.getNrFats() <= 0) throw new IOException( + "boot sector says there are no FATs"); + + this.filesOffset = FatUtils.getFilesOffset(bs); + this.fatType = bs.getFatType(); + this.fat = Fat.read(bs, 0); + + if (!ignoreFatDifferences) { + for (int i=1; i < bs.getNrFats(); i++) { + final Fat tmpFat = Fat.read(bs, i); + if (!fat.equals(tmpFat)) { + throw new IOException("FAT " + i + " differs from FAT 0"); + } + } + } + + if (fatType == FatType.FAT32) { + final Fat32BootSector f32bs = (Fat32BootSector) bs; + ClusterChain rootDirFile = new ClusterChain(fat, + f32bs.getRootDirFirstCluster(), isReadOnly()); + this.rootDirStore = ClusterChainDirectory.readRoot(rootDirFile); + this.fsiSector = FsInfoSector.read(f32bs); + + if (fsiSector.getFreeClusterCount() != fat.getFreeClusterCount()) { + throw new IOException("free cluster count mismatch - fat: " + + fat.getFreeClusterCount() + " - fsinfo: " + + fsiSector.getFreeClusterCount()); + } + } else { + this.rootDirStore = + Fat16RootDirectory.read((Fat16BootSector) bs,readOnly); + this.fsiSector = null; + } + + this.rootDir = new FatLfnDirectory(rootDirStore, fat, isReadOnly()); + + } + + /** + * Reads the file system structure from the specified {@code BlockDevice} + * and returns a fresh {@code FatFileSystem} instance to read or modify + * it. + * + * @param device the {@code BlockDevice} holding the file system + * @param readOnly if the {@code FatFileSystem} should be in read-only mode + * @return the {@code FatFileSystem} instance for the device + * @throws IOException on read error or if the file system structure could + * not be parsed + */ + public static FatFileSystem read(BlockDevice device, boolean readOnly) + throws IOException { + + return new FatFileSystem(device, readOnly); + } + + long getFilesOffset() { + checkClosed(); + + return filesOffset; + } + + /** + * Returns the size of the FAT entries of this {@code FatFileSystem}. + * + * @return the exact type of the FAT used by this file system + */ + public FatType getFatType() { + checkClosed(); + + return this.fatType; + } + + /** + * Returns the volume label of this file system. + * + * @return the volume label + */ + public String getVolumeLabel() { + checkClosed(); + + final String fromDir = rootDirStore.getLabel(); + + if (fromDir == null && fatType != FatType.FAT32) { + return ((Fat16BootSector)bs).getVolumeLabel(); + } else { + return fromDir; + } + } + + /** + * Sets the volume label for this file system. + * + * @param label the new volume label, may be {@code null} + * @throws ReadOnlyException if the file system is read-only + * @throws IOException on write error + */ + public void setVolumeLabel(String label) + throws ReadOnlyException, IOException { + + checkClosed(); + checkReadOnly(); + + rootDirStore.setLabel(label); + + if (fatType != FatType.FAT32) { + ((Fat16BootSector)bs).setVolumeLabel(label); + } + } + + AbstractDirectory getRootDirStore() { + checkClosed(); + + return rootDirStore; + } + + /** + * Flush all changed structures to the device. + * + * @throws IOException on write error + */ + @Override + public void flush() throws IOException { + checkClosed(); + + if (bs.isDirty()) { + bs.write(); + } + + for (int i = 0; i < bs.getNrFats(); i++) { + fat.writeCopy(FatUtils.getFatOffset(bs, i)); + } + + rootDir.flush(); + + if (fsiSector != null) { + fsiSector.setFreeClusterCount(fat.getFreeClusterCount()); + fsiSector.setLastAllocatedCluster(fat.getLastAllocatedCluster()); + fsiSector.write(); + } + } + + @Override + public FatLfnDirectory getRoot() { + checkClosed(); + + return rootDir; + } + + /** + * Returns the fat. + * + * @return Fat + */ + public Fat getFat() { + return fat; + } + + /** + * Returns the bootsector. + * + * @return BootSector + */ + public BootSector getBootSector() { + checkClosed(); + + return bs; + } + + /** + * <p> + * {@inheritDoc} + * </p><p> + * This method shows the free space in terms of available clusters. + * </p> + * + * @return always -1 + */ + @Override + public long getFreeSpace() { + return this.fat.getFreeClusterCount() * this.bs.getBytesPerCluster(); + } + + /** + * <p> + * {@inheritDoc} + * </p><p> + * This method is currently not implemented for {@code FatFileSystem} and + * always returns -1. + * </p> + * + * @return always -1 + */ + @Override + public long getTotalSpace() { + return this.bs.getDataClusterCount() * this.bs.getBytesPerCluster(); + } + + /** + * <p> + * {@inheritDoc} + * </p><p> + * This method is currently not implemented for {@code FatFileSystem} and + * always returns -1. + * </p> + * + * @return always -1 + */ + @Override + public long getUsableSpace() { + // TODO implement me + return -1; + } +} diff --git a/src/main/java/de/waldheinz/fs/fat/FatLfnDirectory.java b/src/main/java/de/waldheinz/fs/fat/FatLfnDirectory.java new file mode 100644 index 0000000..9f4aa9b --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/FatLfnDirectory.java @@ -0,0 +1,438 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import de.waldheinz.fs.AbstractFsObject; +import de.waldheinz.fs.FsDirectory; +import de.waldheinz.fs.FsDirectoryEntry; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * The {@link FsDirectory} implementation for FAT file systems. This + * implementation aims to fully comply to the FAT specification, including + * the quite complex naming system regarding the long file names (LFNs) and + * their corresponding 8+3 short file names. This also means that an + * {@code FatLfnDirectory} is case-preserving but <em>not</em> case-sensitive. + * + * @author gbin + * @author Matthias Treydte <waldheinz at gmail.com> + * @since 0.6 + */ +public final class FatLfnDirectory + extends AbstractFsObject + implements FsDirectory { + + /** + * This set is used to check if a file name is already in use in this + * directory. The FAT specification says that file names must be unique + * ignoring the case, so this set contains all names converted to + * lower-case, and all checks must be performed using lower-case strings. + */ + private final Set<String> usedNames; + private final Fat fat; + private final Map<ShortName, FatLfnDirectoryEntry> shortNameIndex; + private final Map<String, FatLfnDirectoryEntry> longNameIndex; + private final Map<FatDirectoryEntry, FatFile> entryToFile; + private final Map<FatDirectoryEntry, FatLfnDirectory> entryToDirectory; + private Dummy83BufferGenerator dbg; + + final AbstractDirectory dir; + + FatLfnDirectory(AbstractDirectory dir, Fat fat, boolean readOnly) + throws IOException { + + super(readOnly); + + if ((dir == null) || (fat == null)) throw new NullPointerException(); + + this.fat = fat; + this.dir = dir; + + this.shortNameIndex = + new LinkedHashMap<ShortName, FatLfnDirectoryEntry>(); + + this.longNameIndex = + new LinkedHashMap<String, FatLfnDirectoryEntry>(); + + this.entryToFile = + new LinkedHashMap<FatDirectoryEntry, FatFile>(); + + this.entryToDirectory = + new LinkedHashMap<FatDirectoryEntry, FatLfnDirectory>(); + + this.usedNames = new HashSet<String>(); + this.dbg = new Dummy83BufferGenerator(); + + parseLfn(); + } + + FatFile getFile(FatDirectoryEntry entry) throws IOException { + FatFile file = entryToFile.get(entry); + + if (file == null) { + file = FatFile.get(fat, entry); + entryToFile.put(entry, file); + } + + return file; + } + + FatLfnDirectory getDirectory(FatDirectoryEntry entry) throws IOException { + FatLfnDirectory result = entryToDirectory.get(entry); + + if (result == null) { + final ClusterChainDirectory storage = read(entry, fat); + result = new FatLfnDirectory(storage, fat, isReadOnly()); + entryToDirectory.put(entry, result); + } + + return result; + } + + /** + * <p> + * {@inheritDoc} + * </p><p> + * According to the FAT file system specification, leading and trailing + * spaces in the {@code name} are ignored by this method. + * </p> + * + * @param name {@inheritDoc} + * @return {@inheritDoc} + * @throws IOException {@inheritDoc} + */ + @Override + public FatLfnDirectoryEntry addFile(String name) throws IOException { + checkWritable(); + checkUniqueName(name); + + name = name.trim(); + final ShortName sn = makeShortName(name, false); + + final FatLfnDirectoryEntry entry = + new FatLfnDirectoryEntry(name, sn, this, false); + + dir.addEntries(entry.compactForm()); + + shortNameIndex.put(sn, entry); + longNameIndex.put(name.toLowerCase(), entry); + + getFile(entry.realEntry); + + dir.setDirty(); + return entry; + } + + boolean isFreeName(String name) { + return true; + } + + private void checkUniqueName(String name) throws IOException { + } + + private void freeUniqueName(String name) { + } + + private ShortName makeShortName(String name, boolean isDirectory) throws IOException { + final ShortName result; + + try { + result = dbg.generate83BufferNew(name); + } catch (IllegalArgumentException ex) { + throw new IOException( + "could not generate short name for \"" + name + "\"", ex); + } + return result; + } + + /** + * <p> + * {@inheritDoc} + * </p><p> + * According to the FAT file system specification, leading and trailing + * spaces in the {@code name} are ignored by this method. + * </p> + * + * @param name {@inheritDoc} + * @return {@inheritDoc} + * @throws IOException {@inheritDoc} + */ + @Override + public FatLfnDirectoryEntry addDirectory(String name) throws IOException { + checkWritable(); + checkUniqueName(name); + + name = name.trim(); + final ShortName sn = makeShortName(name, true); + final FatDirectoryEntry real = dir.createSub(fat); + real.setShortName(sn); + final FatLfnDirectoryEntry e = + new FatLfnDirectoryEntry(this, real, name); + + try { + dir.addEntries(e.compactForm()); + } catch (IOException ex) { + final ClusterChain cc = + new ClusterChain(fat, real.getStartCluster(), false); + cc.setChainLength(0); + dir.removeEntry(real); + throw ex; + } + + shortNameIndex.put(sn, e); + longNameIndex.put(name.toLowerCase(), e); + + getDirectory(real); + + flush(); + return e; + } + + /** + * <p> + * {@inheritDoc} + * </p><p> + * According to the FAT file system specification, leading and trailing + * spaces in the {@code name} are ignored by this method. + * </p> + * + * @param name {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public FatLfnDirectoryEntry getEntry(String name) { + name = name.trim().toLowerCase(); + + final FatLfnDirectoryEntry entry = longNameIndex.get(name); + + if (entry == null) { + if (!ShortName.canConvert(name)) return null; + return shortNameIndex.get(ShortName.get(name)); + } else { + return entry; + } + } + + private void parseLfn() throws IOException { + int i = 0; + final int size = dir.getEntryCount(); + + while (i < size) { + // jump over empty entries + while (i < size && dir.getEntry(i) == null) { + i++; + } + + if (i >= size) { + break; + } + + int offset = i; // beginning of the entry + // check when we reach a real entry + while (dir.getEntry(i).isLfnEntry()) { + i++; + if (i >= size) { + // This is a cutted entry, forgive it + break; + } + } + + if (i >= size) { + // This is a cutted entry, forgive it + break; + } + + final FatLfnDirectoryEntry current = + FatLfnDirectoryEntry.extract(this, offset, ++i - offset); + + if (!current.realEntry.isDeleted() && current.isValid()) { + checkUniqueName(current.getName()); + + shortNameIndex.put(current.realEntry.getShortName(), current); + longNameIndex.put(current.getName().toLowerCase(), current); + } + } + } + + private void updateLFN() throws IOException { + ArrayList<FatDirectoryEntry> dest = + new ArrayList<FatDirectoryEntry>(); + + for (FatLfnDirectoryEntry currentEntry : shortNameIndex.values()) { + FatDirectoryEntry[] encoded = currentEntry.compactForm(); + dest.addAll(Arrays.asList(encoded)); + } + + final int size = dest.size(); + + dir.changeSize(size); + dir.setEntries(dest); + } + + @Override + public void flush() throws IOException { + checkWritable(); + + for (FatFile f : entryToFile.values()) { + f.flush(); + } + + for (FatLfnDirectory d : entryToDirectory.values()) { + d.flush(); + } + + updateLFN(); + dir.flush(); + } + + @Override + public Iterator<FsDirectoryEntry> iterator() { + return new Iterator<FsDirectoryEntry>() { + + final Iterator<FatLfnDirectoryEntry> it = + shortNameIndex.values().iterator(); + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public FsDirectoryEntry next() { + return it.next(); + } + + /** + * @see java.util.Iterator#remove() + */ + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + /** + * Remove the entry with the given name from this directory. + * + * @param name the name of the entry to remove + * @throws IOException on error removing the entry + * @throws IllegalArgumentException on an attempt to remove the dot entries + */ + @Override + public void remove(String name) + throws IOException, IllegalArgumentException { + + checkWritable(); + + final FatLfnDirectoryEntry entry = getEntry(name); + if (entry == null) return; + + unlinkEntry(entry); + + final ClusterChain cc = new ClusterChain( + fat, entry.realEntry.getStartCluster(), false); + + cc.setChainLength(0); + + freeUniqueName(name); + updateLFN(); + } + + /** + * Unlinks the specified entry from this directory without actually + * deleting it. + * + * @param e the entry to be unlinked + * @see #linkEntry(de.waldheinz.fs.fat.FatLfnDirectoryEntry) + */ + void unlinkEntry(FatLfnDirectoryEntry entry) { + final ShortName sn = entry.realEntry.getShortName(); + + if (sn.equals(ShortName.DOT) || sn.equals(ShortName.DOT_DOT)) throw + new IllegalArgumentException( + "the dot entries can not be removed"); + + final String lowerName = entry.getName().toLowerCase(); + + assert (this.longNameIndex.containsKey(lowerName)); + this.longNameIndex.remove(lowerName); + + assert (this.shortNameIndex.containsKey(sn)); + this.shortNameIndex.remove(sn); + + if (entry.isFile()) { + this.entryToFile.remove(entry.realEntry); + } else { + this.entryToDirectory.remove(entry.realEntry); + } + } + + /** + * Links the specified entry to this directory, updating the entrie's + * short name. + * + * @param entry the entry to be linked (added) to this directory + * @see #unlinkEntry(de.waldheinz.fs.fat.FatLfnDirectoryEntry) + */ + void linkEntry(FatLfnDirectoryEntry entry) throws IOException { + checkUniqueName(entry.getName()); + ShortName name; + name = this.dbg.generate83BufferNew(entry.getName()); + entry.realEntry.setShortName(name); + + this.longNameIndex.put(entry.getName().toLowerCase(), entry); + this.shortNameIndex.put(entry.realEntry.getShortName(), entry); + + updateLFN(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + + " [size=" + shortNameIndex.size() + //NOI18N + ", dir=" + dir + "]"; //NOI18N + } + + private static ClusterChainDirectory read(FatDirectoryEntry entry, Fat fat) + throws IOException { + + if (!entry.isDirectory()) throw + new IllegalArgumentException(entry + " is no directory"); + + final ClusterChain chain = new ClusterChain( + fat, entry.getStartCluster(), + entry.isReadonlyFlag()); + + final ClusterChainDirectory result = + new ClusterChainDirectory(chain, false); + + result.read(); + return result; + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/FatLfnDirectoryEntry.java b/src/main/java/de/waldheinz/fs/fat/FatLfnDirectoryEntry.java new file mode 100644 index 0000000..0882516 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/FatLfnDirectoryEntry.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import de.waldheinz.fs.AbstractFsObject; +import de.waldheinz.fs.FsDirectoryEntry; +import de.waldheinz.fs.ReadOnlyException; +import java.io.IOException; + +/** + * Represents an entry in a {@link FatLfnDirectory}. Besides implementing the + * {@link FsDirectoryEntry} interface for FAT file systems, it allows access + * to the {@link #setArchiveFlag(boolean) archive}, + * {@link #setHiddenFlag(boolean) hidden}, + * {@link #setReadOnlyFlag(boolean) read-only} and + * {@link #setSystemFlag(boolean) system} flags specifed for the FAT file + * system. + * + * @author Matthias Treydte <waldheinz at gmail.com> + * @since 0.6 + */ +public final class FatLfnDirectoryEntry + extends AbstractFsObject + implements FsDirectoryEntry { + + final FatDirectoryEntry realEntry; + + private FatLfnDirectory parent; + private String fileName; + + FatLfnDirectoryEntry(String name, ShortName sn, + FatLfnDirectory parent, boolean directory) { + + super(false); + + this.parent = parent; + this.fileName = name; + + final long now = System.currentTimeMillis(); + this.realEntry = FatDirectoryEntry.create(directory); + this.realEntry.setShortName(sn); + this.realEntry.setCreated(now); + this.realEntry.setLastAccessed(now); + } + + FatLfnDirectoryEntry(FatLfnDirectory parent, + FatDirectoryEntry realEntry, String fileName) { + + super(parent.isReadOnly()); + + this.parent = parent; + this.realEntry = realEntry; + this.fileName = fileName; + } + + static FatLfnDirectoryEntry extract( + FatLfnDirectory dir, int offset, int len) { + + final FatDirectoryEntry realEntry = dir.dir.getEntry(offset + len - 1); + final String fileName; + + if (len == 1) { + /* this is just an old plain 8.3 entry */ + fileName = realEntry.getShortName().asSimpleString(); + } else { + /* stored in reverse order */ + final StringBuilder name = new StringBuilder(13 * (len - 1)); + + for (int i = len - 2; i >= 0; i--) { + FatDirectoryEntry entry = dir.dir.getEntry(i + offset); + name.append(entry.getLfnPart()); + } + + fileName = name.toString().trim(); + } + + return new FatLfnDirectoryEntry(dir, realEntry, fileName); + } + + /** + * Returns if this directory entry has the FAT "hidden" flag set. + * + * @return if this is a hidden directory entry + * @see #setHiddenFlag(boolean) + */ + public boolean isHiddenFlag() { + return this.realEntry.isHiddenFlag(); + } + + /** + * Sets the "hidden" flag on this {@code FatLfnDirectoryEntry} to the + * specified value. + * + * @param hidden if this entry should have the hidden flag set + * @throws ReadOnlyException if this entry is read-only + * @see #isHiddenFlag() + */ + public void setHiddenFlag(boolean hidden) throws ReadOnlyException { + checkWritable(); + + this.realEntry.setHiddenFlag(hidden); + } + + /** + * Returns if this directory entry has the FAT "system" flag set. + * + * @return if this is a "system" directory entry + * @see #setSystemFlag(boolean) + */ + public boolean isSystemFlag() { + return this.realEntry.isSystemFlag(); + } + + /** + * Sets the "system" flag on this {@code FatLfnDirectoryEntry} to the + * specified value. + * + * @param systemEntry if this entry should have the system flag set + * @throws ReadOnlyException if this entry is read-only + * @see #isSystemFlag() + */ + public void setSystemFlag(boolean systemEntry) throws ReadOnlyException { + checkWritable(); + + this.realEntry.setSystemFlag(systemEntry); + } + + /** + * Returns if this directory entry has the FAT "read-only" flag set. This + * entry may still modified if {@link #isReadOnly()} returns {@code true}. + * + * @return if this entry has the read-only flag set + * @see #setReadOnlyFlag(boolean) + */ + public boolean isReadOnlyFlag() { + return this.realEntry.isReadonlyFlag(); + } + + /** + * Sets the "read only" flag on this {@code FatLfnDirectoryEntry} to the + * specified value. This method only modifies the read-only flag as + * specified by the FAT file system, which is essentially ignored by the + * fat32-lib. The true indicator if it is possible to alter this + * + * @param readOnly if this entry should be flagged as read only + * @throws ReadOnlyException if this entry is read-only as given by + * {@link #isReadOnly()} method + * @see #isReadOnlyFlag() + */ + public void setReadOnlyFlag(boolean readOnly) throws ReadOnlyException { + checkWritable(); + + this.realEntry.setReadonlyFlag(readOnly); + } + + /** + * Returns if this directory entry has the FAT "archive" flag set. + * + * @return if this entry has the archive flag set + */ + public boolean isArchiveFlag() { + return this.realEntry.isArchiveFlag(); + } + + /** + * Sets the "archive" flag on this {@code FatLfnDirectoryEntry} to the + * specified value. + * + * @param archive if this entry should have the archive flag set + * @throws ReadOnlyException if this entry is + * {@link #isReadOnly() read-only} + */ + public void setArchiveFlag(boolean archive) throws ReadOnlyException { + checkWritable(); + + this.realEntry.setArchiveFlag(archive); + } + + private int totalEntrySize() { + int result = (fileName.length() / 13) + 1; + + if ((fileName.length() % 13) != 0) { + result++; + } + + return result; + } + + FatDirectoryEntry[] compactForm() { + if (this.realEntry.getShortName().equals(ShortName.DOT) || + this.realEntry.getShortName().equals(ShortName.DOT_DOT) || + this.realEntry.hasShortNameOnly) { + /* the dot entries must not have a LFN */ + return new FatDirectoryEntry[]{this.realEntry}; + } + + final int totalEntrySize = totalEntrySize(); + + final FatDirectoryEntry[] entries = + new FatDirectoryEntry[totalEntrySize]; + + final byte checkSum = this.realEntry.getShortName().checkSum(); + int j = 0; + + for (int i = totalEntrySize - 2; i > 0; i--) { + entries[i] = createPart(fileName.substring(j * 13, j * 13 + 13), + j + 1, checkSum, false); + j++; + } + + entries[0] = createPart(fileName.substring(j * 13), + j + 1, checkSum, true); + + entries[totalEntrySize - 1] = this.realEntry; + + return entries; + } + + @Override + public String getName() { + checkValid(); + + return fileName; + } + + @Override + public void setName(String newName) throws IOException { + checkWritable(); + + if (!this.parent.isFreeName(newName)) { + throw new IOException( + "the name \"" + newName + "\" is already in use"); + } + + this.parent.unlinkEntry(this); + this.fileName = newName; + this.parent.linkEntry(this); + } + + /** + * Moves this entry to a new directory under the specified name. + * + * @param target the direcrory where this entry should be moved to + * @param newName the new name under which this entry will be accessible + * in the target directory + * @throws IOException on error moving this entry + * @throws ReadOnlyException if this directory is read-only + */ + public void moveTo(FatLfnDirectory target, String newName) + throws IOException, ReadOnlyException { + + checkWritable(); + + if (!target.isFreeName(newName)) { + throw new IOException( + "the name \"" + newName + "\" is already in use"); + } + + this.parent.unlinkEntry(this); + this.parent = target; + this.fileName = newName; + this.parent.linkEntry(this); + } + + @Override + public void setLastModified(long lastModified) { + checkWritable(); + realEntry.setLastModified(lastModified); + } + + @Override + public FatFile getFile() throws IOException { + return parent.getFile(realEntry); + } + + @Override + public FatLfnDirectory getDirectory() throws IOException { + return parent.getDirectory(realEntry); + } + + @Override + public String toString() { + return "LFN = " + fileName + " / SFN = " + realEntry.getShortName(); + } + + private static FatDirectoryEntry createPart(String subName, + int ordinal, byte checkSum, boolean isLast) { + + final char[] unicodechar = new char[13]; + subName.getChars(0, subName.length(), unicodechar, 0); + + for (int i=subName.length(); i < 13; i++) { + if (i==subName.length()) { + unicodechar[i] = 0x0000; + } else { + unicodechar[i] = 0xffff; + } + } + + final byte[] rawData = new byte[FatDirectoryEntry.SIZE]; + + if (isLast) { + LittleEndian.setInt8(rawData, 0, ordinal + (1 << 6)); + } else { + LittleEndian.setInt8(rawData, 0, ordinal); + } + + LittleEndian.setInt16(rawData, 1, unicodechar[0]); + LittleEndian.setInt16(rawData, 3, unicodechar[1]); + LittleEndian.setInt16(rawData, 5, unicodechar[2]); + LittleEndian.setInt16(rawData, 7, unicodechar[3]); + LittleEndian.setInt16(rawData, 9, unicodechar[4]); + LittleEndian.setInt8(rawData, 11, 0x0f); // this is the hidden + // attribute tag for + // lfn + LittleEndian.setInt8(rawData, 12, 0); // reserved + LittleEndian.setInt8(rawData, 13, checkSum); // checksum + LittleEndian.setInt16(rawData, 14, unicodechar[5]); + LittleEndian.setInt16(rawData, 16, unicodechar[6]); + LittleEndian.setInt16(rawData, 18, unicodechar[7]); + LittleEndian.setInt16(rawData, 20, unicodechar[8]); + LittleEndian.setInt16(rawData, 22, unicodechar[9]); + LittleEndian.setInt16(rawData, 24, unicodechar[10]); + LittleEndian.setInt16(rawData, 26, 0); // sector... unused + LittleEndian.setInt16(rawData, 28, unicodechar[11]); + LittleEndian.setInt16(rawData, 30, unicodechar[12]); + + return new FatDirectoryEntry(rawData, false); + } + + @Override + public long getLastModified() throws IOException { + return realEntry.getLastModified(); + } + + @Override + public long getCreated() throws IOException { + return realEntry.getCreated(); + } + + @Override + public long getLastAccessed() throws IOException { + return realEntry.getLastAccessed(); + } + + @Override + public boolean isFile() { + return realEntry.isFile(); + } + + @Override + public boolean isDirectory() { + return realEntry.isDirectory(); + } + + @Override + public boolean isDirty() { + return realEntry.isDirty(); + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/FatType.java b/src/main/java/de/waldheinz/fs/fat/FatType.java new file mode 100644 index 0000000..1978264 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/FatType.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +/** + * Enumerates the different entry sizes of 12, 16 and 32 bits for the different + * FAT flavours. + * + * @author Ewout Prangsma <epr at jnode.org> + * @author Matthias Treydte <waldheinz at gmail.com> + */ +public enum FatType { + + /** + * Represents a 12-bit file allocation table. + */ + FAT12((1 << 12) - 16, 0xFFFL, 1.5f, "FAT12 ") { //NOI18N + + @Override + long readEntry(byte[] data, int index) { + final int idx = (int) (index * 1.5); + final int b1 = data[idx] & 0xFF; + final int b2 = data[idx + 1] & 0xFF; + final int v = (b2 << 8) | b1; + + if ((index % 2) == 0) { + return v & 0xFFF; + } else { + return v >> 4; + } + } + + @Override + void writeEntry(byte[] data, int index, long entry) { + final int idx = (int) (index * 1.5); + + if ((index % 2) == 0) { + data[idx] = (byte) (entry & 0xFF); + data[idx + 1] = (byte) ((entry >> 8) & 0x0F); + } else { + data[idx] |= (byte) ((entry & 0x0F) << 4); + data[idx + 1] = (byte) ((entry >> 4) & 0xFF); + } + } + }, + + /** + * Represents a 16-bit file allocation table. + */ + FAT16((1 << 16) - 16, 0xFFFFL, 2.0f, "FAT16 ") { //NOI18N + + @Override + long readEntry(byte[] data, int index) { + final int idx = index << 1; + final int b1 = data[idx] & 0xFF; + final int b2 = data[idx + 1] & 0xFF; + return (b2 << 8) | b1; + } + + @Override + void writeEntry(byte[] data, int index, long entry) { + final int idx = index << 1; + data[idx] = (byte) (entry & 0xFF); + data[idx + 1] = (byte) ((entry >> 8) & 0xFF); + } + }, + + /** + * Represents a 32-bit file allocation table. + */ + FAT32((1 << 28) - 16, 0xFFFFFFFFL, 4.0f, "FAT32 ") { //NOI18N + + @Override + long readEntry(byte[] data, int index) { + final int idx = index * 4; + final long l1 = data[idx] & 0xFF; + final long l2 = data[idx + 1] & 0xFF; + final long l3 = data[idx + 2] & 0xFF; + final long l4 = data[idx + 3] & 0xFF; + return (l4 << 24) | (l3 << 16) | (l2 << 8) | l1; + } + + @Override + void writeEntry(byte[] data, int index, long entry) { + final int idx = index << 2; + data[idx] = (byte) (entry & 0xFF); + data[idx + 1] = (byte) ((entry >> 8) & 0xFF); + data[idx + 2] = (byte) ((entry >> 16) & 0xFF); + data[idx + 3] = (byte) ((entry >> 24) & 0xFF); + } + }; + + private final long minReservedEntry; + private final long maxReservedEntry; + private final long eofCluster; + private final long eofMarker; + private final long bitMask; + private final int maxClusters; + private final String label; + private final float entrySize; + + private FatType(int maxClusters, + long bitMask, float entrySize, String label) { + + this.minReservedEntry = (0xFFFFFF0L & bitMask); + this.maxReservedEntry = (0xFFFFFF6L & bitMask); + this.eofCluster = (0xFFFFFF8L & bitMask); + this.eofMarker = (0xFFFFFFFL & bitMask); + this.entrySize = entrySize; + this.label = label; + this.maxClusters = maxClusters; + this.bitMask = bitMask; + } + + abstract long readEntry(byte[] data, int index); + + abstract void writeEntry(byte[] data, int index, long entry); + + /** + * Returns the maximum number of clusters this file system can address. + * + * @return the maximum cluster count supported + */ + long maxClusters() { + return this.maxClusters; + } + + /** + * Returns the human-readable FAT name string as written to the + * {@link com.meetwise.fs.BootSector}. + * + * @return the boot sector label for this FAT type + */ + String getLabel() { + return this.label; + } + + boolean isReservedCluster(long entry) { + return ((entry >= minReservedEntry) && (entry <= maxReservedEntry)); + } + + boolean isEofCluster(long entry) { + return (entry >= eofCluster); + } + + long getEofMarker() { + return eofMarker; + } + + float getEntrySize() { + return entrySize; + } + + long getBitMask() { + return bitMask; + } +} diff --git a/src/main/java/de/waldheinz/fs/fat/FatUtils.java b/src/main/java/de/waldheinz/fs/fat/FatUtils.java new file mode 100644 index 0000000..b653f7c --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/FatUtils.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import java.io.IOException; + +/** + * <description> + * + * @author Ewout Prangsma < epr at jnode.org> + * @author Fabien DUMINY + * @author Matthias Treydte <waldheinz at gmail.com> + */ +public class FatUtils { + + /** + * Gets the offset (in bytes) of the fat with the given index + * + * @param bs + * @param fatNr (0..) + * @return long + * @throws IOException + */ + public static long getFatOffset(BootSector bs, int fatNr) { + long sectSize = bs.getBytesPerSector(); + long sectsPerFat = bs.getSectorsPerFat(); + long resSects = bs.getNrReservedSectors(); + + long offset = resSects * sectSize; + long fatSize = sectsPerFat * sectSize; + + offset += fatNr * fatSize; + + return offset; + } + + /** + * Gets the offset (in bytes) of the root directory with the given index + * + * @param bs + * @return long + * @throws IOException + */ + public static long getRootDirOffset(BootSector bs) { + long sectSize = bs.getBytesPerSector(); + long sectsPerFat = bs.getSectorsPerFat(); + int fats = bs.getNrFats(); + + long offset = getFatOffset(bs, 0); + + offset += fats * sectsPerFat * sectSize; + + return offset; + } + + /** + * Gets the offset of the data (file) area + * + * @param bs + * @return long + * @throws IOException + */ + public static long getFilesOffset(BootSector bs) { + long offset = getRootDirOffset(bs); + + offset += bs.getRootDirEntryCount() * 32; + + return offset; + } + + private FatUtils() { /* no instances */ } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/FsInfoSector.java b/src/main/java/de/waldheinz/fs/fat/FsInfoSector.java new file mode 100644 index 0000000..0399a9f --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/FsInfoSector.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import de.waldheinz.fs.BlockDevice; +import java.io.IOException; + +/** + * The FAT32 File System Information Sector. + * + * @author Matthias Treydte <waldheinz at gmail.com> + * @see http://en.wikipedia.org/wiki/File_Allocation_Table#FS_Information_Sector + */ +final class FsInfoSector extends Sector { + + /** + * The offset to the free cluster count value in the FS info sector. + */ + public static final int FREE_CLUSTERS_OFFSET = 0x1e8; + + /** + * The offset to the "last allocated cluster" value in this sector. + */ + public static final int LAST_ALLOCATED_OFFSET = 0x1ec; + + /** + * The offset to the signature of this sector. + */ + public static final int SIGNATURE_OFFSET = 0x1fe; + + private FsInfoSector(BlockDevice device, long offset) { + super(device, offset, BootSector.SIZE); + } + + /** + * Reads a {@code FsInfoSector} as specified by the given + * {@code Fat32BootSector}. + * + * @param bs the boot sector that specifies where the FS info sector is + * stored + * @return the FS info sector that was read + * @throws IOException on read error + * @see Fat32BootSector#getFsInfoSectorNr() + */ + public static FsInfoSector read(Fat32BootSector bs) throws IOException { + final FsInfoSector result = + new FsInfoSector(bs.getDevice(), offset(bs)); + + result.read(); + result.verify(); + return result; + } + + /** + * Creates an new {@code FsInfoSector} where the specified + * {@code Fat32BootSector} indicates it should be. + * + * @param bs the boot sector specifying the FS info sector storage + * @return the FS info sector instance that was created + * @throws IOException on write error + * @see Fat32BootSector#getFsInfoSectorNr() + */ + public static FsInfoSector create(Fat32BootSector bs) throws IOException { + final int offset = offset(bs); + + if (offset == 0) throw new IOException( + "creating a FS info sector at offset 0 is strange"); + + final FsInfoSector result = + new FsInfoSector(bs.getDevice(), offset(bs)); + + result.init(); + result.write(); + return result; + } + + private static int offset(Fat32BootSector bs) { + return bs.getFsInfoSectorNr() * bs.getBytesPerSector(); + } + + /** + * Sets the number of free clusters on the file system stored at + * {@link #FREE_CLUSTERS_OFFSET}. + * + * @param value the new free cluster count + * @see Fat#getFreeClusterCount() + */ + public void setFreeClusterCount(long value) { + if (getFreeClusterCount() == value) return; + + set32(FREE_CLUSTERS_OFFSET, value); + } + + /** + * Returns the number of free clusters on the file system as sepcified by + * the 32-bit value at {@link #FREE_CLUSTERS_OFFSET}. + * + * @return the number of free clusters + * @see Fat#getFreeClusterCount() + */ + public long getFreeClusterCount() { + return get32(FREE_CLUSTERS_OFFSET); + } + + /** + * Sets the last allocated cluster that was used in the {@link Fat}. + * + * @param value the FAT's last allocated cluster number + * @see Fat#getLastAllocatedCluster() + */ + public void setLastAllocatedCluster(long value) { + if (getLastAllocatedCluster() == value) return; + + super.set32(LAST_ALLOCATED_OFFSET, value); + } + + /** + * Returns the last allocated cluster number of the {@link Fat} of the + * file system this FS info sector is part of. + * + * @return the last allocated cluster number + * @see Fat#getLastAllocatedCluster() + */ + public long getLastAllocatedCluster() { + return super.get32(LAST_ALLOCATED_OFFSET); + } + + private void init() { + buffer.position(0x00); + buffer.put((byte) 0x52); + buffer.put((byte) 0x52); + buffer.put((byte) 0x61); + buffer.put((byte) 0x41); + + /* 480 reserved bytes */ + + buffer.position(0x1e4); + buffer.put((byte) 0x72); + buffer.put((byte) 0x72); + buffer.put((byte) 0x41); + buffer.put((byte) 0x61); + + setFreeClusterCount(-1); + setLastAllocatedCluster(Fat.FIRST_CLUSTER); + + buffer.position(SIGNATURE_OFFSET); + buffer.put((byte) 0x55); + buffer.put((byte) 0xaa); + + markDirty(); + } + + private void verify() throws IOException { + if (!(get8(SIGNATURE_OFFSET) == 0x55) || + !(get8(SIGNATURE_OFFSET + 1) == 0xaa)) { + + throw new IOException("invalid FS info sector signature"); + } + } + + @Override + public String toString() { + return FsInfoSector.class.getSimpleName() + + " [freeClusterCount=" + getFreeClusterCount() + //NOI18N + ", lastAllocatedCluster=" + getLastAllocatedCluster() + //NOI18N + ", offset=" + getOffset() + //NOI18N + ", dirty=" + isDirty() + //NOI18N + "]"; //NOI18N + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/LittleEndian.java b/src/main/java/de/waldheinz/fs/fat/LittleEndian.java new file mode 100644 index 0000000..a89f7ae --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/LittleEndian.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2003-2009 JNode.org + * 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + + +/** + * Little endian (LSB first) conversion methods. + * + * @author Ewout Prangsma <epr at users.sourceforge.net> + */ +final class LittleEndian { + + private LittleEndian() { /* no instances */ } + + /** + * Gets an 8-bit unsigned integer from the given byte array at + * the given offset. + * + * @param src the byte offset where to read the value from + * @param offset the byte array to extract the value from + * @return the integer that was read + */ + public static int getUInt8(byte[] src, int offset) { + return src[offset] & 0xFF; + } + + /** + * Gets a 16-bit unsigned integer from the given byte array at the given offset. + * + * @param src + * @param offset + */ + public static int getUInt16(byte[] src, int offset) { + final int v0 = src[offset + 0] & 0xFF; + final int v1 = src[offset + 1] & 0xFF; + return ((v1 << 8) | v0); + } + + /** + * Gets a 32-bit unsigned integer from the given byte array at the given offset. + * + * @param src + * @param offset + */ + public static long getUInt32(byte[] src, int offset) { + final long v0 = src[offset + 0] & 0xFF; + final long v1 = src[offset + 1] & 0xFF; + final long v2 = src[offset + 2] & 0xFF; + final long v3 = src[offset + 3] & 0xFF; + return ((v3 << 24) | (v2 << 16) | (v1 << 8) | v0); + } + + /** + * Sets an 8-bit integer in the given byte array at the given offset. + */ + public static void setInt8(byte[] dst, int offset, int value) { + dst[offset] = (byte) value; + } + + /** + * Sets a 16-bit integer in the given byte array at the given offset. + */ + public static void setInt16(byte[] dst, int offset, int value) { + dst[offset + 0] = (byte) (value & 0xFF); + dst[offset + 1] = (byte) ((value >>> 8) & 0xFF); + } + + /** + * Sets a 32-bit integer in the given byte array at the given offset. + */ + public static void setInt32(byte[] dst, int offset, long value) + throws IllegalArgumentException { + + if (value > Integer.MAX_VALUE) { + throw new IllegalArgumentException( + value + " can not be represented in a 32bit dword"); + } + + dst[offset + 0] = (byte) (value & 0xFF); + dst[offset + 1] = (byte) ((value >>> 8) & 0xFF); + dst[offset + 2] = (byte) ((value >>> 16) & 0xFF); + dst[offset + 3] = (byte) ((value >>> 24) & 0xFF); + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/Sector.java b/src/main/java/de/waldheinz/fs/fat/Sector.java new file mode 100644 index 0000000..670f149 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/Sector.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import de.waldheinz.fs.BlockDevice; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * + * @author Matthias Treydte <waldheinz at gmail.com> + */ +class Sector { + private final BlockDevice device; + private final long offset; + + /** + * The buffer holding the contents of this {@code Sector}. + */ + protected final ByteBuffer buffer; + + private boolean dirty; + + protected Sector(BlockDevice device, long offset, int size) { + this.offset = offset; + this.device = device; + this.buffer = ByteBuffer.allocate(size); + this.buffer.order(ByteOrder.LITTLE_ENDIAN); + this.dirty = true; + } + + /** + * Reads the contents of this {@code Sector} from the device into the + * internal buffer and resets the "dirty" state. + * + * @throws IOException on read error + * @see #isDirty() + */ + protected void read() throws IOException { + buffer.rewind(); + buffer.limit(buffer.capacity()); + device.read(offset, buffer); + this.dirty = false; + } + + public final boolean isDirty() { + return this.dirty; + } + + protected final void markDirty() { + this.dirty = true; + } + + /** + * Returns the {@code BlockDevice} where this {@code Sector} is stored. + * + * @return this {@code Sector}'s device + */ + public BlockDevice getDevice() { + return this.device; + } + + public final void write() throws IOException { + if (!isDirty()) return; + + buffer.position(0); + buffer.limit(buffer.capacity()); + device.write(offset, buffer); + this.dirty = false; + } + + protected int get16(int offset) { + return buffer.getShort(offset) & 0xffff; + } + + protected long get32(int offset) { + return buffer.getInt(offset) & 0xffffffff; + } + + protected int get8(int offset) { + return buffer.get(offset) & 0xff; + } + + protected void set16(int offset, int value) { + buffer.putShort(offset, (short) (value & 0xffff)); + dirty = true; + } + + protected void set32(int offset, long value) { + buffer.putInt(offset, (int) (value & 0xffffffff)); + dirty = true; + } + + protected void set8(int offset, int value) { + if ((value & 0xff) != value) { + throw new IllegalArgumentException( + value + " too big to be stored in a single octet"); + } + + buffer.put(offset, (byte) (value & 0xff)); + dirty = true; + } + + /** + * Returns the device offset to this {@code Sector}. + * + * @return the {@code Sector}'s device offset + */ + protected long getOffset() { + return this.offset; + } +} diff --git a/src/main/java/de/waldheinz/fs/fat/ShortName.java b/src/main/java/de/waldheinz/fs/fat/ShortName.java new file mode 100644 index 0000000..0fb382a --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/ShortName.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import java.math.BigInteger; +import java.util.Arrays; + +/** + * Represents a "short" (8.3) file name as used by DOS. + * + * @author Matthias Treydte <waldheinz at gmail.com> + */ +final class ShortName { + + /** + * These are taken from the FAT specification. + */ + private final static byte[] ILLEGAL_CHARS = { + 0x22, 0x2A, 0x2B, 0x2C, 0x2E, 0x2F, 0x3A, 0x3B, + 0x3C, 0x3D, 0x3E, 0x3F, 0x5B, 0x5C, 0x5D, 0x7C + }; + + /** + * The name of the "current directory" (".") entry of a FAT directory. + */ + public final static ShortName DOT = new ShortName(".", ""); // NOI18N + + /** + * The name of the "parent directory" ("..") entry of a FAT directory. + */ + public final static ShortName DOT_DOT = new ShortName("..", ""); // NOI18N + + private final char[] name; + private boolean mShortNameOnly; + + private ShortName(String nameExt) { + if (nameExt.length() > 12) + throw new IllegalArgumentException("name too long"); + + final int i = nameExt.indexOf('.'); + final String nameString, extString; + + if (i < 0) { + nameString = nameExt.toUpperCase(); + extString = ""; + } else { + nameString = nameExt.substring(0, i).toUpperCase(); + extString = nameExt.substring(i + 1).toUpperCase(); + } + + this.name = toCharArray(nameString, extString); + checkValidChars(this.name); + } + + ShortName(String name, String ext) { + this.name = toCharArray(name, ext); + } + + ShortName(char[] name) { + this.name = name; + } + + public ShortName(char[] nameArr, char[] extArr) { + char[] result = new char[11]; + System.arraycopy(nameArr, 0, result, 0, nameArr.length); + System.arraycopy(extArr, 0, result, 8, extArr.length); + this.name = result; + } + + private static char[] toCharArray(String name, String ext) { + checkValidName(name); + checkValidExt(ext); + + final char[] result = new char[11]; + Arrays.fill(result, ' '); + System.arraycopy(name.toCharArray(), 0, result, 0, name.length()); + System.arraycopy(ext.toCharArray(), 0, result, 8, ext.length()); + + return result; + } + + /** + * Calculates the checksum that is used to test a long file name for it's + * validity. + * + * @return the {@code ShortName}'s checksum + */ + public byte checkSum() { + final byte[] dest = new byte[11]; + for (int i = 0; i < 11; i++) + dest[i] = (byte) name[i]; + + int sum = dest[0]; + for (int i = 1; i < 11; i++) { + sum = dest[i] + (((sum & 1) << 7) + ((sum & 0xfe) >> 1)); + } + + return (byte) (sum & 0xff); + } + + /** + * Parses the specified string into a {@code ShortName}. + * + * @param name the name+extension of the {@code ShortName} to get + * @return the {@code ShortName} representing the specified name + * @throws IllegalArgumentException if the specified name can not be parsed + * into a {@code ShortName} + * @see #canConvert(java.lang.String) + */ + public static ShortName get(String name) throws IllegalArgumentException { + if (name.equals(".")) + return DOT; + else if (name.equals("..")) + return DOT_DOT; + else + return new ShortName(name); + } + + /** + * Tests if the specified string can be converted to a {@code ShortName}. + * + * @param nameExt the string to test + * @return if the string can be converted + * @see #get(java.lang.String) + */ + public static boolean canConvert(String nameExt) { + /* TODO: do this without exceptions */ + try { + ShortName.get(nameExt); + return true; + } catch (IllegalArgumentException ex) { + return false; + } + } + + public static ShortName parse(byte[] data) { + final char[] nameArr = new char[8]; + + for (int i = 0; i < nameArr.length; i++) { + nameArr[i] = (char) LittleEndian.getUInt8(data, i); + } + + final char[] extArr = new char[3]; + for (int i = 0; i < extArr.length; i++) { + extArr[i] = (char) LittleEndian.getUInt8(data, 0x08 + i); + } + + return new ShortName(nameArr, extArr); + } + + public void write(byte[] dest) { + for (int i = 0; i < 11; i++) { + dest[i] = (byte) name[i]; + } + } + + public String asSimpleString() { + return new String(this.name).trim(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < this.name.length; i++) { + sb.append(Integer.toHexString(name[i])); + sb.append(' '); + } + return getClass().getSimpleName() + + " [" + + asSimpleString() + " -- " + + sb.toString() + "]"; // NOI18N + } + + private static void checkValidName(String name) { + checkString(name, "name", 1, 8); + } + + private static void checkValidExt(String ext) { + checkString(ext, "extension", 0, 3); + } + + private static void checkString(String str, String strType, + int minLength, int maxLength) { + + if (str == null) + throw new IllegalArgumentException(strType + + " is null"); + if (str.length() < minLength) + throw new IllegalArgumentException(strType + + " must have at least " + minLength + + " characters: " + str); + if (str.length() > maxLength) + throw new IllegalArgumentException(strType + + " has more than " + maxLength + + " characters: " + str); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ShortName)) { + return false; + } + + final ShortName other = (ShortName) obj; + return Arrays.equals(name, other.name); + } + + @Override + public int hashCode() { + return Arrays.hashCode(this.name); + } + + public void setHasShortNameOnly(boolean hasShortNameOnly) { + mShortNameOnly = hasShortNameOnly; + } + + public boolean hasShortNameOnly() { + return mShortNameOnly; + } + + /** + * Checks if the specified char array consists only of "valid" byte values + * according to the FAT specification. + * + * @param chars the char array to test + * @throws IllegalArgumentException if invalid chars are contained + */ + public static void checkValidChars(char[] chars) + throws IllegalArgumentException { + + if (chars[0] == 0x20) + throw new IllegalArgumentException( + "0x20 can not be the first character"); + + for (int i = 0; i < chars.length; i++) { + if ((chars[i] & 0xff) != chars[i]) + throw new IllegalArgumentException("multi-byte character at " + i); + + final byte toTest = (byte) (chars[i] & 0xff); + + if (toTest < 0x20 && toTest != 0x05) + throw new IllegalArgumentException("character < 0x20 at" + i); + + for (int j = 0; j < ILLEGAL_CHARS.length; j++) { + if (toTest == ILLEGAL_CHARS[j]) + throw new IllegalArgumentException("illegal character " + + ILLEGAL_CHARS[j] + " at " + i); + } + } + } +} diff --git a/src/main/java/de/waldheinz/fs/fat/SuperFloppyFormatter.java b/src/main/java/de/waldheinz/fs/fat/SuperFloppyFormatter.java new file mode 100644 index 0000000..5a64639 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/SuperFloppyFormatter.java @@ -0,0 +1,468 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.fat; + +import de.waldheinz.fs.BlockDevice; +import java.io.IOException; +import java.util.Random; + +/** + * <p> + * Allows to create FAT file systems on {@link BlockDevice}s which follow the + * "super floppy" standard. This means that the device will be formatted so + * that it does not contain a partition table. Instead, the entire device holds + * a single FAT file system. + * </p><p> + * This class follows the "builder" pattern, which means it's methods always + * returns the {@code SuperFloppyFormatter} instance they're called on. This + * allows to chain the method calls like this: + * <pre> + * BlockDevice dev = new RamDisk(16700000); + * FatFileSystem fs = SuperFloppyFormatter.get(dev). + * setFatType(FatType.FAT12).format(); + * </pre> + * + * </p> + * + * @author Matthias Treydte <matthias.treydte at meetwise.com> + */ +public final class SuperFloppyFormatter { + + /** + * The media descriptor used (hard disk). + */ + public final static int MEDIUM_DESCRIPTOR_HD = 0xf8; + + /** + * The default number of FATs. + */ + public final static int DEFAULT_FAT_COUNT = 2; + + /** + * The default number of sectors per track. + */ + public final static int DEFAULT_SECTORS_PER_TRACK = 32; + + /** + * The default number of heads. + * + * @since 0.6 + */ + public final static int DEFAULT_HEADS = 64; + + /** + * The default number of heads. + * + * @deprecated the name of this constant was mistyped + * @see #DEFAULT_HEADS + */ + @Deprecated + public final static int DEFULT_HEADS = DEFAULT_HEADS; + + /** + * The default OEM name for file systems created by this class. + */ + public final static String DEFAULT_OEM_NAME = "fat32lib"; //NOI18N + + private static final int MAX_DIRECTORY = 512; + + private final BlockDevice device; + + private String label; + private String oemName; + private FatType fatType; + private int sectorsPerCluster; + private int reservedSectors; + private int fatCount; + + /** + * Creates a new {@code SuperFloppyFormatter} for the specified + * {@code BlockDevice}. + * + * @param device + * @throws IOException on error accessing the specified {@code device} + */ + private SuperFloppyFormatter(BlockDevice device) throws IOException { + this.device = device; + this.oemName = DEFAULT_OEM_NAME; + this.fatCount = DEFAULT_FAT_COUNT; + setFatType(fatTypeFromDevice()); + } + + /** + * Retruns a {@code SuperFloppyFormatter} instance suitable for formatting + * the specified device. + * + * @param dev the device that should be formatted + * @return the formatter for the device + * @throws IOException on error creating the formatter + */ + public static SuperFloppyFormatter get(BlockDevice dev) throws IOException { + return new SuperFloppyFormatter(dev); + } + + /** + * Returns the OEM name that will be written to the {@link BootSector}. + * + * @return the OEM name of the new file system + */ + public String getOemName() { + return oemName; + } + + /** + * Sets the OEM name of the boot sector. + * + * TODO: throw an exception early if name is invalid (too long, ...) + * + * @param oemName the new OEM name + * @return this {@code SuperFloppyFormatter} + * @see BootSector#setOemName(java.lang.String) + */ + public SuperFloppyFormatter setOemName(String oemName) { + this.oemName = oemName; + return this; + } + + /** + * Sets the volume label of the file system to create. + * + * TODO: throw an exception early if label is invalid (too long, ...) + * + * @param label the new file system label, may be {@code null} + * @return this {@code SuperFloppyFormatter} + * @see FatFileSystem#setVolumeLabel(java.lang.String) + */ + public SuperFloppyFormatter setVolumeLabel(String label) { + this.label = label; + return this; + } + + /** + * Returns the volume label that will be given to the new file system. + * + * @return the file system label, may be {@code null} + * @see FatFileSystem#getVolumeLabel() + */ + public String getVolumeLabel() { + return label; + } + + private void initBootSector(BootSector bs) + throws IOException { + + bs.init(); + bs.setFileSystemTypeLabel(fatType.getLabel()); + bs.setNrReservedSectors(reservedSectors); + bs.setNrFats(fatCount); + bs.setSectorsPerCluster(sectorsPerCluster); + bs.setMediumDescriptor(MEDIUM_DESCRIPTOR_HD); + bs.setSectorsPerTrack(DEFAULT_SECTORS_PER_TRACK); + bs.setNrHeads(DEFAULT_HEADS); + bs.setOemName(oemName); + } + + /** + * Initializes the boot sector and file system for the device. The file + * system created by this method will always be in read-write mode. + * + * @return the file system that was created + * @throws IOException on write error + */ + public FatFileSystem format() throws IOException { + final int sectorSize = device.getSectorSize(); + final int totalSectors = (int)(device.getSize() / sectorSize); + + final FsInfoSector fsi; + final BootSector bs; + if (sectorsPerCluster == 0) throw new AssertionError(); + + if (fatType == FatType.FAT32) { + bs = new Fat32BootSector(device); + initBootSector(bs); + + final Fat32BootSector f32bs = (Fat32BootSector) bs; + + f32bs.setFsInfoSectorNr(1); + + f32bs.setSectorsPerFat(sectorsPerFat(0, totalSectors)); + final Random rnd = new Random(System.currentTimeMillis()); + f32bs.setFileSystemId(rnd.nextInt()); + + f32bs.setVolumeLabel(label); + + /* create FS info sector */ + fsi = FsInfoSector.create(f32bs); + } else { + bs = new Fat16BootSector(device); + initBootSector(bs); + + final Fat16BootSector f16bs = (Fat16BootSector) bs; + + final int rootDirEntries = rootDirectorySize( + device.getSectorSize(), totalSectors); + + f16bs.setRootDirEntryCount(rootDirEntries); + f16bs.setSectorsPerFat(sectorsPerFat(rootDirEntries, totalSectors)); + if (label != null) f16bs.setVolumeLabel(label); + fsi = null; + } + + +// bs.write(); + + if (fatType == FatType.FAT32) { + Fat32BootSector f32bs = (Fat32BootSector) bs; + /* possibly writes the boot sector copy */ + f32bs.writeCopy(device); + } + + final Fat fat = Fat.create(bs, 0); + + final AbstractDirectory rootDirStore; + if (fatType == FatType.FAT32) { + rootDirStore = ClusterChainDirectory.createRoot(fat); + fsi.setFreeClusterCount(fat.getFreeClusterCount()); + fsi.setLastAllocatedCluster(fat.getLastAllocatedCluster()); + fsi.write(); + } else { + rootDirStore = Fat16RootDirectory.create((Fat16BootSector) bs); + } + + final FatLfnDirectory rootDir = + new FatLfnDirectory(rootDirStore, fat, false); + + rootDir.flush(); + + for (int i = 0; i < bs.getNrFats(); i++) { + fat.writeCopy(FatUtils.getFatOffset(bs, i)); + } + + bs.write(); + + FatFileSystem fs = FatFileSystem.read(device, false); + + if (label != null) { + fs.setVolumeLabel(label); + } + + fs.flush(); + return fs; + } + + private int sectorsPerFat(int rootDirEntries, int totalSectors) + throws IOException { + + final int bps = device.getSectorSize(); + final int rootDirSectors = + ((rootDirEntries * 32) + (bps - 1)) / bps; + final long tmp1 = + totalSectors - (this.reservedSectors + rootDirSectors); + int tmp2 = (256 * this.sectorsPerCluster) + this.fatCount; + + if (fatType == FatType.FAT32) + tmp2 /= 2; + + final int result = (int) ((tmp1 + (tmp2 - 1)) / tmp2); + + return result; + } + + /** + * Determines a usable FAT type from the {@link #device} by looking at the + * {@link BlockDevice#getSize() device size} only. + * + * @return the suggested FAT type + * @throws IOException on error determining the device's size + */ + private FatType fatTypeFromDevice() throws IOException { + return fatTypeFromSize(device.getSize()); + } + + /** + * Determines a usable FAT type from the {@link #device} by looking at the + * {@link BlockDevice#getSize() device size} only. + * + * @return the suggested FAT type + * @throws IOException on error determining the device's size + */ + public static FatType fatTypeFromSize(long sizeInBytes) { + final long sizeInMb = sizeInBytes / (1024 * 1024); + if (sizeInMb < 4) return FatType.FAT12; + else if (sizeInMb < 512) return FatType.FAT16; + else return FatType.FAT32; + } + + public static int clusterSizeFromSize(long sizeInBytes, int sectorSize){ + switch(fatTypeFromSize(sizeInBytes)) { + case FAT12: + return sectorsPerCluster12(sizeInBytes, sectorSize); + case FAT16: + return sectorsPerCluster16FromSize(sizeInBytes, sectorSize); + case FAT32: + return sectorsPerCluster32FromSize(sizeInBytes, sectorSize); + + default: + throw new AssertionError(); + } + } + + /** + * Returns the exact type of FAT the will be created by this formatter. + * + * @return the FAT type + */ + public FatType getFatType() { + return this.fatType; + } + + /** + * Sets the type of FAT that will be created by this + * {@code SuperFloppyFormatter}. + * + * @param fatType the desired {@code FatType} + * @return this {@code SuperFloppyFormatter} + * @throws IOException on error setting the {@code fatType} + * @throws IllegalArgumentException if {@code fatType} does not support the + * size of the device + */ + public SuperFloppyFormatter setFatType(FatType fatType) + throws IOException, IllegalArgumentException { + + if (fatType == null) throw new NullPointerException(); + + switch (fatType) { + case FAT12: case FAT16: + this.reservedSectors = 1; + break; + + case FAT32: + this.reservedSectors = 32; + } + + this.sectorsPerCluster = defaultSectorsPerCluster(fatType); + this.fatType = fatType; + + return this; + } + + private static int rootDirectorySize(int bps, int nbTotalSectors) { + final int totalSize = bps * nbTotalSectors; + if (totalSize >= MAX_DIRECTORY * 5 * 32) { + return MAX_DIRECTORY; + } else { + return totalSize / (5 * 32); + } + } + + static private int MAX_FAT32_CLUSTERS = 0x0FFFFFF5; + + static private int sectorsPerCluster32FromSize(long size, int sectorSize) { + final long sectors = size / sectorSize; + + if (sectors <= 66600) throw new IllegalArgumentException( + "disk too small for FAT32"); + + return + sectors > 67108864 ? 64 : + sectors > 33554432 ? 32 : + sectors > 16777216 ? 16 : + sectors > 532480 ? 8 : 1; + } + + private int sectorsPerCluster32() throws IOException { + if (this.reservedSectors != 32) throw new IllegalStateException( + "number of reserved sectors must be 32"); + + if (this.fatCount != 2) throw new IllegalStateException( + "number of FATs must be 2"); + + final long sectors = device.getSize() / device.getSectorSize(); + + if (sectors <= 66600) throw new IllegalArgumentException( + "disk too small for FAT32"); + + return sectorsPerCluster32FromSize(device.getSize(), device.getSectorSize()); + } + + static private int MAX_FAT16_CLUSTERS = 65524; + + static private int sectorsPerCluster16FromSize(long size, int sectorSize) { + final long sectors = size / sectorSize; + + if (sectors <= 8400) throw new IllegalArgumentException( + "disk too small for FAT16"); + + if (sectors > 4194304) throw new IllegalArgumentException( + "disk too large for FAT16"); + + return + sectors > 2097152 ? 64 : + sectors > 1048576 ? 32 : + sectors > 524288 ? 16 : + sectors > 262144 ? 8 : + sectors > 32680 ? 4 : 2; + } + + private int sectorsPerCluster16() throws IOException { + if (this.reservedSectors != 1) throw new IllegalStateException( + "number of reserved sectors must be 1"); + + if (this.fatCount != 2) throw new IllegalStateException( + "number of FATs must be 2"); + + long size = device.getSize(); + int sectorSize = device.getSectorSize(); + return sectorsPerCluster16FromSize(size, sectorSize); + } + + private int defaultSectorsPerCluster(FatType fatType) throws IOException { + long size = device.getSize(); + int sectorSize = device.getSectorSize(); + + switch (fatType) { + case FAT12: + return sectorsPerCluster12(size, sectorSize); + + case FAT16: + return sectorsPerCluster16(); + + case FAT32: + return sectorsPerCluster32(); + + default: + throw new AssertionError(); + } + } + + static private int sectorsPerCluster12(long size, int sectorSize) { + int result = 1; + + final long sectors = size / sectorSize; + + while (sectors / result > Fat16BootSector.MAX_FAT12_CLUSTERS) { + result *= 2; + if (result * size > 4096) throw new + IllegalArgumentException("disk too large for FAT12"); + } + + return result; + } + +} diff --git a/src/main/java/de/waldheinz/fs/fat/package-info.java b/src/main/java/de/waldheinz/fs/fat/package-info.java new file mode 100644 index 0000000..4baf68f --- /dev/null +++ b/src/main/java/de/waldheinz/fs/fat/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/** + * Contains the classes implementing the FAT(12/16/32) file system. + */ +package de.waldheinz.fs.fat; diff --git a/src/main/java/de/waldheinz/fs/package-info.java b/src/main/java/de/waldheinz/fs/package-info.java new file mode 100644 index 0000000..eb534b4 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/** + * Contains the file-system independent classes and interfaces, as well as + * the {@link de.waldheinz.fs.FileSystemFactory} for creating + * {@link de.waldheinz.fs.FileSystem} instances. + */ +package de.waldheinz.fs; diff --git a/src/main/java/de/waldheinz/fs/util/FileDisk.java b/src/main/java/de/waldheinz/fs/util/FileDisk.java new file mode 100644 index 0000000..0f3f0bb --- /dev/null +++ b/src/main/java/de/waldheinz/fs/util/FileDisk.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.util; + +import de.waldheinz.fs.BlockDevice; +import de.waldheinz.fs.ReadOnlyException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * This is a {@code BlockDevice} that uses a {@link File} as it's backing store. + * + * @author Matthias Treydte <matthias.treydte at meetwise.com> + */ +public final class FileDisk implements BlockDevice { + + /** + * The number of bytes per sector for all {@code FileDisk} instances. + */ + public final static int BYTES_PER_SECTOR = 512; + + private final RandomAccessFile raf; + private final FileChannel fc; + private final boolean readOnly; + private boolean closed; + + /** + * Creates a new instance of {@code FileDisk} for the specified + * {@code File}. + * + * @param file the file that holds the disk contents + * @param readOnly if the file should be opened in read-only mode, which + * will result in a read-only {@code FileDisk} instance + * @throws FileNotFoundException if the specified file does not exist + * @see #isReadOnly() + */ + public FileDisk(File file, boolean readOnly) throws FileNotFoundException { + if (!file.exists()) throw new FileNotFoundException(); + + this.readOnly = readOnly; + this.closed = false; + final String modeString = readOnly ? "r" : "rw"; //NOI18N + this.raf = new RandomAccessFile(file, modeString); + this.fc = raf.getChannel(); + } + + public FileDisk(RandomAccessFile raf, FileChannel fc, boolean readOnly) { + this.closed = false; + this.raf = raf; + this.fc = fc; + this.readOnly = readOnly; + } + + private FileDisk(RandomAccessFile raf, boolean readOnly) { + this.closed = false; + this.raf = raf; + this.fc = raf.getChannel(); + this.readOnly = readOnly; + } + + /** + * Creates a new {@code FileDisk} of the specified size. The + * {@code FileDisk} returned by this method will be writable. + * + * @param file the file to hold the {@code FileDisk} contents + * @param size the size of the new {@code FileDisk} + * @return the created {@code FileDisk} instance + * @throws IOException on error creating the {@code FileDisk} + */ + public static FileDisk create(File file, long size) throws IOException { + try { + final RandomAccessFile raf = + new RandomAccessFile(file, "rw"); //NOI18N + raf.setLength(size); + + return new FileDisk(raf, false); + } catch (FileNotFoundException ex) { + throw new IOException(ex); + } + } + + @Override + public long getSize() throws IOException { + checkClosed(); + + return raf.length(); + } + + @Override + public void read(long devOffset, ByteBuffer dest) throws IOException { + checkClosed(); + + int toRead = dest.remaining(); + if ((devOffset + toRead) > getSize()) throw new IOException( + "reading past end of device"); + + while (toRead > 0) { + final int read = fc.read(dest, devOffset); + if (read < 0) throw new IOException(); + toRead -= read; + devOffset += read; + } + } + + @Override + public void write(long devOffset, ByteBuffer src) throws IOException { + checkClosed(); + + if (this.readOnly) throw new ReadOnlyException(); + + int toWrite = src.remaining(); + + if ((devOffset + toWrite) > getSize()) throw new IOException( + "writing past end of file"); + + while (toWrite > 0) { + final int written = fc.write(src, devOffset); + if (written < 0) throw new IOException(); + toWrite -= written; + devOffset += written; + } + } + + @Override + public void flush() throws IOException { + checkClosed(); + } + + @Override + public int getSectorSize() { + checkClosed(); + + return BYTES_PER_SECTOR; + } + + @Override + public void close() throws IOException { + if (isClosed()) return; + + this.closed = true; + this.fc.close(); + this.raf.close(); + } + + @Override + public boolean isClosed() { + return this.closed; + } + + private void checkClosed() { + if (closed) throw new IllegalStateException("device already closed"); + } + + @Override + public boolean isReadOnly() { + checkClosed(); + + return this.readOnly; + } + +} diff --git a/src/main/java/de/waldheinz/fs/util/RamDisk.java b/src/main/java/de/waldheinz/fs/util/RamDisk.java new file mode 100644 index 0000000..51a1be1 --- /dev/null +++ b/src/main/java/de/waldheinz/fs/util/RamDisk.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package de.waldheinz.fs.util; + +import de.waldheinz.fs.*; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.zip.GZIPInputStream; + +/** + * A {@link BlockDevice} that lives entirely in heap memory. This is basically + * a RAM disk. A {@code RamDisk} is always writable. + * + * @author Matthias Treydte <waldheinz at gmail.com> + */ +public final class RamDisk implements BlockDevice { + + /** + * The default sector size for {@code RamDisk}s. + */ + public final static int DEFAULT_SECTOR_SIZE = 512; + + private final int sectorSize; + private final ByteBuffer data; + private final int size; + private boolean closed; + + /** + * Reads a GZIP compressed disk image from the specified input stream and + * returns a {@code RamDisk} holding the decompressed image. + * + * @param in the stream to read the disk image from + * @return the decompressed {@code RamDisk} + * @throws IOException on read or decompression error + */ + public static RamDisk readGzipped(InputStream in) throws IOException { + final GZIPInputStream zis = new GZIPInputStream(in); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + final byte[] buffer = new byte[4096]; + + int read = zis.read(buffer); + int total = 0; + + while (read >= 0) { + total += read; + bos.write(buffer, 0, read); + read = zis.read(buffer); + } + + if (total < DEFAULT_SECTOR_SIZE) throw new IOException( + "read only " + total + " bytes"); //NOI18N + + final ByteBuffer bb = ByteBuffer.wrap(bos.toByteArray(), 0, total); + return new RamDisk(bb, DEFAULT_SECTOR_SIZE); + } + + private RamDisk(ByteBuffer buffer, int sectorSize) { + this.size = buffer.limit(); + this.sectorSize = sectorSize; + this.data = buffer; + this.closed = false; + } + + /** + * Creates a new instance of {@code RamDisk} of this specified + * size and using the {@link #DEFAULT_SECTOR_SIZE}. + * + * @param size the size of the new block device + */ + public RamDisk(int size) { + this(size, DEFAULT_SECTOR_SIZE); + } + + /** + * Creates a new instance of {@code RamDisk} of this specified + * size and sector size + * + * @param size the size of the new block device + * @param sectorSize the sector size of the new block device + */ + public RamDisk(int size, int sectorSize) { + if (sectorSize < 1) throw new IllegalArgumentException( + "invalid sector size"); //NOI18N + + this.sectorSize = sectorSize; + this.size = size; + this.data = ByteBuffer.allocate(size); + } + + @Override + public long getSize() { + checkClosed(); + return this.size; + } + + @Override + public void read(long devOffset, ByteBuffer dest) throws IOException { + checkClosed(); + + if (devOffset > getSize()){ + final StringBuilder sb = new StringBuilder(); + sb.append("read at ").append(devOffset); + sb.append(" is off size (").append(getSize()).append(")"); + + throw new IllegalArgumentException(sb.toString()); + } + + data.limit((int) (devOffset + dest.remaining())); + data.position((int) devOffset); + + dest.put(data); + } + + @Override + public void write(long devOffset, ByteBuffer src) throws IOException { + checkClosed(); + + if (devOffset + src.remaining() > getSize()) throw new + IllegalArgumentException( + "offset=" + devOffset + + ", length=" + src.remaining() + + ", size=" + getSize()); + + data.limit((int) (devOffset + src.remaining())); + data.position((int) devOffset); + + + data.put(src); + } + + /** + * Returns a slice of the {@code ByteBuffer} that is used by this + * {@code RamDisk} as it's backing store. The returned buffer will be + * live (reflecting any changes made through the + * {@link #write(long, java.nio.ByteBuffer) method}, but read-only. + * + * @return a buffer holding the contents of this {@code RamDisk} + */ + public ByteBuffer getBuffer() { + return this.data.asReadOnlyBuffer(); + } + + @Override + public void flush() throws IOException { + checkClosed(); + } + + @Override + public int getSectorSize() { + checkClosed(); + return this.sectorSize; + } + + @Override + public void close() throws IOException { + this.closed = true; + } + + @Override + public boolean isClosed() { + return this.closed; + } + + private void checkClosed() { + if (closed) throw new IllegalStateException("device already closed"); + } + + /** + * Returns always {@code false}, as a {@code RamDisk} is always writable. + * + * @return always {@code false} + */ + @Override + public boolean isReadOnly() { + checkClosed(); + + return false; + } + +} diff --git a/src/main/java/de/waldheinz/fs/util/package-info.java b/src/main/java/de/waldheinz/fs/util/package-info.java new file mode 100644 index 0000000..afde34e --- /dev/null +++ b/src/main/java/de/waldheinz/fs/util/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2009,2010 Matthias Treydte <mt@waldheinz.de> + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/** + * Contains some utility classes that are useful independent of the file system + * type. + */ +package de.waldheinz.fs.util; + |