/* * $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.exfat; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; /** * @author Matthias Treydte <waldheinz at gmail.com> */ public class DirectoryParser { private static final int ENTRY_SIZE = 32; private static final int ENAME_MAX_LEN = 15; private static final int VALID = 0x80; private static final int CONTINUED = 0x40; /** * If this bit is not set it means "critical", if it is set "benign". */ private static final int IMPORTANCE_MASK = 0x20; private static final int EOD = (0x00); private static final int BITMAP = (0x01 | VALID); private static final int UPCASE = (0x02 | VALID); private static final int LABEL = (0x03 | VALID); private static final int FILE = (0x05); private static final int FILE_INFO = (0x00 | CONTINUED); private static final int FILE_NAME = (0x01 | CONTINUED); private static final int FLAG_FRAGMENTED = 1; private static final int FLAG_CONTIGUOUS = 3; public static DirectoryParser create(Node node) throws IOException { return create(node, false); } public static DirectoryParser create(Node node, boolean showDeleted) throws IOException { assert (node.isDirectory()) : "not a directory"; //NOI18N final DirectoryParser result = new DirectoryParser(node, showDeleted); result.init(); return result; } private final ExFatSuperBlock sb; private final ByteBuffer chunk; private final Node node; private boolean showDeleted; private long cluster; private UpcaseTable upcase; private int index; private DirectoryParser(Node node, boolean showDeleted) { this.node = node; this.showDeleted = showDeleted; this.sb = node.getSuperBlock(); this.chunk = ByteBuffer.allocate(sb.getBytesPerCluster()); this.chunk.order(ByteOrder.LITTLE_ENDIAN); this.cluster = node.getStartCluster(); this.upcase = null; } public DirectoryParser setUpcase(UpcaseTable upcase) { if (this.upcase != null) { throw new IllegalStateException("already had an upcase table"); } this.upcase = upcase; return this; } private void init() throws IOException { this.sb.readCluster(chunk, cluster); chunk.rewind(); } private boolean advance() throws IOException { assert ((chunk.position() % ENTRY_SIZE) == 0) : "not on entry boundary"; //NOI18N if (chunk.remaining() == 0) { cluster = node.nextCluster(cluster); if (Cluster.invalid(cluster)) { return false; } this.chunk.rewind(); this.sb.readCluster(chunk, cluster); this.chunk.rewind(); } return true; } private void skip(int bytes) { chunk.position(chunk.position() + bytes); } public void parse(Visitor v) throws IOException { while (true) { final int entryType = DeviceAccess.getUint8(chunk); if (entryType == LABEL) { parseLabel(v); } else if (entryType == BITMAP) { parseBitmap(v); } else if (entryType == UPCASE) { parseUpcaseTable(v); } else if ((entryType & FILE) == FILE) { boolean deleted = (entryType & VALID) == 0; if (showDeleted || !deleted) { parseFile(v, deleted); } else { skip(ENTRY_SIZE - 1); } } else if (entryType == EOD) { return; } else { if ((entryType & VALID) != 0) { throw new IOException( "unknown entry type " + entryType); } else { skip(ENTRY_SIZE - 1); } } if (!advance()) { return; } index++; } } private void parseLabel(Visitor v) throws IOException { final int len = DeviceAccess.getUint8(chunk); if (len > ENAME_MAX_LEN) { throw new IOException(len + " is too long"); } final StringBuilder labelBuilder = new StringBuilder(len); for (int i = 0; i < len; i++) { labelBuilder.append(DeviceAccess.getChar(chunk)); } v.foundLabel(labelBuilder.toString()); skip((ENAME_MAX_LEN - len) * DeviceAccess.BYTES_PER_CHAR); } private void parseBitmap(Visitor v) throws IOException { skip(19); /* unknown content */ final long startCluster = DeviceAccess.getUint32(chunk); final long size = DeviceAccess.getUint64(chunk); v.foundBitmap(startCluster, size); } private void parseUpcaseTable(Visitor v) throws IOException { skip(3); /* unknown */ final long checksum = DeviceAccess.getUint32(chunk); assert (checksum >= 0); skip(12); /* unknown */ final long startCluster = DeviceAccess.getUint32(chunk); final long size = DeviceAccess.getUint64(chunk); v.foundUpcaseTable(this, startCluster, size, checksum); } private void parseFile(Visitor v, boolean deleted) throws IOException { int actualChecksum = startChecksum(); int conts = DeviceAccess.getUint8(chunk); if (conts < 2) { throw new IOException("too few continuations (" + conts + ")"); } final int referenceChecksum = DeviceAccess.getUint16(chunk); final int attrib = DeviceAccess.getUint16(chunk); skip(2); /* unknown */ final EntryTimes times = EntryTimes.read(chunk); skip(7); /* unknown */ advance(); actualChecksum = addChecksum(actualChecksum); if ((DeviceAccess.getUint8(chunk) & FILE_INFO) != FILE_INFO) { throw new IOException("expected file info"); } if (deleted) { // Keep the index consistent with the index when not recovering deleted files index++; } final int flag = DeviceAccess.getUint8(chunk); skip(1); /* unknown */ int nameLen = DeviceAccess.getUint8(chunk); final int nameHash = DeviceAccess.getUint16(chunk); skip(2); /* unknown */ final long realSize = DeviceAccess.getUint64(chunk); skip(4); /* unknown */ final long startCluster = DeviceAccess.getUint32(chunk); final long size = DeviceAccess.getUint64(chunk); if (realSize != size) { throw new IOException("real size does not equal size"); } conts--; /* read file name */ final StringBuilder nameBuilder = new StringBuilder(nameLen); while (conts-- > 0) { advance(); actualChecksum = addChecksum(actualChecksum); if ((DeviceAccess.getUint8(chunk) & FILE_NAME) != FILE_NAME) { throw new IOException("expected file name"); } if (deleted) { // Keep the index consistent with the index when not recovering deleted files index++; } skip(1); /* unknown */ final int toRead = Math.min(ENAME_MAX_LEN, nameLen); for (int i = 0; i < toRead; i++) { nameBuilder.append(DeviceAccess.getChar(chunk)); } nameLen -= toRead; assert (nameLen >= 0); if (nameLen == 0) { assert (conts == 0) : "conts remaining?!"; //NOI18N skip((ENAME_MAX_LEN - toRead) * DeviceAccess.BYTES_PER_CHAR); } } if (!deleted && referenceChecksum != actualChecksum) { throw new IOException("checksum mismatch"); } final String name = nameBuilder.toString(); if ((this.upcase != null) && (hashName(name) != nameHash)) { throw new IOException("name hash mismatch (" + Integer.toHexString(hashName(name)) + " != " + Integer.toHexString(nameHash) + ")"); } v.foundNode(Node.create(sb, startCluster, attrib, name, (flag == FLAG_CONTIGUOUS), realSize, times, deleted), index); } private int hashName(String name) throws IOException { int hash = 0; for (int i = 0; i < name.length(); i++) { final int c = this.upcase.toUpperCase(name.charAt(i)); hash = ((hash << 15) | (hash >> 1)) + (c & 0xff); hash &= 0xffff; hash = ((hash << 15) | (hash >> 1)) + (c >> 8); hash &= 0xffff; } return (hash & 0xffff); } private int startChecksum() { final int oldPos = chunk.position(); chunk.position(chunk.position() - 1); assert ((chunk.position() % ENTRY_SIZE) == 0); int result = 0; for (int i = 0; i < ENTRY_SIZE; i++) { final int b = DeviceAccess.getUint8(chunk); if ((i == 2) || (i == 3)) continue; result = ((result << 15) | (result >> 1)) + b; result &= 0xffff; } chunk.position(oldPos); return result; } private int addChecksum(int sum) { chunk.mark(); assert ((chunk.position() % ENTRY_SIZE) == 0); for (int i = 0; i < ENTRY_SIZE; i++) { sum = ((sum << 15) | (sum >> 1)) + DeviceAccess.getUint8(chunk); sum &= 0xffff; } chunk.reset(); return sum; } interface Visitor { public void foundLabel( String label) throws IOException; /** * @param startCluster * @param size bitmap size in bytes */ public void foundBitmap( long startCluster, long size) throws IOException; /** * @param checksum * @param startCluster * @param size table size in bytes */ public void foundUpcaseTable(DirectoryParser parser, long checksum, long startCluster, long size) throws IOException; public void foundNode(Node node, int index) throws IOException; } }