/* * AppleCommander - An Apple ][ image utility. * Copyright (C) 2002-2022 by Robert Greene * robgreene at users.sourceforge.net * * 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 2 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, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.webcodepro.applecommander.storage.os.prodos; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.HashSet; import com.webcodepro.applecommander.storage.DirectoryEntry; import com.webcodepro.applecommander.storage.DiskException; import com.webcodepro.applecommander.storage.DiskCorruptException; import com.webcodepro.applecommander.storage.DiskFullException; import com.webcodepro.applecommander.storage.FileEntry; import com.webcodepro.applecommander.storage.FormattedDisk; import com.webcodepro.applecommander.storage.StorageBundle; import com.webcodepro.applecommander.storage.physical.ImageOrder; import com.webcodepro.applecommander.util.AppleUtil; import com.webcodepro.applecommander.util.TextBundle; /** * Manages a disk that is in the ProDOS format. *

* Date created: Oct 3, 2002 11:45:25 PM * @author Rob Greene * * Changed at: Dec 1, 2017 * @author Lisias Toledo */ public class ProdosFormatDisk extends FormattedDisk { private TextBundle textBundle = StorageBundle.getInstance(); /** * The location of the "next block" pointer in a directory entry. * This is a 2-byte word (lo/hi) format. $0000 is end of directory. */ private static final int NEXT_BLOCK_POINTER = 2; /** * The location of the "previous block" pointer in a directory entry. * This is a 2-byte word (lo/hi) format. $0000 is start of directory. */ private static final int PREV_BLOCK_POINTER = 0; /** * The Volume Directory block number. */ private static final int VOLUME_DIRECTORY_BLOCK = 2; /** * A complete list of all known ProDOS filetypes. Note that this * list really cannot be complete, as there are multiple mappings per * identifier in some cases - differentiated by AUXTYPE. This is * loaded via the static initializer. */ private static ProdosFileType[] fileTypes; /** * This array of strings contains all filetypes. This is lazy * initialized by getFiletypes. */ private static String[] filetypeStrings; /** * Hold on to the volume directory header. */ private ProdosVolumeDirectoryHeader volumeHeader; /** * This class holds filetype mappings. */ private static class ProdosFileType { private byte type; private String string; private boolean addressRequired; private boolean canCompile; public ProdosFileType(byte type, String string, boolean addressRequired, boolean canCompile) { this.type = type; this.string = string; this.addressRequired = addressRequired; this.canCompile = canCompile; } public byte getType() { return type; } public String getString() { return string; } public boolean needsAddress() { return addressRequired; } public boolean canCompile() { return canCompile; } } /** * Use this inner interface for managing the disk usage data. * This off-loads format-specific implementation to the implementing class. */ private class ProdosDiskUsage implements DiskUsage { private int location = -1; private transient byte[] data = null; public boolean hasNext() { return location == -1 || location < getVolumeHeader().getTotalBlocks() - 1; } public void next() { location++; } /** * Get the free setting for the bitmap at the current location. */ public boolean isFree() { if (location == -1) { throw new IllegalArgumentException(StorageBundle.getInstance() .get("ProdosFormatDisk.InvalidDimensionError")); //$NON-NLS-1$ } if (data == null) { data = readVolumeBitMap(); } return isBlockFree(data, location); } public boolean isUsed() { return !isFree(); } } /** * Constructor for ProdosFormatDisk. * @param filename */ public ProdosFormatDisk(String filename, ImageOrder imageOrder) { super(filename, imageOrder); volumeHeader = new ProdosVolumeDirectoryHeader(this); } /** * Initialize all file types. */ static { fileTypes = new ProdosFileType[256]; InputStream inputStream = ProdosFormatDisk.class.getResourceAsStream("ProdosFileTypes.properties"); //$NON-NLS-1$ Properties properties = new Properties(); try { properties.load(inputStream); for (int i=0; i<256; i++) { String byt = AppleUtil.getFormattedByte(i).toLowerCase(); String string = (String) properties.get("filetype." + byt); //$NON-NLS-1$ if (string == null || string.length() == 0) { string = "$" + byt.toUpperCase(); //$NON-NLS-1$ } boolean addressRequired = Boolean.valueOf((String) properties.get( "filetype." + byt + ".address")).booleanValue(); //$NON-NLS-1$ //$NON-NLS-2$ boolean canCompile = Boolean.valueOf((String) properties.get( "filetype." + byt + ".compile")).booleanValue(); //$NON-NLS-1$ //$NON-NLS-2$ fileTypes[i] = new ProdosFileType((byte)i, string, addressRequired, canCompile); } } catch (IOException ignored) { // Ignored } } /** * Create a ProdosFormatDisk. */ public static ProdosFormatDisk[] create(String filename, String diskName, ImageOrder imageOrder) { ProdosFormatDisk disk = new ProdosFormatDisk(filename, imageOrder); disk.format(); disk.setDiskName(diskName); return new ProdosFormatDisk[] { disk }; } /** * Identify the operating system format of this disk. * @see com.webcodepro.applecommander.storage.FormattedDisk#getFormat() */ public String getFormat() { return textBundle.get("Prodos"); //$NON-NLS-1$ } /** * Create a FileEntry in the Volume Directory. */ public ProdosFileEntry createFile() throws DiskFullException { return createFile(volumeHeader); } /** * Create a FileEntry in the given directory. */ public ProdosFileEntry createFile(ProdosCommonDirectoryHeader directory) throws DiskFullException { int blockNumber = directory.getFileEntryBlock(); int headerBlock = blockNumber; while (blockNumber != 0) { byte[] block = readBlock(blockNumber); int offset = 4; while (offset+ProdosCommonEntry.ENTRY_LENGTH < BLOCK_SIZE) { int value = AppleUtil.getUnsignedByte(block[offset]); if ((value & 0xf0) == 0) { ProdosFileEntry fileEntry = new ProdosFileEntry(this, blockNumber, offset); fileEntry.setKeyPointer(0); //may have been recycled fileEntry.setCreationDate(new Date()); fileEntry.setProdosVersion(0); fileEntry.setMinimumProdosVersion(0); fileEntry.setCanDestroy(true); fileEntry.setCanRead(true); fileEntry.setCanRename(true); fileEntry.setCanWrite(true); fileEntry.setSeedlingFile(); fileEntry.setHeaderPointer(headerBlock); fileEntry.setFilename(textBundle.get("ProdosFormatDisk.Blank")); //$NON-NLS-1$ directory.incrementFileCount(); return fileEntry; } offset+= ProdosCommonEntry.ENTRY_LENGTH; } int nextBlockNumber = AppleUtil.getWordValue(block, NEXT_BLOCK_POINTER); if (nextBlockNumber == 0 && directory instanceof ProdosSubdirectoryHeader) { byte[] volumeBitmap = readVolumeBitMap(); nextBlockNumber = findFreeBlock(volumeBitmap); setBlockUsed(volumeBitmap, nextBlockNumber); writeVolumeBitMap(volumeBitmap); byte[] oldBlock = readBlock(blockNumber); AppleUtil.setWordValue(oldBlock, NEXT_BLOCK_POINTER, nextBlockNumber); writeBlock(blockNumber, oldBlock); byte[] nextBlock = new byte[BLOCK_SIZE]; AppleUtil.setWordValue(nextBlock, PREV_BLOCK_POINTER, blockNumber); writeBlock(nextBlockNumber, nextBlock); ProdosSubdirectoryHeader header = (ProdosSubdirectoryHeader) directory; int blockCount = header.getProdosDirectoryEntry().getBlocksUsed(); blockCount++; header.getProdosDirectoryEntry().setBlocksUsed(blockCount); header.getProdosDirectoryEntry().setEofPosition(blockCount * BLOCK_SIZE); } blockNumber = nextBlockNumber; } throw new DiskFullException(textBundle.get("ProdosFormatDisk.UnableToAllocateSpaceError"), this.getFilename()); //$NON-NLS-1$ } /** * Retrieve a list of files. * @throws DiskException * @see com.webcodepro.applecommander.storage.FormattedDisk#getFiles() */ public List getFiles() throws DiskException { return getFiles(VOLUME_DIRECTORY_BLOCK); } /** * Build a list of files, starting in the given block number. * This works for the master as well as the subdirectories. */ protected List getFiles(int blockNumber) throws DiskException { List files = new ArrayList<>(); final Set visits = new HashSet<>(); while (blockNumber != 0) { // Prevents a recursive catalog crawling. if ( visits.contains(blockNumber)) throw new DiskCorruptException(this.getFilename(), DiskCorruptException.Kind.RECURSIVE_DIRECTORY_STRUCTURE, new ProdosBlockAddress(blockNumber)); else visits.add(blockNumber); byte[] block = readBlock(blockNumber); int offset = 4; while (offset+ProdosCommonEntry.ENTRY_LENGTH < BLOCK_SIZE) { ProdosCommonEntry tester = new ProdosCommonEntry(this, blockNumber, offset); if (tester.isVolumeHeader() || tester.isSubdirectoryHeader()) { // ignore it, we've already got it } else if (!tester.isEmpty()) { ProdosFileEntry fileEntry = new ProdosFileEntry(this, blockNumber, offset); if (fileEntry.isDirectory()) { int keyPointer = fileEntry.getKeyPointer(); ProdosDirectoryEntry directoryEntry = new ProdosDirectoryEntry(this, blockNumber, offset, new ProdosSubdirectoryHeader(this, keyPointer)); files.add(directoryEntry); } else { files.add(fileEntry); } } offset+= ProdosCommonEntry.ENTRY_LENGTH; } blockNumber = AppleUtil.getWordValue(block, NEXT_BLOCK_POINTER); } return files; } /** * Return the amount of free space in bytes. * @see com.webcodepro.applecommander.storage.FormattedDisk#getFreeSpace() */ public int getFreeSpace() { return getFreeBlocks() * BLOCK_SIZE; } /** * Return the number of free blocks on the disk. */ public int getFreeBlocks() { int freeBlocks = 0; int blocksToProcess = (volumeHeader.getTotalBlocks() + 4095) / 4096; int blockNumber = volumeHeader.getBitMapPointer(); for (int ix=0; ix getDiskInformation() { List list = super.getDiskInformation(); list.add(new DiskInformation(textBundle.get("TotalBlocks"), //$NON-NLS-1$ volumeHeader.getTotalBlocks())); list.add(new DiskInformation(textBundle.get("FreeBlocks"), //$NON-NLS-1$ getFreeBlocks())); list.add(new DiskInformation(textBundle.get("UsedBlocks"), //$NON-NLS-1$ getUsedBlocks())); list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.VolumeAccess"), //$NON-NLS-1$ (volumeHeader.canDestroy() ? textBundle.get("Destroy") : "") + //$NON-NLS-1$//$NON-NLS-2$ (volumeHeader.canRead() ? textBundle.get("Read") : "") + //$NON-NLS-1$//$NON-NLS-2$ (volumeHeader.canRename() ? textBundle.get("Rename") : "") + //$NON-NLS-1$//$NON-NLS-2$ (volumeHeader.canWrite() ? textBundle.get("Write") : ""))); //$NON-NLS-1$//$NON-NLS-2$ list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.BitmapBlockNumber"), volumeHeader.getBitMapPointer())); //$NON-NLS-1$ list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.CreationDate"), volumeHeader.getCreationDate())); //$NON-NLS-1$ list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.FileEntriesPerBlock"), volumeHeader.getEntriesPerBlock())); //$NON-NLS-1$ list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.FileEntryLength"), volumeHeader.getEntryLength())); //$NON-NLS-1$ list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.ActiveFilesInRootDirectory"), volumeHeader.getFileCount())); //$NON-NLS-1$ list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.MinimumVersionProdos"), //$NON-NLS-1$ volumeHeader.getMinimumProdosVersion())); list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.CreationVersionProdos"), volumeHeader.getProdosVersion())); //$NON-NLS-1$ list.add(new DiskInformation(textBundle.get("ProdosFormatDisk.VolumeName"), volumeHeader.getVolumeName())); //$NON-NLS-1$ return list; } /** * Get the standard file column header information. * This default implementation is intended only for standard mode. */ public List getFileColumnHeaders(int displayMode) { List list = new ArrayList<>(); switch (displayMode) { case FILE_DISPLAY_NATIVE: list.add(new FileColumnHeader(" ", 1, FileColumnHeader.ALIGN_CENTER, "locked")); list.add(new FileColumnHeader(textBundle.get("Name"), 15, FileColumnHeader.ALIGN_LEFT, "name")); list.add(new FileColumnHeader(textBundle.get("Filetype"), 8, FileColumnHeader.ALIGN_CENTER, "type")); list.add(new FileColumnHeader(textBundle.get("Blocks"), 3, FileColumnHeader.ALIGN_RIGHT, "blocks")); list.add(new FileColumnHeader(textBundle.get("Modified"), 10, FileColumnHeader.ALIGN_CENTER, "modified")); list.add(new FileColumnHeader( textBundle.get("ProdosFormatDisk.Created"), 10, FileColumnHeader.ALIGN_CENTER, "created")); list.add(new FileColumnHeader( textBundle.get("ProdosFormatDisk.Length"), 10, FileColumnHeader.ALIGN_RIGHT, "size")); list.add(new FileColumnHeader( textBundle.get("ProdosFormatDisk.AuxType"), 8, FileColumnHeader.ALIGN_LEFT, "auxType")); break; case FILE_DISPLAY_DETAIL: list.add(new FileColumnHeader(" ", 1, FileColumnHeader.ALIGN_CENTER, "locked")); list.add(new FileColumnHeader(textBundle.get("Name"), 15, FileColumnHeader.ALIGN_LEFT, "name")); list.add(new FileColumnHeader(textBundle.get("DeletedQ"), 7, FileColumnHeader.ALIGN_CENTER, "deleted")); list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.Permissions"), 8, FileColumnHeader.ALIGN_LEFT, "permissions")); list.add(new FileColumnHeader(textBundle.get("Filetype"), 8, FileColumnHeader.ALIGN_CENTER, "type")); list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.DirectoryQ"), 9, FileColumnHeader.ALIGN_CENTER, "directory")); list.add(new FileColumnHeader(textBundle.get("Blocks"), 3, FileColumnHeader.ALIGN_RIGHT, "blocks")); list.add(new FileColumnHeader(textBundle.get("Modified"), 10, FileColumnHeader.ALIGN_CENTER, "modified")); list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.Created"), 10, FileColumnHeader.ALIGN_CENTER, "created")); list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.Length"), 10, FileColumnHeader.ALIGN_RIGHT, "size")); list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.AuxType"), 8, FileColumnHeader.ALIGN_LEFT, "auxType")); list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.DirectoryHeader"), 5, FileColumnHeader.ALIGN_RIGHT, "directory")); list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.KeyBlock"), 5, FileColumnHeader.ALIGN_RIGHT, "keyBlock")); list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.KeyType"), 8, FileColumnHeader.ALIGN_LEFT, "keyType")); list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.Changed"), 5, FileColumnHeader.ALIGN_CENTER, "changed")); list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.MinimumProdosVersion"), 2, FileColumnHeader.ALIGN_CENTER, "minimumProdosVersion")); list.add(new FileColumnHeader(textBundle.get("ProdosFormatDisk.ProdosVersion"), 2, FileColumnHeader.ALIGN_CENTER, "prodosVersion")); break; default: // FILE_DISPLAY_STANDARD list.addAll(super.getFileColumnHeaders(displayMode)); break; } return list; } /** * Indicates if this disk format supports "deleted" files. */ public boolean supportsDeletedFiles() { return true; } /** * Indicates if this disk image can read data from a file. */ public boolean canReadFileData() { return true; } /** * Identify if this disk format is capable of having directories. * @see com.webcodepro.applecommander.storage.FormattedDisk#canHaveDirectories() */ public boolean canHaveDirectories() { return true; } /** * Indicates if this disk image can write data to a file. */ public boolean canWriteFileData() { return true; } /** * Indicates if this disk image can delete a file. */ public boolean canDeleteFile() { return true; } /** * Get the data associated with the specified FileEntry. * Note that this could return a 16MB file! Sparse files are not treated specially. */ public byte[] getFileData(FileEntry fileEntry) { if ( !(fileEntry instanceof ProdosFileEntry)) { throw new IllegalArgumentException(textBundle.get("ProdosFormatDisk.MustHaveEntry")); //$NON-NLS-1$ } ProdosFileEntry prodosEntry = (ProdosFileEntry) fileEntry; byte[] fileData = new byte[prodosEntry.getEofPosition()]; if (prodosEntry.isSeedlingFile()) { byte[] blockData = readBlock(prodosEntry.getKeyPointer()); System.arraycopy(blockData, 0, fileData, 0, prodosEntry.getEofPosition()); } else if (prodosEntry.isSaplingFile()) { byte[] indexBlock = readBlock(prodosEntry.getKeyPointer()); getIndexBlockData(fileData, indexBlock, 0); } else if (prodosEntry.isTreeFile()) { byte[] masterIndexBlock = readBlock(prodosEntry.getKeyPointer()); int offset = 0; for (int i=0; i<0x100; i++) { int blockNumber = AppleUtil.getWordValue(masterIndexBlock[i], masterIndexBlock[i+0x100]); if (blockNumber > 0) { // FIXME - this may break sparse files! byte[] indexBlock = readBlock(blockNumber); offset= getIndexBlockData(fileData, indexBlock, offset); } } } else { throw new IllegalArgumentException(textBundle.get("ProdosFormatDisk.UnknownStorageType")); //$NON-NLS-1$ } return fileData; } /** * Free blocks used by a ProdosFileEntry. */ protected void freeBlocks(ProdosFileEntry prodosFileEntry) { byte[] bitmap = readVolumeBitMap(); int block = prodosFileEntry.getKeyPointer(); if (block == 0) return; // new entry if (prodosFileEntry.isGEOSFile()) { // A GEOS file allocates another block, pointed to by the aux bytes. setBlockFree(bitmap,prodosFileEntry.getAuxiliaryType()); } setBlockFree(bitmap,block); if (prodosFileEntry.isSaplingFile()) { freeBlocksInIndex(bitmap,block,false); } else if (prodosFileEntry.isTreeFile()) { byte[] masterIndexBlock = readBlock(block); for (int i=0; i<0x100; i++) { if (!prodosFileEntry.isGEOSFile() || (prodosFileEntry.isGEOSFile() && (i < 0xfe))) { // As long as we're not deleting a GEOS file, delete all index entries. // GEOS uses records 0xfe and 0xff for space calculations, not pointers. int indexBlockNumber = AppleUtil.getWordValue( masterIndexBlock[i], masterIndexBlock[i+0x100]); if (indexBlockNumber > 0) freeBlocksInIndex(bitmap,indexBlockNumber,prodosFileEntry.isGEOSFile()); } } } writeVolumeBitMap(bitmap); } /** * Free the given index block and the data blocks it points to. */ private void freeBlocksInIndex(byte[] bitmap, int indexBlockNumber, boolean isGEOS) { setBlockFree(bitmap, indexBlockNumber); byte[] indexBlock = readBlock(indexBlockNumber); for (int i=0; i<0x100; i++) { if (!isGEOS || (isGEOS && (i < 0xfe))) { // As long as we're not deleting a GEOS file, delete all entries. // GEOS uses records 0xfe and 0xff for space calculations, not pointers. int blockNumber = AppleUtil.getWordValue(indexBlock[i], indexBlock[i+0x100]); if (blockNumber > 0) setBlockFree(bitmap, blockNumber); } } } /** * Read file data from the given index block. * Note that block number 0 is an unused block. */ protected int getIndexBlockData(byte[] fileData, byte[] indexBlock, int offset) { for (int i=0; i<0x100; i++) { int blockNumber = AppleUtil.getWordValue(indexBlock[i], indexBlock[i+0x100]); byte[] blockData = readBlock(blockNumber); if (offset + blockData.length > fileData.length) { // end of file int bytesToCopy = fileData.length - offset; if (blockNumber != 0) System.arraycopy(blockData, 0, fileData, offset, bytesToCopy); offset+= bytesToCopy; break; } if (blockNumber != 0) System.arraycopy(blockData, 0, fileData, offset, blockData.length); offset+= blockData.length; } return offset; } /** * Set the data associated with the specified ProdosFileEntry into sectors * on the disk. Automatically grows the filesystem structures from seedling * to sapling to tree. */ protected void setFileData(ProdosFileEntry fileEntry, byte[] fileData) throws DiskFullException { if (fileEntry.isGEOSFile()) { // If this is a GEOS file, things are a bit different. setGEOSFileData(fileEntry, fileData); } else { // compute free space and see if the data will fit! int numberOfDataBlocks = (fileData.length + BLOCK_SIZE - 1) / BLOCK_SIZE; if (fileData.length == 0) numberOfDataBlocks = 1; int numberOfBlocks = numberOfDataBlocks; if (numberOfBlocks > 1) { numberOfBlocks+= ((numberOfDataBlocks-1) / 256) + 1; // that's 128K if (numberOfDataBlocks > 256) { numberOfBlocks++; } } if (numberOfBlocks > getFreeBlocks() + fileEntry.getBlocksUsed()) { throw new DiskFullException(textBundle. format("ProdosFormatDisk.NotEnoughSpaceOnDiskError", //$NON-NLS-1$ numberOfBlocks, getFreeBlocks()) , this.getFilename()); } // free "old" data and just rewrite stuff... freeBlocks(fileEntry); byte[] bitmap = readVolumeBitMap(); int blockNumber = fileEntry.getKeyPointer(); if (blockNumber == 0) { blockNumber = findFreeBlock(bitmap); } int indexBlockNumber = 0; byte[] indexBlockData = null; int masterIndexBlockNumber = 0; byte[] masterIndexBlockData = new byte[BLOCK_SIZE]; int offset = 0; int blockCount = 0; // Need to let a file length go through once while ((offset < fileData.length) || ((fileData.length == 0) && (offset == 0))){ if (blockCount > 0) blockNumber = findFreeBlock(bitmap); setBlockUsed(bitmap, blockNumber); blockCount++; byte[] blockData = new byte[BLOCK_SIZE]; int length = Math.min(BLOCK_SIZE, fileData.length - offset); System.arraycopy(fileData,offset,blockData,0,length); writeBlock(blockNumber, blockData); if (numberOfDataBlocks > 1) { // growing to a tree file if (offset > 0 && (offset / BLOCK_SIZE) % 256 == 0) { if (masterIndexBlockNumber == 0) { masterIndexBlockNumber = findFreeBlock(bitmap); setBlockUsed(bitmap, masterIndexBlockNumber); blockCount++; } writeBlock(indexBlockNumber, indexBlockData); indexBlockData = null; indexBlockNumber = 0; } // new index block if (indexBlockData == null) { // sapling files indexBlockNumber = findFreeBlock(bitmap); indexBlockData = new byte[BLOCK_SIZE]; setBlockUsed(bitmap, indexBlockNumber); blockCount++; // This is only used for Tree files (but we always record it): int position = (offset / (BLOCK_SIZE * 256)); byte low = (byte)(indexBlockNumber % 256); byte high = (byte)(indexBlockNumber / 256); masterIndexBlockData[position] = low; masterIndexBlockData[position + 0x100] = high; } // record last block position in index block int position = (offset / BLOCK_SIZE) % 256; byte low = (byte)(blockNumber % 256); byte high = (byte)(blockNumber / 256); indexBlockData[position] = low; indexBlockData[position + 0x100] = high; } offset+= BLOCK_SIZE; } if (numberOfDataBlocks == 1) { fileEntry.setKeyPointer(blockNumber); fileEntry.setSeedlingFile(); } else if (numberOfDataBlocks <= 256) { writeBlock(indexBlockNumber, indexBlockData); fileEntry.setKeyPointer(indexBlockNumber); fileEntry.setSaplingFile(); } else { writeBlock(indexBlockNumber, indexBlockData); writeBlock(masterIndexBlockNumber, masterIndexBlockData); fileEntry.setKeyPointer(masterIndexBlockNumber); fileEntry.setTreeFile(); } fileEntry.setBlocksUsed(blockCount); fileEntry.setEofPosition(fileData.length); fileEntry.setLastModificationDate(new Date()); writeVolumeBitMap(bitmap); } } /** * Set the data associated with the specified ProdosFileEntry into sectors * on the disk. Automatically grows the filesystem structures from seedling * to sapling to tree. */ // TODO: the writing of a single fork can be factored out... it is very common to routines nearby. protected void setFileData(ProdosFileEntry fileEntry, byte[] dataFork, byte[] resourceFork) throws DiskFullException { int dataLength = 0, resourceLength = 0; if (dataFork != null) dataLength = dataFork.length; if (resourceFork != null) resourceLength = resourceFork.length; // compute free space and see if the data will fit! int numberOfDataBlocks = (dataLength + BLOCK_SIZE - 1) / BLOCK_SIZE + (resourceLength + BLOCK_SIZE - 1) / BLOCK_SIZE; int numberOfBlocks = numberOfDataBlocks; if (numberOfBlocks > 1) { numberOfBlocks+= ((numberOfDataBlocks-1) / 256) + 1; // that's 128K if (numberOfDataBlocks > 256) { numberOfBlocks++; } } if (numberOfBlocks > getFreeBlocks() + fileEntry.getBlocksUsed()) { throw new DiskFullException(textBundle. format("ProdosFormatDisk.NotEnoughSpaceOnDiskError", //$NON-NLS-1$ numberOfBlocks, getFreeBlocks()) , this.getFilename()); } // free "old" data and just rewrite stuff... freeBlocks(fileEntry); byte[] bitmap = readVolumeBitMap(); int blockNumber = fileEntry.getKeyPointer(); if (blockNumber == 0) { blockNumber = findFreeBlock(bitmap); } int blockCount = 0; int extendedKeyBlockNumber = findFreeBlock(bitmap); setBlockUsed(bitmap, extendedKeyBlockNumber); byte[] extendedKeyBlockData = new byte[BLOCK_SIZE]; int indexBlockNumber = 0; byte[] indexBlockData = null; int masterIndexBlockNumber = 0; byte[] masterIndexBlockData = new byte[BLOCK_SIZE]; int offset = 0; numberOfDataBlocks = (dataLength + BLOCK_SIZE - 1) / BLOCK_SIZE ; while (offset < dataLength) { blockNumber = findFreeBlock(bitmap); setBlockUsed(bitmap, blockNumber); blockCount++; byte[] blockData = new byte[BLOCK_SIZE]; int length = Math.min(BLOCK_SIZE, dataLength - offset); System.arraycopy(dataFork,offset,blockData,0,length); writeBlock(blockNumber, blockData); if (numberOfDataBlocks > 1) { // growing to a tree file if (offset > 0 && (offset / BLOCK_SIZE) % 256 == 0) { if (masterIndexBlockNumber == 0) { masterIndexBlockNumber = findFreeBlock(bitmap); setBlockUsed(bitmap, masterIndexBlockNumber); blockCount++; } writeBlock(indexBlockNumber, indexBlockData); indexBlockData = null; indexBlockNumber = 0; } // new index block if (indexBlockData == null) { // sapling files indexBlockNumber = findFreeBlock(bitmap); indexBlockData = new byte[BLOCK_SIZE]; setBlockUsed(bitmap, indexBlockNumber); blockCount++; // This is only used for Tree files (but we always record it): int position = (offset / (BLOCK_SIZE * 256)); byte low = (byte)(indexBlockNumber % 256); byte high = (byte)(indexBlockNumber / 256); masterIndexBlockData[position] = low; masterIndexBlockData[position + 0x100] = high; } // record last block position in index block int position = (offset / BLOCK_SIZE) % 256; byte low = (byte)(blockNumber % 256); byte high = (byte)(blockNumber / 256); indexBlockData[position] = low; indexBlockData[position + 0x100] = high; } offset+= BLOCK_SIZE; } AppleUtil.setWordValue(extendedKeyBlockData,0x03,numberOfDataBlocks); // Set the number of blocks used AppleUtil.set3ByteValue(extendedKeyBlockData,0x05, dataLength); // Set the number of bytes used if ((numberOfDataBlocks == 0) || (numberOfDataBlocks == 1)) { extendedKeyBlockData[0] = 1; // Set the seedling ProDOS storage type AppleUtil.setWordValue(extendedKeyBlockData,1,blockNumber); // Set the master block number } else if (numberOfDataBlocks <= 256) { writeBlock(indexBlockNumber, indexBlockData); AppleUtil.setWordValue(extendedKeyBlockData,1,indexBlockNumber); // Set the master block number extendedKeyBlockData[0] = 2; // Set the sapling ProDOS storage type } else { writeBlock(indexBlockNumber, indexBlockData); writeBlock(masterIndexBlockNumber, masterIndexBlockData); AppleUtil.setWordValue(extendedKeyBlockData,1,masterIndexBlockNumber); // Set the master block number extendedKeyBlockData[0] = 3; // Set the tree ProDOS storage type } blockCount++; // To account for extendedKeyBlock offset = 0; indexBlockNumber = 0; indexBlockData = null; masterIndexBlockNumber = 0; numberOfDataBlocks = (resourceLength + BLOCK_SIZE - 1) / BLOCK_SIZE; while (offset < resourceLength) { if (blockCount > 0) blockNumber = findFreeBlock(bitmap); setBlockUsed(bitmap, blockNumber); blockCount++; byte[] blockData = new byte[BLOCK_SIZE]; int length = Math.min(BLOCK_SIZE, resourceLength - offset); System.arraycopy(resourceFork,offset,blockData,0,length); writeBlock(blockNumber, blockData); if (numberOfDataBlocks > 1) { // growing to a tree file if (offset > 0 && (offset / BLOCK_SIZE) % 256 == 0) { if (masterIndexBlockNumber == 0) { masterIndexBlockNumber = findFreeBlock(bitmap); setBlockUsed(bitmap, masterIndexBlockNumber); blockCount++; } writeBlock(indexBlockNumber, indexBlockData); indexBlockData = null; indexBlockNumber = 0; } // new index block if (indexBlockData == null) { // sapling files indexBlockNumber = findFreeBlock(bitmap); indexBlockData = new byte[BLOCK_SIZE]; setBlockUsed(bitmap, indexBlockNumber); blockCount++; // This is only used for Tree files (but we always record it): int position = (offset / (BLOCK_SIZE * 256)); byte low = (byte)(indexBlockNumber % 256); byte high = (byte)(indexBlockNumber / 256); masterIndexBlockData[position] = low; masterIndexBlockData[position + 0x100] = high; } // record last block position in index block int position = (offset / BLOCK_SIZE) % 256; byte low = (byte)(blockNumber % 256); byte high = (byte)(blockNumber / 256); indexBlockData[position] = low; indexBlockData[position + 0x100] = high; } offset+= BLOCK_SIZE; } AppleUtil.setWordValue(extendedKeyBlockData,0x103,numberOfDataBlocks); // Set the number of blocks used AppleUtil.set3ByteValue(extendedKeyBlockData,0x105, resourceLength); // Set the number of bytes used if ((numberOfDataBlocks == 0) || (numberOfDataBlocks == 1)) { extendedKeyBlockData[0x100] = 1; // Set the seedling ProDOS storage type AppleUtil.setWordValue(extendedKeyBlockData,0x101,blockNumber); // Set the master block number } else if (numberOfDataBlocks <= 256) { writeBlock(indexBlockNumber, indexBlockData); AppleUtil.setWordValue(extendedKeyBlockData,0x101,indexBlockNumber); // Set the master block number extendedKeyBlockData[0x100] = 2; // Set the sapling ProDOS storage type } else { writeBlock(indexBlockNumber, indexBlockData); writeBlock(masterIndexBlockNumber, masterIndexBlockData); AppleUtil.setWordValue(extendedKeyBlockData,0x101,masterIndexBlockNumber); // Set the master block number extendedKeyBlockData[0x100] = 3; // Set the tree ProDOS storage type } writeBlock(extendedKeyBlockNumber, extendedKeyBlockData); fileEntry.setKeyPointer(extendedKeyBlockNumber); fileEntry.setBlocksUsed(blockCount); fileEntry.setEofPosition(dataLength+resourceLength); fileEntry.setLastModificationDate(new Date()); writeVolumeBitMap(bitmap); } /** * Set the data associated with the specified ProdosFileEntry into sectors * on the disk. Take GEOS file structures into account. */ protected void setGEOSFileData(ProdosFileEntry fileEntry, byte[] fileData) throws DiskFullException { // compute free space and see if the data will fit! int numberOfDataBlocks = (fileData.length - 1) / BLOCK_SIZE; int numberOfBlocks = numberOfDataBlocks; numberOfBlocks+= ((numberOfDataBlocks-1) / 254) + 1; // GEOS uses the last two blocks for eof calculations if (numberOfDataBlocks > 254) { numberOfBlocks++; } if (numberOfBlocks > getFreeBlocks() + fileEntry.getBlocksUsed()) { throw new DiskFullException(textBundle. format("ProdosFormatDisk.NotEnoughSpaceOnDiskError", //$NON-NLS-1$ numberOfBlocks, getFreeBlocks()) , this.getFilename()); } // free "old" data and just rewrite stuff... freeBlocks(fileEntry); byte[] bitmap = readVolumeBitMap(); // Place the first BLOCK_SIZE bytes of data in a block pointed to by the aux address. int headerBlockNumber = findFreeBlock(bitmap); byte[] headerData = new byte[BLOCK_SIZE]; setBlockUsed(bitmap, headerBlockNumber); System.arraycopy(fileData,0,headerData,0,BLOCK_SIZE); writeBlock(headerBlockNumber, headerData); fileEntry.setAddress(headerBlockNumber); if (AppleUtil.getUnsignedByte(fileData[0x180]) >> 4 == 2) { setGEOSSaplingData(bitmap, fileEntry, fileData); } else { setGEOSTreeData(bitmap, fileEntry, fileData); } fileEntry.setGEOSMeta(headerData); } /** * Set the GEOS "sapling" file data. */ protected void setGEOSSaplingData(byte[] bitmap, ProdosFileEntry fileEntry, byte[] fileData) throws DiskFullException { int indexBlockNumber = findFreeBlock(bitmap); setBlockUsed(bitmap, indexBlockNumber); byte[] indexBlockData = new byte[BLOCK_SIZE]; int offset = BLOCK_SIZE; int blockNumber = 0; int blockCount = 1; // The header block counts for one while (offset < fileData.length) { blockNumber = findFreeBlock(bitmap); setBlockUsed(bitmap, blockNumber); blockCount++; byte[] blockData = new byte[BLOCK_SIZE]; int length = Math.min(BLOCK_SIZE, fileData.length - offset); System.arraycopy(fileData,offset,blockData,0,length); writeBlock(blockNumber, blockData); // record last block position in index block int position = ((offset - BLOCK_SIZE) / BLOCK_SIZE) % 256; byte low = (byte)(blockNumber % 256); byte high = (byte)(blockNumber / 256); indexBlockData[position] = low; indexBlockData[position + 0x100] = high; offset+= BLOCK_SIZE; } indexBlockData[255] = (byte)((fileData.length - BLOCK_SIZE) % 256); // Lo file eof indexBlockData[511] = (byte)((fileData.length - BLOCK_SIZE) / 256); // Med file eof writeBlock(indexBlockNumber, indexBlockData); fileEntry.setKeyPointer(indexBlockNumber); fileEntry.setSaplingFile(); fileEntry.setBlocksUsed(blockCount); fileEntry.setEofPosition(fileData.length - BLOCK_SIZE); fileEntry.setLastModificationDate(new Date()); writeVolumeBitMap(bitmap); } /** * Set the GEOS "tree" file data. */ protected void setGEOSTreeData(byte[] bitmap, ProdosFileEntry fileEntry, byte[] fileData) throws DiskFullException { int masterIndexBlockNumber = findFreeBlock(bitmap); setBlockUsed(bitmap, masterIndexBlockNumber); byte[] masterIndexBlockData = new byte[BLOCK_SIZE]; int offset = BLOCK_SIZE; int blockCount = 2; // Start by counting the header block and master index int eofCount = 0; for (int masterIterator = 0; masterIterator < 254; masterIterator++) { if ((fileData[0x100+masterIterator] != 0xff) && (offset < fileData.length)){ byte[] lengthData = new byte[BLOCK_SIZE]; System.arraycopy(fileData,offset,lengthData,0,BLOCK_SIZE); offset += BLOCK_SIZE; int recordLength = AppleUtil.getUnsignedByte(lengthData[0xff]) + AppleUtil.getUnsignedByte(lengthData[0x1ff])*256; int indexBlockNumber = findFreeBlock(bitmap); setBlockUsed(bitmap, indexBlockNumber); blockCount +=1; byte[] indexBlockData = new byte[BLOCK_SIZE]; int blockNumber = 0; int startingPoint = offset; while (offset < startingPoint + recordLength) { blockNumber = findFreeBlock(bitmap); setBlockUsed(bitmap, blockNumber); blockCount +=1; byte[] blockData = new byte[BLOCK_SIZE]; int length = Math.min(BLOCK_SIZE, recordLength - offset + startingPoint); System.arraycopy(fileData,offset,blockData,0,length); writeBlock(blockNumber, blockData); eofCount += length; // record last block position in index block int position = ((offset-startingPoint) / BLOCK_SIZE) % 256; byte low = (byte)(blockNumber % 256); byte high = (byte)(blockNumber / 256); indexBlockData[position] = low; indexBlockData[position + 0x100] = high; offset+= BLOCK_SIZE; } indexBlockData[0xff] = (byte) (recordLength & 0x0000ff); indexBlockData[0x1ff] = (byte)((recordLength & 0x00ff00) >> 8); indexBlockData[0x1fe] = (byte)((recordLength & 0xff0000) >> 16); writeBlock(indexBlockNumber, indexBlockData); byte low = (byte)(indexBlockNumber % 256); byte high = (byte)(indexBlockNumber / 256); masterIndexBlockData[masterIterator] = low; masterIndexBlockData[masterIterator + 0x100] = high; } else if (fileData[0x100+masterIterator] == 0xff) { masterIndexBlockData[masterIterator] = (byte)0xff; masterIndexBlockData[masterIterator+0x100] = (byte)0xff; } } masterIndexBlockData[0x0ff] = (byte) (eofCount & 0x0000ff); masterIndexBlockData[0x1ff] = (byte)((eofCount & 0x00ff00) >> 8); masterIndexBlockData[0x1fe] = (byte)((eofCount & 0xff0000) >> 16); writeBlock(masterIndexBlockNumber, masterIndexBlockData); fileEntry.setKeyPointer(masterIndexBlockNumber); fileEntry.setTreeFile(); fileEntry.setBlocksUsed(blockCount); fileEntry.setEofPosition(eofCount); fileEntry.setLastModificationDate(new Date()); writeVolumeBitMap(bitmap); } /** * Locate a free block in the Volume Bitmap. */ protected int findFreeBlock(byte[] volumeBitmap) throws DiskFullException { int block = 1; int blocksOnDisk = getBitmapLength(); while (block < blocksOnDisk) { if (isBlockFree(volumeBitmap,block)) { if ((block+1) * BLOCK_SIZE <= getPhysicalSize()) { return block; } throw new ProdosDiskSizeDoesNotMatchException( textBundle.get("ProdosFormatDisk.ProdosDiskSizeDoesNotMatchError") //$NON-NLS-1$ , this.getFilename()); } block++; } throw new DiskFullException( textBundle.get("ProdosFormatDisk.NoFreeBlockAvailableError") //$NON-NLS-1$ , this.getFilename()); } /** * Read the Volume Bit Map. */ public byte[] readVolumeBitMap() { int volumeBitmapBlock = volumeHeader.getBitMapPointer(); int volumeBitmapBlocks = volumeHeader.getTotalBlocks(); int blocksToRead = (volumeBitmapBlocks / 4096) + 1; // Read in the entire volume bitmap: byte[] data = new byte[blocksToRead * BLOCK_SIZE]; for (int i=0; i 2) ? block-1 : 0; AppleUtil.setWordValue(data, 0, prevBlock); AppleUtil.setWordValue(data, 2, nextBlock); writeBlock(block, data); } // setup volume header information (each set will also save data) volumeHeader.setVolumeHeader(); volumeHeader.setVolumeName(volumeName); volumeHeader.setCreationDate(new Date()); volumeHeader.setProdosVersion(0); volumeHeader.setMinimumProdosVersion(0); volumeHeader.setHasChanged(true); volumeHeader.setCanDestroy(true); volumeHeader.setCanRead(true); volumeHeader.setCanRename(true); volumeHeader.setCanWrite(true); volumeHeader.setEntryLength(); volumeHeader.setEntriesPerBlock(); volumeHeader.setFileCount(0); volumeHeader.setBitMapPointer(6); volumeHeader.setTotalBlocks(totalBlocks); // setup bitmap usage byte[] bitmap = readVolumeBitMap(); for (int block=0; block 0) { String what = filename.substring(pos+1); ProdosFileType type = findFileType(what); if (type != null) { filetype = type.getString(); } } return filetype; } /** * Return the filetype of this file. This will be three characters, * according to ProDOS - a "$xx" if unknown. */ public static String getFiletype(int filetype) { ProdosFileType prodostype = fileTypes[filetype]; return prodostype.getString(); } /** * Get the numerical filetype. */ public byte getFiletype(String filetype) { ProdosFileType type = findFileType(filetype); if (type != null) { return type.getType(); } return 0x00; } /** * Locate the associated ProdosFileType. */ public static ProdosFileType findFileType(String filetype) { for (int i=0; i