/*- * Copyright (C) 2008-2014 Erik Larsson * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.catacombae.storage.fs.hfscommon; import java.io.UnsupportedEncodingException; import java.nio.CharBuffer; import java.util.LinkedList; import org.catacombae.hfs.UnicodeNormalizationToolkit; import org.catacombae.util.Util; import org.catacombae.hfs.types.hfscommon.CommonHFSCatalogFolderRecord; import org.catacombae.hfs.types.hfscommon.CommonHFSCatalogFileRecord; import org.catacombae.hfs.types.hfscommon.CommonHFSCatalogFolderThreadRecord; import org.catacombae.hfs.types.hfscommon.CommonHFSCatalogFileThreadRecord; import org.catacombae.hfs.types.hfscommon.CommonHFSCatalogFolderThread; import org.catacombae.hfs.types.hfscommon.CommonHFSCatalogLeafRecord; import org.catacombae.hfs.types.hfscommon.CommonHFSCatalogNodeID; import org.catacombae.io.ReadableRandomAccessStream; import org.catacombae.storage.fs.FSFolder; import org.catacombae.storage.fs.FSForkType; import org.catacombae.storage.fs.FSLink; import org.catacombae.storage.fs.FileSystemHandler; import org.catacombae.storage.fs.FSEntry; import org.catacombae.hfs.HFSVolume; import org.catacombae.storage.fs.FSFile; import org.catacombae.storage.fs.FileSystemCapability; /** * HFS+ implementation of a FileSystemHandler. This implementation can be used * to access HFS+ file systems. * * @author <a href="http://www.catacombae.org/" target="_top">Erik Larsson</a> */ public abstract class HFSCommonFileSystemHandler extends FileSystemHandler { private static final boolean DEBUG = Util.booleanEnabledByProperties(false, "org.catacombae.debug", "org.catacombae.storage.debug", "org.catacombae.storage.fs.debug", "org.catacombae.storage.fs.hfscommon.debug", "org.catacombae.storage.fs.hfscommon." + HFSCommonFileSystemHandler.class.getSimpleName() + ".debug"); protected final HFSVolume view; private boolean posixNames; private boolean doUnicodeFileNameComposition; protected boolean hideProtected; protected HFSCommonFileSystemHandler(HFSVolume iView, boolean posixNames, boolean iDoUnicodeFileNameComposition, boolean hideProtected) { this.view = iView; this.posixNames = posixNames; this.doUnicodeFileNameComposition = iDoUnicodeFileNameComposition; this.hideProtected = hideProtected; } public static FileSystemCapability[] getStaticCapabilities() { return new FileSystemCapability[] { FileSystemCapability.CREATE_TIME, FileSystemCapability.BACKUP_TIME, }; } public FileSystemCapability[] getCapabilities() { return getStaticCapabilities(); } @Override public FSEntry[] list(String... path) { CommonHFSCatalogFolderRecord curFolder = view.getCatalogFile().getRootFolder(); for(String nextFolderName : path) { CommonHFSCatalogLeafRecord subRecord = getRecord(curFolder, nextFolderName); if(subRecord != null && subRecord instanceof CommonHFSCatalogFolderRecord) curFolder = (CommonHFSCatalogFolderRecord) subRecord; else return null; // Invalid path, no matching child folder was found. } return listFSEntries(curFolder); } @Override public FSEntry getEntry(String... path) { return getEntry(view.getCatalogFile().getRootFolder(), path); } FSEntry getEntry(CommonHFSCatalogFolderRecord rootRecord, String... path) { CommonHFSCatalogLeafRecord rec = getRecord(rootRecord, path); if(rec == null) return null; else if(rec instanceof CommonHFSCatalogFileRecord) return entryFromRecord((CommonHFSCatalogFileRecord)rec); else if(rec instanceof CommonHFSCatalogFolderRecord) return entryFromRecord((CommonHFSCatalogFolderRecord)rec); else throw new RuntimeException("Did not excpect a " + rec.getClass() + " here!"); } protected abstract String[] getAbsoluteLinkPath(String[] path, int pathLength, CommonHFSCatalogFileRecord rec); /** * Searches the hierarchy rooted in <code>rootRecord</code> for the record addressed by * <code>path</code>. If any symbolic or hard links exist in the path to the requested entry, * they will be resolved, but the requested destination will be returned as it is. * * @param rootRecord (non-null) the root record from which we will begin searching. * @param path the path to our requested entry. May be empty, in which case * <code>rootRecord</code> is returned. * @return the requested entry, or <code>null</code> if it wasn't found. */ protected CommonHFSCatalogLeafRecord getRecord( final CommonHFSCatalogFolderRecord rootRecord, final String... path) { /* * Algorithm (variables are prefixed with $): * * $currentRoot = $root * for each path component $pc except the last one: * while currentRoot is a link: * $currentRoot = resolveLink(entry) * * if currentRoot is a directory: * $currentRoot = find($pc, $currentRoot) * else: * return null * * return currentRoot */ /* String prefix = globalPrefix; globalPrefix += " "; log(prefix + "getRecord(" + (rootRecord != null ? rootRecord.getKey().getParentID().toLong() + ":\"" + getProperNodeName(rootRecord) + "\"" : "null" ) + ", { " + (path != null && path.length > 0 ? "\"" + Util.concatenateStrings(path, "\", \"") + "\"" : path == null ? "null" : "" ) + " });"); try { */ if(rootRecord == null) throw new IllegalArgumentException("rootRecord == null"); if(path == null) throw new IllegalArgumentException("path == null"); LinkedList<String[]> visitedList = null; CommonHFSCatalogLeafRecord currentRoot = rootRecord; // We iterate over all records except the last one, which is our target. for(int i = 0; i < path.length; ++i) { String curPathComponent = getOnDiskName(path[i]); //log(prefix + " getRecord: Processing path element " + (i + 1) + "/" + // path.length + ": \"" + curPathComponent + "\""); LinkedList<String[]> curVisitedList = null; // Iterate through all links. while(currentRoot instanceof CommonHFSCatalogFileRecord) { final String[] absPath; absPath = getAbsoluteLinkPath(path, i, (CommonHFSCatalogFileRecord) currentRoot); if(absPath == null) { break; } // Reset visited list before usage if this is the first time if(curVisitedList == null) { if(visitedList == null) visitedList = new LinkedList<String[]>(); else visitedList.clear(); curVisitedList = visitedList; } if(absPath == null) throw new RuntimeException("'assertion' failed. absPath " + "shouldn't be null"); else if(Util.contains(curVisitedList, absPath)) { if(DEBUG) { System.err.println("WARNING: Detected cyclic link " + "structure when resolving link target."); System.err.println(" Resolve stack:"); for(String[] sa : curVisitedList) { System.err.println(" " + Util.concatenateStrings(sa, "/")); } System.err.println(" " + Util.concatenateStrings(absPath, "/")); } return null; // Circular linking. } else { curVisitedList.addLast(absPath); //log(prefix + " getRecord: Trying to get record for absolute link target..."); CommonHFSCatalogLeafRecord linkTarget = getRecord(view.getCatalogFile().getRootFolder(), absPath); //log(prefix + " getRecord: target record = " + linkTarget); if(linkTarget != null) { currentRoot = linkTarget; } } } CommonHFSCatalogFolderRecord currentRootFolder; if(currentRoot instanceof CommonHFSCatalogFolderRecord) currentRootFolder = (CommonHFSCatalogFolderRecord) currentRoot; else { //log(prefix + " getRecord: Returning with error - currentRoot not instanceof CommonHFSCatalogFolderRecord (" + currentRoot + ")"); return null; // We encountered a pathname component which wasn't a folder. } //log(prefix + " getting record (" + currentRootFolder.getData().getFolderID().toLong() + ":\"" + curPathComponent + "\")"); CommonHFSCatalogLeafRecord newRoot = view.getCatalogFile().getRecord(currentRootFolder.getData().getFolderID(), view.encodeString(curPathComponent)); if(newRoot != null) currentRoot = newRoot; else { //log(prefix + " getRecord: Returning with error - no match was found for \"" + curPathComponent + "\""); return null; // Invalid path, no matching child was found. } } //log(prefix + " getRecord: Returning successfully with " + currentRoot + " (" + currentRoot.getKey().getParentID().toLong() + ":\"" + getProperNodeName(currentRoot) + "\")"); return currentRoot; /* } finally { log(prefix + "Returning from getRecord."); globalPrefix = prefix; } */ } protected FSFile newFSFile(CommonHFSCatalogFileRecord fileRecord) { return new HFSCommonFSFile(this, fileRecord); } protected FSFile newFSFile(CommonHFSCatalogFileRecord hardLinkRecord, CommonHFSCatalogFileRecord fileRecord) { return new HFSCommonFSFile(this, hardLinkRecord, fileRecord); } protected FSEntry entryFromRecord(CommonHFSCatalogFileRecord fileRecord) { return newFSFile(fileRecord); } protected FSFile createFSFile(CommonHFSCatalogFileRecord fileRecord) { return newFSFile(fileRecord); } protected FSFile createFSFile(CommonHFSCatalogFileRecord hardLinkRecord, CommonHFSCatalogFileRecord fileRecord) { return newFSFile(hardLinkRecord, fileRecord); } protected FSFolder createFSFolder(CommonHFSCatalogFileRecord hardLinkRecord, CommonHFSCatalogFolderRecord folderRecord) { return new HFSCommonFSFolder(this, hardLinkRecord, folderRecord); } private FSEntry entryFromRecord(CommonHFSCatalogFolderRecord folderRecord) { return new HFSCommonFSFolder(this, folderRecord); } /* private FSEntry entriFromRecord(CommonHFSCatalogLeafRecord rec) { if(rec instanceof CommonHFSCatalogFileRecord) { return entryFromRecord((CommonHFSCatalogFileRecord)rec); } else if(rec instanceof CommonHFSCatalogFolderRecord) { return entryFromRecord((CommonHFSCatalogFolderRecord)rec); } else //throw new RuntimeException("Did not expect a " + rec.getClass() + // " here.") return null; } */ @Override public FSForkType[] getSupportedForkTypes() { return new FSForkType[] { FSForkType.DATA, FSForkType.MACOS_RESOURCE }; } private static char[] posixWrap(final char[] nodeNameChars) { for(int i = 0; i < nodeNameChars.length; ++i) { if(nodeNameChars[i] == '/') { nodeNameChars[i] = ':'; } else if(nodeNameChars[i] == ':') { nodeNameChars[i] = '/'; } } return nodeNameChars; } private static String posixWrap(final String nodeName) { return new String(posixWrap(nodeName.toCharArray())); } protected String getLogicalName(String onDiskName) { String logicalName = onDiskName; if(doUnicodeFileNameComposition) { logicalName = UnicodeNormalizationToolkit.getDefaultInstance(). compose(logicalName); } if(posixNames) { logicalName = posixWrap(logicalName); } return logicalName; } protected String getOnDiskName(String logicalName) { String onDiskName = logicalName; if(doUnicodeFileNameComposition) { onDiskName = UnicodeNormalizationToolkit.getDefaultInstance(). decompose(CharBuffer.wrap(onDiskName)); } if(posixNames) { onDiskName = posixWrap(onDiskName); } return onDiskName; } protected String getProperNodeName(CommonHFSCatalogLeafRecord record) { return getLogicalName(view.decodeString(record.getKey().getNodeName())); } /** * Converts a HFS+ POSIX UTF-8 pathname into pathname component strings. * * @param path the bytes that make up the HFS+ POSIX UTF-8 pathname string. * @return the pathname components of the HFS+ POSIX UTF-8 pathname. */ public String[] splitPOSIXUTF8Path(byte[] path) { return splitPOSIXUTF8Path(path, 0, path.length); } /** * Converts a HFS+ POSIX UTF-8 pathname into pathname component strings. * * @param path the bytes that make up the HFS+ POSIX UTF-8 pathname string. * @param offset offset to the beginning of string data in <code>path</code>. * @param length length of string data in <code>path</code>. * @return the pathname components of the HFS+ POSIX UTF-8 pathname. */ public String[] splitPOSIXUTF8Path(byte[] path, int offset, int length) { try { String s = new String(path, offset, length, "UTF-8"); String[] res = s.split("/"); if(!posixNames) { /* As per the MacOS <-> POSIX translation semantics, all POSIX * ':' characters are really '/' characters in the MacOS * world. */ for(int i = 0; i < res.length; ++i) { res[i] = posixWrap(res[i]); } } return res; } catch(UnsupportedEncodingException e) { throw new RuntimeException("REALLY UNEXPECTED: Could not decode UTF-8!", e); } } protected ReadableRandomAccessStream getReadableDataForkStream( CommonHFSCatalogFileRecord fileRecord) { return view.getReadableDataForkStream(fileRecord); } ReadableRandomAccessStream getReadableResourceForkStream(CommonHFSCatalogFileRecord fileRecord) { return view.getReadableResourceForkStream(fileRecord); } /* boolean isUnicodeCompositionEnabled() { return doUnicodeFileNameComposition; } * */ protected abstract boolean shouldHide(CommonHFSCatalogLeafRecord rec); String[] listNames(CommonHFSCatalogFolderRecord folderRecord) { CommonHFSCatalogLeafRecord[] subRecords = view.getCatalogFile().listRecords(folderRecord); LinkedList<String> result = new LinkedList<String>(); for(int i = 0; i < subRecords.length; ++i) { CommonHFSCatalogLeafRecord curRecord = subRecords[i]; if(!shouldHide(curRecord)) result.add(getProperNodeName(curRecord)); } return result.toArray(new String[result.size()]); } FSEntry[] listFSEntries(CommonHFSCatalogFolderRecord folderRecord) { CommonHFSCatalogLeafRecord[] subRecords = view.getCatalogFile().listRecords(folderRecord); LinkedList<FSEntry> result = new LinkedList<FSEntry>(); for(int i = 0; i < subRecords.length; ++i) { CommonHFSCatalogLeafRecord curRecord = subRecords[i]; FSEntry curEntry = null; if(shouldHide(curRecord)); else if(curRecord instanceof CommonHFSCatalogFileRecord) curEntry = entryFromRecord((CommonHFSCatalogFileRecord)curRecord); else if(curRecord instanceof CommonHFSCatalogFolderRecord) curEntry = entryFromRecord((CommonHFSCatalogFolderRecord)curRecord); if(curEntry != null) result.addLast(curEntry); } return result.toArray(new FSEntry[result.size()]); } HFSCommonFSFolder lookupParentFolder(CommonHFSCatalogLeafRecord childRecord) { CommonHFSCatalogFolderRecord folderRec = lookupParentFolderRecord(childRecord); if(folderRec != null) return new HFSCommonFSFolder(this, folderRec); else return null; } private CommonHFSCatalogFolderRecord lookupParentFolderRecord( CommonHFSCatalogLeafRecord childRecord) { CommonHFSCatalogNodeID parentID = childRecord.getKey().getParentID(); // Look for the thread record associated with the parent dir CommonHFSCatalogLeafRecord parent = view.getCatalogFile().getRecord(parentID, view.getEmptyString()); if(parent == null) { if(parentID.toLong() == 1) return null; // There is no parent to root. else throw new RuntimeException("INTERNAL ERROR: No folder thread found for ID " + parentID.toLong() + "!"); } if(parent instanceof CommonHFSCatalogFolderThreadRecord) { CommonHFSCatalogFolderThread data = ((CommonHFSCatalogFolderThreadRecord)parent).getData(); CommonHFSCatalogLeafRecord rec = view.getCatalogFile().getRecord(data.getParentID(), data.getNodeName()); if(rec == null) return null; else if(rec instanceof CommonHFSCatalogFolderRecord) return (CommonHFSCatalogFolderRecord)rec; else throw new RuntimeException("Internal error: rec not instanceof " + "CommonHFSCatalogFolderRecord, but instead:" + rec.getClass()); } else if(parent instanceof CommonHFSCatalogFileThreadRecord) { throw new RuntimeException("Tried to get folder thread record (" + parentID + ",\"\") but found a file thread record!"); } else { throw new RuntimeException("Tried to get folder thread record (" + parentID + ",\"\") but found a " + parent.getClass() + "!"); } } /** * Returns the underlying BaseHFSFileSystemView that serves the file system * handler with data.<br> * <b>Don't use this method if you want your code to be file system * independent!</b> * * @return the underlying BaseHFSFileSystemView. */ public HFSVolume getFSView() { return view; } @Override public void close() { view.close(); } @Override public FSFolder getRoot() { return new HFSCommonFSFolder(this, view.getCatalogFile().getRootFolder()); } @Override public String parsePosixPathnameComponent(String posixPathnameComponent) { return posixNames ? posixPathnameComponent : posixWrap(posixPathnameComponent); } @Override public String generatePosixPathnameComponent(String fsPathnameComponent) { return posixNames ? fsPathnameComponent : posixWrap(fsPathnameComponent); } @Override public String[] getTargetPath(FSLink link, String[] parentDir) { if(link instanceof HFSCommonFSLink) { HFSCommonFSLink hfsLink = (HFSCommonFSLink) link; return getTruePathFromPosixPath(hfsLink.getLinkTargetPosixPath(), parentDir); } else throw new RuntimeException("Invalid type: " + link.getClass()); } public void setHideProtected(boolean hideProtected) { this.hideProtected = hideProtected; } protected abstract Long getLinkCount(CommonHFSCatalogFileRecord fr); }