/* * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. * * 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 jace.hardware.massStorage; import static jace.hardware.massStorage.IDisk.BLOCK_SIZE; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; /** * Prodos directory node * * @author Brendan Robert (BLuRry) brendan.robert@gmail.com */ public class DirectoryNode extends DiskNode implements FileFilter { public static final byte STANDARD_PERMISSIONS = (byte) 0x0c3; public static final int PRODOS_VERSION = 0x023; public static final int FILE_ENTRY_SIZE = 0x027; public static final int ENTRIES_PER_BLOCK = (ProdosVirtualDisk.BLOCK_SIZE - 4) / FILE_ENTRY_SIZE; private boolean isRoot; private List<DiskNode> directoryEntries; public DirectoryNode(ProdosVirtualDisk ownerFilesystem, File physicalDir, int baseBlock, boolean root) throws IOException { super(ownerFilesystem, baseBlock); init(ownerFilesystem, physicalDir, root); } public DirectoryNode(ProdosVirtualDisk ownerFilesystem, File physicalDir, boolean root) throws IOException { super(ownerFilesystem); init(ownerFilesystem, physicalDir, root); } private void init(ProdosVirtualDisk ownerFilesystem, File physicalFile, boolean root) throws IOException { isRoot = root; directoryEntries = new ArrayList<>(); setPhysicalFile(physicalFile); setType(EntryType.SUBDIRECTORY); setName(physicalFile.getName()); allocate(); } @Override public void doDeallocate() { } @Override public void doAllocate() throws IOException { for (int i = 1; i < getBlockCount(); i++) { if (isRoot) { new SubNode(i, this, getOwnerFilesystem().getNextFreeBlock(3)); } else { new SubNode(i, this); } } for (File f : physicalFile.listFiles()) { addFile(f); } Collections.sort(children, (DiskNode o1, DiskNode o2) -> o1.getName().compareTo(o2.getName())); } @Override public void doRefresh() { } @Override /** * Checks contents of subdirectory for changes as well as directory itself * (super class) */ public boolean checkFile() throws IOException { boolean success = true; if (!allocated) { allocate(); } else { try { if (!super.checkFile()) { return false; } HashSet<String> realFiles = new HashSet<>(); File[] realFileList = physicalFile.listFiles(this); for (File f : realFileList) { realFiles.add(f.getName()); } for (Iterator<DiskNode> i = directoryEntries.iterator(); i.hasNext();) { DiskNode node = i.next(); if (realFiles.contains(node.getPhysicalFile().getName())) { realFiles.remove(node.getPhysicalFile().getName()); } else { i.remove(); success = false; } if (node.isAllocated()) { if (!(node instanceof DirectoryNode) && !node.checkFile()) { success = false; } } } if (!realFiles.isEmpty()) { success = false; // New files showed up -- deal with them! realFiles.stream().forEach((fileName) -> { addFile(new File(physicalFile, fileName)); }); } } catch (IOException ex) { return false; } } return success; } @Override public void readBlock(int block, byte[] buffer) throws IOException { checkFile(); int start = 0; int end = 0; int offset = 4; generatePointers(buffer, block); // System.out.println("Directory "+getName()+" sequence "+block+"; physical block "+getNodeSequence(block).getBaseBlock()); if (block == 0) { generateHeader(buffer); offset += FILE_ENTRY_SIZE; end = ENTRIES_PER_BLOCK - 1; } else { start = (block * ENTRIES_PER_BLOCK) - 1; end = start + ENTRIES_PER_BLOCK; } for (int i = start; i < end && i < directoryEntries.size(); i++, offset += FILE_ENTRY_SIZE) { // TODO: Add any parts that are not file entries. // System.out.println("Entry "+i+": "+children.get(i).getName()+"; offset "+offset); generateFileEntry(buffer, offset, i); } } @Override public boolean accept(File file) { if (file.getName().endsWith("~")) { return false; } char c = file.getName().charAt(0); if (c == '.' || c == '~') { return false; } return !file.isHidden(); } private void generatePointers(byte[] buffer, int sequence) { DiskNode prev = getNodeSequence(sequence - 1); DiskNode next = getNodeSequence(sequence + 1); // System.out.println("Sequence "+sequence+" prev="+(prev != null ? prev.getBaseBlock() : 0)+"; next="+(next != null ? next.getBaseBlock() : 0)); // Previous block (or 0) generateWord(buffer, 0, prev != null ? prev.getBaseBlock() : 0); // Next block (or 0) generateWord(buffer, 0x02, next != null ? next.getBaseBlock() : 0); } /** * Generate the directory header found in the base block of a directory * * @param buffer where to write data */ @SuppressWarnings("static-access") private void generateHeader(byte[] buffer) { // Directory header + name length // Volumme header = 0x0f0; Subdirectory header = 0x0e0 buffer[4] = (byte) ((isRoot ? 0x0F0 : 0x0E0) | getName().length()); generateName(buffer, 5, this); if (!isRoot) { buffer[0x014] = 0x075; buffer[0x015] = PRODOS_VERSION; buffer[0x017] = STANDARD_PERMISSIONS; buffer[0x018] = FILE_ENTRY_SIZE; buffer[0x019] = ENTRIES_PER_BLOCK; } generateTimestamp(buffer, 0x01c, getPhysicalFile().lastModified()); // Prodos 1.0 = 0 buffer[0x020] = PRODOS_VERSION; // Minimum version = 0 (no min) buffer[0x021] = 0x000; // Directory may be read/written to, may not be destroyed or renamed buffer[0x022] = STANDARD_PERMISSIONS; // Entry size buffer[0x023] = (byte) FILE_ENTRY_SIZE; // Entries per block buffer[0x024] = (byte) ENTRIES_PER_BLOCK; // Directory items count generateWord(buffer, 0x025, directoryEntries.size()+1); if (isRoot) { // Volume bitmap pointer generateWord(buffer, 0x027, ownerFilesystem.freespaceBitmap.getBaseBlock()); // Total number of blocks generateWord(buffer, 0x029, ownerFilesystem.MAX_BLOCK); } else { // According to the Beneath Apple Prodos supplement int indexInParent = getParent().getChildren().indexOf(this) + 2; int parentBlock = getParent().getNodeSequence(indexInParent / ENTRIES_PER_BLOCK).getBaseBlock(); // Parent pointer generateWord(buffer, 0x027, parentBlock); buffer[0x029] = (byte) (indexInParent % ENTRIES_PER_BLOCK); buffer[0x02a] = (byte) FILE_ENTRY_SIZE; } } /** * Generate the entry of a directory * * @param buffer where to write data * @param offset starting offset in buffer to write * @param fileNumber number of file (indexed in Children array) to write */ private void generateFileEntry(byte[] buffer, int offset, int fileNumber) throws IOException { DiskNode child = directoryEntries.get(fileNumber); // Entry Type and length buffer[offset] = (byte) ((child.getType().code << 4) | child.getName().length()); // Name generateName(buffer, offset + 1, child); // File type buffer[offset + 0x010] = (byte) ((child instanceof DirectoryNode) ? 0x0f : ((FileNode) child).fileType); // Key pointer generateWord(buffer, offset + 0x011, child.getBaseBlock()); // Blocks used -- will report only one unless file is actually allocated generateWord(buffer, offset + 0x013, child.additionalNodes.size() + 1); // EOF (file size or directory structure size int length = child.getLength(); length &= 0x0ffffff; generateWord(buffer, offset + 0x015, length & 0x0ffff); buffer[offset + 0x017] = (byte) ((length >> 16) & 0x0ff); // Creation date generateTimestamp(buffer, offset + 0x018, child.physicalFile.lastModified()); // Version = 1.0 buffer[offset + 0x01c] = PRODOS_VERSION; // Minimum version = 0 buffer[offset + 0x01d] = 0; // Access = Read-only buffer[offset + 0x01e] = STANDARD_PERMISSIONS; // AUX type if (child instanceof FileNode) { generateWord(buffer, offset + 0x01f, ((FileNode) child).loadAddress); } // Modification date generateTimestamp(buffer, offset + 0x021, child.physicalFile.lastModified()); // Key pointer for directory generateWord(buffer, offset + 0x025, getBaseBlock()); } private void generateTimestamp(byte[] buffer, int offset, long date) { Calendar c = Calendar.getInstance(); c.setTimeInMillis(date); // yyyyyyym mmmddddd - Byte 0,1 // ---hhhhh --mmmmmm - Byte 2,3 buffer[offset + 0] = (byte) (((((c.get(Calendar.MONTH) + 1) & 7) << 5) | c.get(Calendar.DAY_OF_MONTH)) & 0x0ff); buffer[offset + 1] = (byte) (((c.get(Calendar.YEAR) - 2000) << 1) | ((c.get(Calendar.MONTH) + 1) >> 3)); buffer[offset + 2] = (byte) c.get(Calendar.MINUTE); buffer[offset + 3] = (byte) c.get(Calendar.HOUR_OF_DAY); } private void generateWord(byte[] buffer, int i, int value) { // Little endian format buffer[i] = (byte) (value & 0x0ff); buffer[i + 1] = (byte) ((value >> 8) & 0x0ff); } private void generateName(byte[] buffer, int offset, DiskNode node) { for (int i = 0; i < node.getName().length() && i < 15; i++) { buffer[offset + i] = (byte) node.getName().charAt(i); } } private Optional<DiskNode> findChildByFilename(String name) { return directoryEntries.stream().filter((child) -> child.getPhysicalFile().getName().equals(name)).findFirst(); } private void addFile(File file) { if (!hasChildNamed(file.getName())) { try { if (file.isDirectory()) { addFileEntry(new DirectoryNode(getOwnerFilesystem(), file, false)); } else { addFileEntry(new FileNode(getOwnerFilesystem(), file)); } } catch (IOException ex) { Logger.getLogger(DirectoryNode.class.getName()).log(Level.SEVERE, null, ex); } } } private void addFileEntry(DiskNode entry) { directoryEntries.add(entry); entry.setParent(this); } @Override public int getLength() { return getBlockCount() * BLOCK_SIZE; } private int getBlockCount() { return isRoot ? 4 : 1 + (physicalFile.listFiles().length / ENTRIES_PER_BLOCK); } }