/*
* $Id$
*
* Copyright (C) 2003-2015 JNode.org
*
* 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 org.jnode.fs.ext2;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.NoSuchElementException;
import org.apache.log4j.Logger;
import org.jnode.fs.FSDirectoryId;
import org.jnode.fs.FSEntry;
import org.jnode.fs.FileSystemException;
import org.jnode.fs.spi.AbstractFSDirectory;
import org.jnode.fs.spi.AbstractFileSystem;
import org.jnode.fs.spi.FSEntryTable;
import org.jnode.fs.util.FSUtils;
import org.jnode.util.LittleEndian;
/**
* @author Andras Nagy
*/
public class Ext2Directory extends AbstractFSDirectory implements FSDirectoryId {
protected INode iNode;
protected Ext2Entry entry;
private final Logger log = Logger.getLogger(getClass());
/**
* @param entry the Ext2Entry representing this directory
*/
public Ext2Directory(Ext2Entry entry) throws IOException {
super((Ext2FileSystem) entry.getFileSystem());
this.iNode = entry.getINode();
Ext2FileSystem fs = (Ext2FileSystem) entry.getFileSystem();
this.entry = entry;
boolean readOnly;
if ((iNode.getFlags() & Ext2Constants.EXT2_INDEX_FL) != 0 ||
(iNode.getFlags() & Ext2Constants.EXT4_HUGE_FILE_FL) != 0 ||
(iNode.getFlags() & Ext2Constants.EXT4_INODE_EXTENTS_FLAG) != 0) {
readOnly = true; //force readonly
if ((iNode.getFlags() & Ext2Constants.EXT4_INODE_EXTENTS_FLAG) != 0)
log.debug("inode uses extents: " + entry);
if ((iNode.getFlags() & Ext2Constants.EXT4_HUGE_FILE_FL) != 0)
log.info("inode is for a huge-file: " + entry);
if ((iNode.getFlags() & Ext2Constants.EXT2_INDEX_FL) != 0)
log.info("inode uses index: " + entry);
} else {
readOnly = fs.isReadOnly();
}
setRights(true, !readOnly);
log.debug("directory size: " + iNode.getSize());
}
/**
* Method to create a new ext2 directory entry from the given name
*
* @param name
* @return @throws
* IOException
*/
public FSEntry createDirectoryEntry(String name) throws IOException {
if (!canWrite())
throw new IOException("Filesystem or directory is mounted read-only!");
//create a new iNode for the file
//TODO: access rights, file type, UID and GID should be passed through
// the FSDirectory interface
INode newINode;
Ext2DirectoryRecord dr;
Ext2Entry newEntry;
Ext2FileSystem fs = (Ext2FileSystem) getFileSystem();
try {
int rights =
0xFFFF & (Ext2Constants.EXT2_S_IRWXU | Ext2Constants.EXT2_S_IRWXG | Ext2Constants.EXT2_S_IRWXO);
newINode = fs.createINode((int) iNode.getGroup(), Ext2Constants.EXT2_S_IFDIR, rights, 0, 0);
dr = new Ext2DirectoryRecord(fs, newINode.getINodeNr(), Ext2Constants.EXT2_FT_DIR, name);
addDirectoryRecord(dr);
newINode.setLinksCount(newINode.getLinksCount() + 1);
newEntry = new Ext2Entry(newINode, dr.getFileOffset(), name, Ext2Constants.EXT2_FT_DIR, fs, this);
//add "."
Ext2Directory newDir = new Ext2Directory(newEntry);
Ext2DirectoryRecord drThis =
new Ext2DirectoryRecord(fs, newINode.getINodeNr(), Ext2Constants.EXT2_FT_DIR, ".");
newINode.setLinksCount(2);
newDir.addDirectoryRecord(drThis);
//add ".."
long parentINodeNr = ((Ext2Directory) entry.getDirectory()).getINode().getINodeNr();
Ext2DirectoryRecord drParent = new Ext2DirectoryRecord(fs, parentINodeNr, Ext2Constants.EXT2_FT_DIR, "..");
newDir.addDirectoryRecord(drParent);
//increase the reference count for the parent directory
INode parentINode = fs.getINode(parentINodeNr);
parentINode.setLinksCount(parentINode.getLinksCount() + 1);
//update the number of used directories in the block group
int group = (int) ((newINode.getINodeNr() - 1) / fs.getSuperblock().getINodesPerGroup());
fs.modifyUsedDirsCount(group, 1);
//update the new inode
newINode.update();
} catch (FileSystemException ex) {
final IOException ioe = new IOException();
ioe.initCause(ex);
throw ioe;
}
return newEntry;
}
/**
* Abstract method to create a new ext2 file entry from the given name
*
* @param name
* @return @throws
* IOException
*/
public FSEntry createFileEntry(String name) throws IOException {
if (!canWrite())
throw new IOException("Filesystem or directory is mounted read-only!");
//create a new iNode for the file
//TODO: access rights, file type, UID and GID should be passed through
// the FSDirectory interface
INode newINode;
Ext2DirectoryRecord dr;
Ext2FileSystem fs = (Ext2FileSystem) getFileSystem();
try {
int rights =
0xFFFF & (Ext2Constants.EXT2_S_IRWXU | Ext2Constants.EXT2_S_IRWXG | Ext2Constants.EXT2_S_IRWXO);
newINode = fs.createINode((int) iNode.getGroup(), Ext2Constants.EXT2_S_IFREG, rights, 0, 0);
dr = new Ext2DirectoryRecord(fs, newINode.getINodeNr(), Ext2Constants.EXT2_FT_REG_FILE, name);
addDirectoryRecord(dr);
newINode.setLinksCount(newINode.getLinksCount() + 1);
} catch (FileSystemException ex) {
final IOException ioe = new IOException();
ioe.initCause(ex);
throw ioe;
}
return new Ext2Entry(newINode, dr.getFileOffset(), name, Ext2Constants.EXT2_FT_REG_FILE, fs, this);
}
/**
* Attach an inode to a directory (not used normally, only during fs
* creation)
*
* @param iNodeNr
* @return @throws
* IOException
*/
protected FSEntry addINode(long iNodeNr, String linkName, int fileType) throws IOException {
if (!canWrite())
throw new IOException("Filesystem or directory is mounted read-only!");
//TODO: access rights, file type, UID and GID should be passed through
// the FSDirectory interface
Ext2DirectoryRecord dr;
Ext2FileSystem fs = (Ext2FileSystem) getFileSystem();
try {
dr = new Ext2DirectoryRecord(fs, iNodeNr, fileType, linkName);
addDirectoryRecord(dr);
// update the directory inode
iNode.update();
INode linkedINode = fs.getINode(iNodeNr);
linkedINode.setLinksCount(linkedINode.getLinksCount() + 1);
return new Ext2Entry(linkedINode, dr.getFileOffset(), linkName, fileType, fs, this);
} catch (FileSystemException ex) {
final IOException ioe = new IOException();
ioe.initCause(ex);
throw ioe;
}
}
private void addDirectoryRecord(Ext2DirectoryRecord dr) throws IOException, FileSystemException {
//synchronize to the inode cache to make sure that the inode does not
// get
//flushed between reading it and locking it
synchronized (((Ext2FileSystem) getFileSystem()).getInodeCache()) {
//reread the inode before synchronizing to it to make sure
//all threads use the same instance
long iNodeNr = iNode.getINodeNr();
iNode = ((Ext2FileSystem) getFileSystem()).getINode(iNodeNr);
//lock the inode into the cache so it is not flushed before synchronizing to it
//(otherwise a new instance of INode referring to the same inode could be put
//in the cache resulting in the possibility of two threads manipulating the same
//inode at the same time because they would synchronize to different INode instances)
iNode.incLocked();
}
//a single inode may be represented by more than one Ext2Directory instances,
//but each will use the same instance of the underlying inode (see Ext2FileSystem.getINode()),
//so synchronize to the inode.
synchronized (iNode) {
try {
Ext2File dir = new Ext2File(entry); //read itself as a file
//find the last directory record (if any)
Ext2FSEntryIterator iterator = new Ext2FSEntryIterator(entry);
Ext2DirectoryRecord rec = null;
while (iterator.hasNext()) {
rec = iterator.nextDirectoryRecord();
}
Ext2FileSystem fs = (Ext2FileSystem) getFileSystem();
if (rec != null) {
long lastPos = rec.getFileOffset();
long lastLen = rec.getRecLen();
//truncate the last record to its minimal size (cut the padding from the end)
rec.truncateRecord();
//directoryRecords may not extend over block boundaries:
//see if the new record fits in the same block after truncating the last record
long remainingLength = fs.getBlockSize() - (lastPos % fs.getBlockSize()) - rec.getRecLen();
log.debug("LAST-1 record: begins at: " + lastPos + ", length: " + lastLen);
log.debug("LAST-1 truncated length: " + rec.getRecLen());
log.debug("Remaining length: " + remainingLength);
if (remainingLength >= dr.getRecLen()) {
//write back the last record truncated
//TODO optimize it also to use ByteBuffer at lower level
ByteBuffer buf = ByteBuffer.wrap(rec.getData(), rec.getOffset(), rec.getRecLen());
dir.write(lastPos, buf);
// dir.write(lastPos, rec.getData(), rec.getOffset(), rec
// .getRecLen());
//pad the end of the new record with zeroes
dr.expandRecord(lastPos + rec.getRecLen(), lastPos + rec.getRecLen() + remainingLength);
//append the new record at the end of the list
//TODO optimize it also to use ByteBuffer at lower level
buf = ByteBuffer.wrap(dr.getData(), dr.getOffset(), dr.getRecLen());
dir.write(lastPos + rec.getRecLen(), buf);
// dir.write(lastPos + rec.getRecLen(), dr.getData(), dr
// .getOffset(), dr.getRecLen());
log.debug("addDirectoryRecord(): LAST record: begins at: " +
(rec.getFileOffset() + rec.getRecLen()) + ", length: " + dr.getRecLen());
} else {
//the new record must go to the next block
//(the previously last record (rec) stays padded to the
// end of the block, so we can
// append after that)
dr.expandRecord(lastPos + lastLen, lastPos + lastLen + fs.getBlockSize());
//TODO optimize it also to use ByteBuffer at lower level
ByteBuffer buf = ByteBuffer.wrap(dr.getData(), dr.getOffset(), dr.getRecLen());
dir.write(lastPos + lastLen, buf);
// dir.write(lastPos + lastLen, dr.getData(), dr
// .getOffset(), dr.getRecLen());
log.debug("addDirectoryRecord(): LAST record: begins at: " + (lastPos + lastLen) +
", length: " + dr.getRecLen());
}
} else { //rec==null, ie. this is the first record in the directory
dr.expandRecord(0, fs.getBlockSize());
//TODO optimize it also to use ByteBuffer at lower level
ByteBuffer buf = ByteBuffer.wrap(dr.getData(), dr.getOffset(), dr.getRecLen());
dir.write(0, buf);
//dir.write(0, dr.getData(), dr.getOffset(), dr.getRecLen());
log.debug("addDirectoryRecord(): LAST record: begins at: 0, length: " + dr.getRecLen());
}
//dir.flush();
iNode.setMtime(System.currentTimeMillis() / 1000);
// update the directory inode
iNode.update();
return;
} catch (Throwable ex) {
final IOException ioe = new IOException();
ioe.initCause(ex);
throw ioe;
} finally {
//unlock the inode from the cache
iNode.decLocked();
}
}
}
/**
* Return the number of the block that contains the given byte
*/
int translateToBlock(long index) {
return (int) (index / iNode.getExt2FileSystem().getBlockSize());
}
/**
* Return the offset inside the block that contains the given byte
*/
int translateToOffset(long index) {
return (int) (index % iNode.getExt2FileSystem().getBlockSize());
}
/**
* Gets the inode for this directory.
*
* @return the inode.
*/
public INode getINode() {
return iNode;
}
@Override
public String getDirectoryId() {
return Long.toString(iNode.getINodeNr());
}
@Override
public FSEntry getEntryById(String id) throws IOException {
checkEntriesLoaded();
return getEntryTable().getById(id);
}
class Ext2FSEntryIterator implements Iterator<FSEntry> {
ByteBuffer data;
int index;
Ext2DirectoryRecord current;
public Ext2FSEntryIterator(Ext2Entry entry) throws IOException {
//read itself as a file
Ext2File directoryFile = new Ext2File(entry);
//read the whole directory
data = ByteBuffer.allocate((int) directoryFile.getLength());
directoryFile.read(0, data);
//data = new byte[(int) directoryFile.getLength()];
//directoryFile.read(0, data, 0, (int) directoryFile.getLength());
index = 0;
}
public boolean hasNext() {
Ext2DirectoryRecord dr = null;
Ext2FileSystem fs = (Ext2FileSystem) getFileSystem();
try {
do {
if (index >= iNode.getSize())
return false;
if (data.capacity() < 8 || LittleEndian.getUInt16(data.array(), index + 4) == 0) {
return false;
}
//TODO optimize it also to use ByteBuffer at lower level
dr = new Ext2DirectoryRecord(fs, data.array(), index, index);
index += dr.getRecLen();
} while (dr.getINodeNr() == 0); //inode nr=0 means the entry is unused
} catch (Exception e) {
fs.handleFSError(e);
return false;
}
current = dr;
return true;
}
public FSEntry next() {
if (current == null) {
//hasNext actually reads the next element
if (!hasNext())
throw new NoSuchElementException();
}
Ext2DirectoryRecord dr = current;
Ext2FileSystem fs = (Ext2FileSystem) getFileSystem();
current = null;
try {
return new Ext2Entry(((Ext2FileSystem) getFileSystem()).getINode(dr.getINodeNr()),
dr.getFileOffset(), dr.getName(), dr.getType(), fs, Ext2Directory.this);
} catch (IOException e) {
throw new NoSuchElementException("Root cause: " + e.getMessage());
} catch (FileSystemException e) {
throw new NoSuchElementException("Root cause: " + e.getMessage());
}
}
/**
* @see java.util.Iterator#remove()
*/
public void remove() {
throw new UnsupportedOperationException();
}
/**
* Returns the next record as an Ext2DirectoryRecord instance
*
* @return
*/
protected Ext2DirectoryRecord nextDirectoryRecord() {
if (current == null) {
//hasNext actually reads the next element
if (!hasNext())
throw new NoSuchElementException();
}
Ext2DirectoryRecord dr = current;
current = null;
return dr;
}
}
/**
* Read the entries from the device and return the result in a new
* FSEntryTable
*
* @return the FSEntryTable containing the directory's entries.
*/
protected FSEntryTable readEntries() throws IOException {
Ext2FSEntryIterator it = new Ext2FSEntryIterator(entry);
ArrayList<FSEntry> entries = new ArrayList<FSEntry>();
while (it.hasNext()) {
final FSEntry entry = it.next();
log.debug("readEntries: entry=" + FSUtils.toString(entry, false));
entries.add(entry);
}
FSEntryTable table = new FSEntryTable((AbstractFileSystem<?>) getFileSystem(), entries);
return table;
}
/**
* Write the entries in the table to the device.
*
* @param table
*/
protected void writeEntries(FSEntryTable table) throws IOException {
//nothing to do because createFileEntry and createDirectoryEntry do the
// job
}
@Override
public String toString() {
return String.format("directory-%d['%s' entries:%d]", iNode.getINodeNr(), entry.getName(),
getEntryTable().size());
}
}