550 lines
18 KiB
Java
550 lines
18 KiB
Java
package com.bytezone.diskbrowser.prodos.write;
|
|
|
|
import static com.bytezone.diskbrowser.prodos.ProdosConstants.BLOCK_SIZE;
|
|
import static com.bytezone.diskbrowser.prodos.ProdosConstants.ENTRIES_PER_BLOCK;
|
|
import static com.bytezone.diskbrowser.prodos.ProdosConstants.ENTRY_SIZE;
|
|
import static com.bytezone.diskbrowser.prodos.ProdosConstants.FILE_TYPE_DIRECTORY;
|
|
import static com.bytezone.diskbrowser.prodos.ProdosConstants.SUBDIRECTORY;
|
|
import static com.bytezone.diskbrowser.prodos.ProdosConstants.SUBDIRECTORY_HEADER;
|
|
import static com.bytezone.diskbrowser.utilities.Utility.unsignedShort;
|
|
import static com.bytezone.diskbrowser.utilities.Utility.writeShort;
|
|
|
|
import java.io.DataInputStream;
|
|
import java.io.IOException;
|
|
import java.time.LocalDateTime;
|
|
import java.util.ArrayList;
|
|
import java.util.BitSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.TreeMap;
|
|
|
|
import com.bytezone.diskbrowser.prodos.write.ExtendedKeyBlock.ForkType;
|
|
|
|
// -----------------------------------------------------------------------------------//
|
|
public class ProdosDisk
|
|
// -----------------------------------------------------------------------------------//
|
|
{
|
|
static final String UNDERLINE = "------------------------------------------------\n";
|
|
static final String message = "DiskBrowser";
|
|
|
|
private static final int CATALOG_SIZE = 4;
|
|
private static final int BITS_PER_BLOCK = 8 * BLOCK_SIZE;
|
|
|
|
public static final String[] storageTypes =
|
|
{ "Deleted", "Seedling", "Sapling", "Tree", "", "", "", "", "", "", "", "", "",
|
|
"Subdirectory", "Subdirectory Header", "Volume Directory Header" };
|
|
|
|
private BitSet volumeBitMap;
|
|
private final int maxBlocks;
|
|
private final byte[] buffer;
|
|
private final byte[] bootSector = new byte[BLOCK_SIZE];
|
|
|
|
private VolumeDirectoryHeader volumeDirectoryHeader;
|
|
private Map<Integer, SubdirectoryHeader> subdirectoryHeaders = new TreeMap<> ();
|
|
private List<String> paths = new ArrayList<> ();
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
public ProdosDisk (int blocks, String volumeName) throws IOException, DiskFullException
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
try (DataInputStream in = new DataInputStream (ProdosDisk.class.getClassLoader ()
|
|
.getResourceAsStream ("com/bytezone/diskbrowser/prodos/write/block-00.bin")))
|
|
{
|
|
int count = in.read (bootSector);
|
|
if (count != BLOCK_SIZE)
|
|
System.out.println ("Error with prodos boot sector");
|
|
}
|
|
catch (IOException e)
|
|
{
|
|
e.printStackTrace ();
|
|
}
|
|
|
|
maxBlocks = blocks;
|
|
buffer = new byte[blocks * BLOCK_SIZE];
|
|
|
|
volumeBitMap = new BitSet (blocks);
|
|
for (int i = 0; i < blocks; i++)
|
|
volumeBitMap.set (i, true);
|
|
|
|
createCatalog (volumeName);
|
|
|
|
volumeDirectoryHeader.write ();
|
|
for (SubdirectoryHeader subdirectoryHeader : subdirectoryHeaders.values ())
|
|
subdirectoryHeader.write ();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
private void createCatalog (String volumeName) throws DiskFullException
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
// reserve two boot blocks
|
|
allocateNextBlock ();
|
|
allocateNextBlock ();
|
|
|
|
System.arraycopy (bootSector, 0, buffer, 0, 512);
|
|
System.arraycopy (message.getBytes (), 0, buffer, 768, message.length ());
|
|
|
|
// write 4 catalog blocks
|
|
for (int i = 0, prevBlockNo = 0; i < CATALOG_SIZE; i++)
|
|
{
|
|
int blockNo = allocateNextBlock ();
|
|
int ptr = blockNo * BLOCK_SIZE;
|
|
|
|
writeShort (buffer, ptr, prevBlockNo);
|
|
writeShort (buffer, ptr + 2, 0);
|
|
|
|
if (prevBlockNo > 0)
|
|
writeShort (buffer, prevBlockNo * BLOCK_SIZE + 2, blockNo);
|
|
|
|
prevBlockNo = blockNo;
|
|
|
|
if (i == 0)
|
|
{
|
|
volumeDirectoryHeader = new VolumeDirectoryHeader (this, ptr + 4);
|
|
volumeDirectoryHeader.fileName = volumeName;
|
|
volumeDirectoryHeader.totalBlocks = maxBlocks;
|
|
volumeDirectoryHeader.creationDate = LocalDateTime.now ();
|
|
volumeDirectoryHeader.write ();
|
|
}
|
|
}
|
|
|
|
// reserve the bitmap blocks
|
|
int indexBlocks = (maxBlocks - 1) / BITS_PER_BLOCK + 1;
|
|
for (int i = 0; i < indexBlocks; i++)
|
|
allocateNextBlock ();
|
|
|
|
writeVolumeBitMap ();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
public int getFreeBlocks ()
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
return volumeBitMap.cardinality ();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
public FileEntry addFile (String path, byte type, int auxType, LocalDateTime created,
|
|
LocalDateTime modified, byte[] fileBuffer, int eof)
|
|
throws DiskFullException, VolumeCatalogFullException, FileAlreadyExistsException
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
if (path.isBlank ())
|
|
throw new IllegalArgumentException ("Path is empty");
|
|
|
|
// save path for verification
|
|
paths.add (path);
|
|
|
|
// split the full path into an array of subdirectories and a file name
|
|
String[] subdirectories;
|
|
String fileName;
|
|
|
|
int pos = path.lastIndexOf ('/');
|
|
if (pos > 0)
|
|
{
|
|
subdirectories = path.substring (0, pos).split ("/");
|
|
fileName = path.substring (pos + 1);
|
|
}
|
|
else
|
|
{
|
|
subdirectories = new String[0];
|
|
fileName = path;
|
|
}
|
|
|
|
// search for each subdirectory, create any that don't exist
|
|
int catalogBlockNo = createPath (subdirectories);
|
|
|
|
// check that the file doesn't already exist
|
|
Optional<FileEntry> fileEntryOpt = searchDirectory (catalogBlockNo, fileName);
|
|
if (fileEntryOpt.isPresent ())
|
|
{
|
|
System.out.println ("File already exists: " + path);
|
|
System.out.println (fileEntryOpt.get ());
|
|
throw new FileAlreadyExistsException (fileName);
|
|
}
|
|
|
|
// create a file entry in the current catalog block
|
|
FileEntry fileEntry = findFreeSlot (catalogBlockNo);
|
|
|
|
if (fileEntry != null)
|
|
{
|
|
fileEntry.fileName = fileName;
|
|
fileEntry.version = 0x00;
|
|
fileEntry.minVersion = 0x00;
|
|
fileEntry.headerPointer = catalogBlockNo; // block containing catalog header
|
|
fileEntry.fileType = type;
|
|
fileEntry.auxType = auxType;
|
|
fileEntry.creationDate = created;
|
|
fileEntry.modifiedDate = modified;
|
|
|
|
FileWriter fileWriter = new FileWriter (this);
|
|
fileWriter.writeFile (fileBuffer, eof);
|
|
|
|
fileEntry.storageType = fileWriter.storageType;
|
|
fileEntry.keyPointer = fileWriter.keyPointer;
|
|
fileEntry.blocksUsed = fileWriter.blocksUsed;
|
|
fileEntry.eof = fileWriter.eof;
|
|
|
|
fileEntry.write ();
|
|
updateFileCount (fileEntry.headerPointer);
|
|
}
|
|
|
|
return fileEntry;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
private int createPath (String[] subdirectories)
|
|
throws DiskFullException, VolumeCatalogFullException
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
// search for each subdirectory, create any that don't exist
|
|
int catalogBlockNo = 2;
|
|
|
|
FileEntry fileEntry = null;
|
|
for (int i = 0; i < subdirectories.length; i++)
|
|
{
|
|
Optional<FileEntry> fileEntryOpt =
|
|
searchDirectory (catalogBlockNo, subdirectories[i]);
|
|
if (fileEntryOpt.isEmpty ())
|
|
fileEntry = createSubdirectory (catalogBlockNo, subdirectories[i]);
|
|
else
|
|
fileEntry = fileEntryOpt.get ();
|
|
|
|
catalogBlockNo = fileEntry.keyPointer;
|
|
}
|
|
|
|
return catalogBlockNo;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
public void addResourceFork (FileEntry fileEntry, byte[] fileBuffer, int eof)
|
|
throws DiskFullException
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
int blockNo = allocateNextBlock (); // allocate extended key block
|
|
|
|
FileWriter fileWriter = new FileWriter (this); // create resource fork
|
|
fileWriter.writeFile (fileBuffer, eof);
|
|
|
|
ExtendedKeyBlock extendedKeyBlock = new ExtendedKeyBlock (this, blockNo * BLOCK_SIZE);
|
|
|
|
extendedKeyBlock.addMiniEntry (ForkType.DATA, fileEntry);
|
|
extendedKeyBlock.addMiniEntry (ForkType.RESOURCE, fileWriter);
|
|
|
|
fileEntry.keyPointer = blockNo; // extended key block
|
|
fileEntry.storageType = 0x05; // extended
|
|
fileEntry.blocksUsed += fileWriter.blocksUsed + 1;
|
|
fileEntry.eof = BLOCK_SIZE;
|
|
|
|
fileEntry.write ();
|
|
extendedKeyBlock.write ();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
private boolean verify (String path)
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
// split the full path into an array of subdirectories and a file name
|
|
String[] subdirectories;
|
|
String fileName;
|
|
|
|
int pos = path.lastIndexOf ('/');
|
|
if (pos > 0)
|
|
{
|
|
subdirectories = path.substring (0, pos).split ("/");
|
|
fileName = path.substring (pos + 1);
|
|
}
|
|
else
|
|
{
|
|
subdirectories = new String[0];
|
|
fileName = path;
|
|
}
|
|
|
|
// search for each subdirectory, fail any that don't exist
|
|
int catalogBlockNo = 2;
|
|
|
|
FileEntry fileEntry = null;
|
|
for (int i = 0; i < subdirectories.length; i++)
|
|
{
|
|
Optional<FileEntry> fileEntryOpt =
|
|
searchDirectory (catalogBlockNo, subdirectories[i]);
|
|
if (fileEntryOpt.isEmpty ())
|
|
{
|
|
System.out.println ("path doesn't exist");
|
|
return false;
|
|
}
|
|
|
|
fileEntry = fileEntryOpt.get ();
|
|
|
|
catalogBlockNo = fileEntry.keyPointer;
|
|
}
|
|
|
|
// check that the file already exists
|
|
Optional<FileEntry> fileEntryOpt = searchDirectory (catalogBlockNo, fileName);
|
|
if (fileEntryOpt.isPresent ())
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
void verify ()
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
for (SubdirectoryHeader subdirectoryHeader : subdirectoryHeaders.values ())
|
|
{
|
|
FileEntry fileEntry = subdirectoryHeader.getParentFileEntry ();
|
|
if (!fileEntry.fileName.equals (subdirectoryHeader.fileName))
|
|
System.out.printf ("fail: %s%n", subdirectoryHeader.fileName);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
void display ()
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
volumeDirectoryHeader.list ();
|
|
|
|
for (SubdirectoryHeader subdirectoryHeader : subdirectoryHeaders.values ())
|
|
subdirectoryHeader.list ();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
public void close ()
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
writeVolumeBitMap ();
|
|
volumeDirectoryHeader.write ();
|
|
for (SubdirectoryHeader subdirectoryHeader : subdirectoryHeaders.values ())
|
|
subdirectoryHeader.write ();
|
|
|
|
if (false)
|
|
{
|
|
System.out.printf ("Verifying %s files%n", paths.size ());
|
|
for (String path : paths)
|
|
{
|
|
System.out.printf ("%-35s ", path);
|
|
if (!verify (path))
|
|
System.out.println ("fail");
|
|
else
|
|
System.out.println ("pass");
|
|
}
|
|
System.out.println ();
|
|
}
|
|
// verify ();
|
|
// display ();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
private Optional<FileEntry> searchDirectory (int blockNo, String fileName)
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
int emptySlotPtr = 0;
|
|
|
|
do
|
|
{
|
|
int offset = blockNo * BLOCK_SIZE;
|
|
int ptr = offset + 4;
|
|
for (int i = 0; i < ENTRIES_PER_BLOCK; i++)
|
|
{
|
|
int storageTypeNameLength = buffer[ptr] & 0xFF;
|
|
if (storageTypeNameLength == 0)
|
|
{
|
|
if (emptySlotPtr == 0)
|
|
emptySlotPtr = ptr;
|
|
}
|
|
else
|
|
{
|
|
int nameLength = buffer[ptr] & 0x0F;
|
|
int storageType = (buffer[ptr] & 0xF0) >>> 4;
|
|
|
|
if (storageType < SUBDIRECTORY_HEADER
|
|
&& fileName.equals (new String (buffer, ptr + 1, nameLength)))
|
|
{
|
|
FileEntry fileEntry = new FileEntry (this, ptr);
|
|
fileEntry.read ();
|
|
return Optional.of (fileEntry);
|
|
}
|
|
}
|
|
|
|
ptr += ENTRY_SIZE;
|
|
}
|
|
blockNo = unsignedShort (buffer, offset + 2);
|
|
} while (blockNo > 0);
|
|
|
|
return Optional.empty ();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
private FileEntry createSubdirectory (int blockNo, String name)
|
|
throws DiskFullException, VolumeCatalogFullException
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
// this will return a new, empty file entry, or throw an exception
|
|
FileEntry fileEntry = findFreeSlot (blockNo);
|
|
|
|
if (fileEntry == null) // should be impossible
|
|
{
|
|
System.out.println ("failed");
|
|
return null;
|
|
}
|
|
|
|
fileEntry.storageType = SUBDIRECTORY;
|
|
fileEntry.fileName = name;
|
|
fileEntry.keyPointer = allocateNextBlock (); // allocate block for the subdirectory header
|
|
fileEntry.blocksUsed = 1;
|
|
fileEntry.eof = BLOCK_SIZE;
|
|
fileEntry.fileType = FILE_TYPE_DIRECTORY;
|
|
fileEntry.headerPointer = blockNo;
|
|
fileEntry.creationDate = LocalDateTime.now ();
|
|
fileEntry.modifiedDate = LocalDateTime.now ();
|
|
|
|
fileEntry.write ();
|
|
|
|
updateFileCount (fileEntry.headerPointer);
|
|
|
|
SubdirectoryHeader subdirectoryHeader =
|
|
new SubdirectoryHeader (this, fileEntry.keyPointer * BLOCK_SIZE + 4);
|
|
|
|
subdirectoryHeader.fileName = name;
|
|
subdirectoryHeader.creationDate = LocalDateTime.now ();
|
|
subdirectoryHeader.fileCount = 0;
|
|
subdirectoryHeader.setParentDetails (fileEntry);
|
|
|
|
subdirectoryHeader.write ();
|
|
|
|
subdirectoryHeaders.put (fileEntry.keyPointer, subdirectoryHeader);
|
|
|
|
return fileEntry;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
private void updateFileCount (int catalogBlock)
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
if (catalogBlock == 2)
|
|
{
|
|
volumeDirectoryHeader.fileCount++;
|
|
volumeDirectoryHeader.write ();
|
|
}
|
|
else
|
|
{
|
|
SubdirectoryHeader subdirectoryHeader = subdirectoryHeaders.get (catalogBlock);
|
|
subdirectoryHeader.fileCount++;
|
|
subdirectoryHeader.write ();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
int allocateNextBlock () throws DiskFullException
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
int nextBlock = volumeBitMap.nextSetBit (0);
|
|
if (nextBlock < 0)
|
|
throw new DiskFullException ("Disk Full");
|
|
|
|
volumeBitMap.set (nextBlock, false); // mark as unavailable
|
|
|
|
return nextBlock;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
private FileEntry findFreeSlot (int blockNo)
|
|
throws DiskFullException, VolumeCatalogFullException
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
// check for Volume Directory Header full
|
|
if (blockNo == 2 && volumeDirectoryHeader.fileCount == 51)
|
|
throw new VolumeCatalogFullException ("Volume Directory is full"); // stupid
|
|
|
|
// get the subdirectory header before the blockNo can change
|
|
SubdirectoryHeader subdirectoryHeader = subdirectoryHeaders.get (blockNo);
|
|
|
|
int lastBlockNo = 0; // used for linking directory blocks
|
|
|
|
do
|
|
{
|
|
int offset = blockNo * BLOCK_SIZE;
|
|
int ptr = offset + 4;
|
|
|
|
for (int i = 0; i < ENTRIES_PER_BLOCK; i++)
|
|
{
|
|
if (buffer[ptr] == 0) // free slot
|
|
return new FileEntry (this, ptr);
|
|
|
|
ptr += ENTRY_SIZE;
|
|
}
|
|
|
|
lastBlockNo = blockNo;
|
|
blockNo = unsignedShort (buffer, offset + 2); // next block
|
|
} while (blockNo > 0);
|
|
|
|
if (subdirectoryHeader == null) // this should be impossible
|
|
throw new VolumeCatalogFullException ("Volume Directory is full");
|
|
|
|
// no free slots, so add a new catalog block
|
|
blockNo = allocateNextBlock ();
|
|
|
|
// update links
|
|
int ptr = blockNo * BLOCK_SIZE;
|
|
writeShort (buffer, lastBlockNo * BLOCK_SIZE + 2, blockNo); // point to next block
|
|
writeShort (buffer, ptr, lastBlockNo); // point to previous block
|
|
|
|
// update parent's file entry size (this is the subdirectory file entry
|
|
subdirectoryHeader.updateParentFileEntry ();
|
|
|
|
return new FileEntry (this, ptr + 4); // first slot in new block
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
private void writeVolumeBitMap ()
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
int ptr = (2 + CATALOG_SIZE) * BLOCK_SIZE;
|
|
int val = 0;
|
|
int blockNo = 0;
|
|
|
|
while (blockNo < maxBlocks)
|
|
{
|
|
val = val << 1;
|
|
if (volumeBitMap.get (blockNo++))
|
|
val |= 1;
|
|
|
|
if (blockNo % 8 == 0)
|
|
{
|
|
buffer[ptr++] = (byte) val;
|
|
val = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
public byte[] getBuffer ()
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
return buffer;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------//
|
|
@Override
|
|
public String toString ()
|
|
// ---------------------------------------------------------------------------------//
|
|
{
|
|
StringBuilder text = new StringBuilder ();
|
|
|
|
text.append (volumeDirectoryHeader);
|
|
text.append ("\n");
|
|
|
|
for (SubdirectoryHeader subdirectoryHeader : subdirectoryHeaders.values ())
|
|
{
|
|
text.append (subdirectoryHeader);
|
|
text.append ("\n");
|
|
text.append (subdirectoryHeader.getParentFileEntry ());
|
|
text.append ("\n");
|
|
}
|
|
|
|
return text.toString ();
|
|
}
|
|
}
|