Merge pull request #150 from AppleCommander/feature/pascal-fixups

Feature/pascal fixups
This commit is contained in:
A2 Geek authored and GitHub committed 2025-07-28 14:51:50 -05:00
commit 99e1015caf
15 files changed
+146 -133

No files matched your search

@@ -19,18 +19,6 @@
*/
package io.github.applecommander.acx.command;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Function;
import java.util.logging.Logger;
import com.webcodepro.applecommander.storage.DirectoryEntry;
import com.webcodepro.applecommander.storage.FileEntry;
import com.webcodepro.applecommander.storage.os.prodos.ProdosFormatDisk;
@@ -42,7 +30,6 @@ import com.webcodepro.applecommander.util.readerwriter.OverrideFileEntryReader;
import com.webcodepro.shrinkit.HeaderBlock;
import com.webcodepro.shrinkit.NuFileArchive;
import com.webcodepro.shrinkit.ThreadRecord;
import io.github.applecommander.acx.base.ReadWriteDiskCommandOptions;
import io.github.applecommander.acx.converter.IntegerTypeConverter;
import io.github.applecommander.acx.fileutil.FileUtils;
@@ -60,6 +47,13 @@ import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Function;
import java.util.logging.Logger;
@Command(name = "import", description = "Import file onto disk.",
aliases = { "put" })
public class ImportCommand extends ReadWriteDiskCommandOptions {
@@ -116,7 +110,7 @@ public class ImportCommand extends ReadWriteDiskCommandOptions {
new IOException(String.format("Directory '%s' not found.", dir)));
}
}
FileUtils copier = new FileUtils(overwriteFlag);
FileEntryReader inputReader = inputData.get();
for (FileEntryReader processorReader : processor.apply(inputReader)) {
@@ -161,7 +155,7 @@ public class ImportCommand extends ReadWriteDiskCommandOptions {
byte[] data = Files.readAllBytes(path);
fileEntryReader = OverrideFileEntryReader.builder()
.fileData(data)
.filename(filename)
.filename(path.getFileName().toString())
.prodosFiletype("BIN")
.build();
} catch (IOException e) {
@@ -365,7 +359,7 @@ public class ImportCommand extends ReadWriteDiskCommandOptions {
}
public static class Overrides {
@Option(names = { "-t", "--type" }, description = "ProDOS File type. "
@Option(names = { "-t", "--type" }, description = "ProDOS or native file type. "
+ "(Each filesystem translates between it's native types and ProDOS.)")
private Optional<String> fileType;
@@ -85,7 +85,7 @@ public class FileUtils {
}
public void copyFile(DirectoryEntry directory, FileEntryReader source) throws DiskException {
String sourceName = source.getFilename().get();
String sourceName = source.getFilename().orElseThrow();
String sanitizedName = directory.getFormattedDisk().getSuggestedFilename(sourceName);
final Optional<FileEntry> fileEntry = directory.getFiles().stream()
.filter(entry -> entry.getFilename().equals(sanitizedName))
@@ -415,9 +415,11 @@ public abstract class FormattedDisk extends Disk implements DirectoryEntry {
/**
* Provides conversation from a given ProDOS file type since as it is common across
* many archiving tools.
* many archiving tools. This should also allow "native" filetypes to just pass-through.
* (For example, a "B" in a DOS disk should just return a "B" while a "BAS" would be
* transformed to a "B" filetype.)
*/
public abstract String fromProdosFiletype(String prodosFiletype);
public abstract String toNativeFiletype(String prodosFiletype);
/**
* Provides conversation to a given ProDOS file type since as it is common across
* many archiving tools.
@@ -562,7 +562,7 @@ public class CpmFormatDisk extends FormattedDisk {
* many archiving tools.
*/
@Override
public String fromProdosFiletype(String prodosFiletype) {
public String toNativeFiletype(String prodosFiletype) {
if ("TXT".equalsIgnoreCase(prodosFiletype)) {
return "TXT";
}
@@ -788,7 +788,11 @@ public class DosFormatDisk extends FormattedDisk {
* many archiving tools.
*/
@Override
public String fromProdosFiletype(String prodosFiletype) {
public String toNativeFiletype(String prodosFiletype) {
if (FILE_TYPE_MAPPING.containsKey(prodosFiletype)) {
// This is really a DOS file type. Pass it back.
return prodosFiletype;
}
return FILE_TYPE_MAPPING.entrySet()
.stream()
.filter(e -> e.getValue().equals(prodosFiletype))
@@ -708,7 +708,7 @@ public class GutenbergFormatDisk extends FormattedDisk {
* many archiving tools.
*/
@Override
public String fromProdosFiletype(String prodosFiletype) {
public String toNativeFiletype(String prodosFiletype) {
return "T";
}
/**
@@ -546,7 +546,7 @@ public class NakedosFormatDisk extends FormattedDisk {
* many archiving tools.
*/
@Override
public String fromProdosFiletype(String prodosFiletype) {
public String toNativeFiletype(String prodosFiletype) {
return "B";
}
/**
@@ -22,6 +22,8 @@
package com.webcodepro.applecommander.storage.os.pascal;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -59,12 +61,35 @@ public class PascalFileEntry implements FileEntry {
/**
* Constructor for PascalFileEntry.
*/
public PascalFileEntry(byte[] fileEntry, PascalFormatDisk disk) {
public PascalFileEntry(byte[] fileEntry, int index, PascalFormatDisk disk) {
super();
this.fileEntry = fileEntry;
this.index = index;
this.disk = disk;
}
/**
* Write the fileEntry data to the disk image.
*/
protected void writeFileEntry() {
if (this.deleted) {
return; // prevents trying to save a deleted file (not a reality in Pascal filesystem)
}
List<PascalFileEntry> dir = disk.getDirectory();
if (this.index < dir.size()) {
dir.set(this.index, this);
}
else if (this.index == dir.size()) {
dir.add(this.index, this);
}
else {
throw new RuntimeException(textBundle.format("PascalFormatDisk.UnexpectedDirectoryIndex", this.index, dir.size()));
}
// Since every "set" triggers a write, the file count tends to get whacked; this makes it sane.
dir.get(0).setFileCount(dir.size()-1);
disk.putDirectory(dir);
}
/**
* Get the block number of the file's 1st block.
*/
@@ -77,6 +102,7 @@ public class PascalFileEntry implements FileEntry {
*/
public void setFirstBlock(int first) {
AppleUtil.setWordValue(fileEntry, 0, first);
writeFileEntry();
}
/**
@@ -91,6 +117,7 @@ public class PascalFileEntry implements FileEntry {
*/
public void setLastBlock(int last) {
AppleUtil.setWordValue(fileEntry, 2, last);
writeFileEntry();
}
/**
@@ -105,6 +132,7 @@ public class PascalFileEntry implements FileEntry {
*/
public void setFilename(String filename) {
AppleUtil.setPascalString(fileEntry, 6, filename.toUpperCase(), 15);
writeFileEntry();
}
/**
@@ -147,6 +175,7 @@ public class PascalFileEntry implements FileEntry {
} else {
AppleUtil.setWordValue(fileEntry, 4, 0);
}
writeFileEntry();
}
/**
@@ -175,6 +204,7 @@ public class PascalFileEntry implements FileEntry {
*/
public void setBytesUsedInLastBlock(int value) {
AppleUtil.setWordValue(fileEntry, 22, value);
writeFileEntry();
}
/**
@@ -235,7 +265,7 @@ public class PascalFileEntry implements FileEntry {
if (index != 0) {
dir.remove(index);
PascalFileEntry volEntry = (PascalFileEntry) dir.get(0);
volEntry.setFileCount(count - 2); // inlcudes the volume entry
volEntry.setFileCount(count - 2); // includes the volume entry
dir.set(0, volEntry);
disk.putDirectory(dir);
deleted = true;
@@ -254,6 +284,7 @@ public class PascalFileEntry implements FileEntry {
*/
public void setModificationDate(Date date) {
AppleUtil.setPascalDate(fileEntry, 24, date);
writeFileEntry();
}
/**
@@ -356,102 +387,90 @@ public class PascalFileEntry implements FileEntry {
}
/**
* Find index of last CR < 1023 bytes from offset.
* @author John B. Matthews
* Convert the given text file to conform to the expected Pascal text file format.
* <p/>
* Initial header page of 1024 bytes (left as zeros), followed by 1024-byte chunks
* that terminate on CR, with all remaining bytes being zero.
*/
private int findEOL(byte[] data, int offset) throws DiskFullException {
int i = offset + 1022;
while (i > offset) {
if (data[i] == 13) {
return i;
private byte[] convertTextToBlocks(byte[] text) throws DiskFullException {
try {
byte[] chunk = new byte[1024];
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(chunk);
int offset = 0;
while (offset < text.length) {
Arrays.fill(chunk, (byte) 0);
int size = 0;
for (int i=0; i<Math.min(chunk.length,text.length-offset); i++) {
if (text[offset+i] == 13) {
size = i+1;
}
}
if (size == 0) {
size = text.length-offset;
if (size >= 1024) {
storageError(textBundle.get("PascalFileEntry.LineLengthError"));
}
}
System.arraycopy(text, offset, chunk, 0, size);
out.write(chunk);
offset += size;
}
i--;
return out.toByteArray();
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
storageError(textBundle.get("PascalFileEntry.LineLengthError")); //$NON-NLS-1$
return 0;
}
/**
* Set file data for this file entry. Because the directory entry may
* have been changed, use this.index to determine which entry to update.
* author John B. Matthews.
* @see #setEntryIndex
* @see PascalFormatDisk#createFile
*/
public void setFileData(byte[] data) throws DiskFullException {
int first = getFirstBlock();
int last = getLastBlock();
// Prepare a text file for writing since it has a special format
if (fileEntry[4] == 3) { // text
data = filterText(data);
byte[] buf1 = new byte[512];
byte[] buf2 = new byte[512];
int offset = 0;
int pages = 0;
disk.writeBlock(first, buf1); // First two blocks (first page) is ignored by Pascal text
disk.writeBlock(first+1,buf2); // ...so write a page of zeroes
pages++;
while (offset + 1023 < data.length) { // We have at least one full page of data (1024 bytes)
if ((pages * 2) > (last - first - 2)) {
storageError(textBundle.get("PascalFileEntry.NotEnoughRoom")); //$NON-NLS-1$
}
int crPtr = findEOL(data, offset); // Copy to last CR & we'll zero-pad to end of page
System.arraycopy(data, offset, buf1, 0, 512);
System.arraycopy(data, offset+512, buf2, 0, crPtr - offset + 1 - 512);
disk.writeBlock(first + (pages * 2), buf1);
disk.writeBlock(first + (pages * 2) + 1, buf2);
pages++;
Arrays.fill(buf1, (byte) 0);
Arrays.fill(buf2, (byte) 0);
offset = crPtr + 1;
}
if (offset < data.length) { // We have less than a full page of data left over
int len1 = data.length - offset;
int len2 = 0;
if (len1 > 512) { // That final page spans both blocks
len2 = len1 - 512; // Second block gets the remainder of the partial page length minus 512 bytes
len1 = 512; // The first block will write the first 512 bytes
}
System.arraycopy(data, offset, buf1, 0, len1);
disk.writeBlock(first + (pages * 2), buf1); // Copy out the first block
if (len2 > 0) {
System.arraycopy(data, offset+512, buf2, 0, len2);
}
len2 = 512; // TEXT files always pad out to 1KB
disk.writeBlock(first + (pages * 2) + 1, buf2); // Copy out second block
setBytesUsedInLastBlock(len2); // The second block holds the last byte
setLastBlock(first + (pages * 2) + 2); // Final block +1... i.e. pages++
} else { // The last page was completely full, so the last byte used in the last block is 512
setLastBlock(first + pages * 2);
setBytesUsedInLastBlock(512);
}
data = convertTextToBlocks(data);
}
} else { // data or code
if (data.length > (last - first) * 512) {
// Write file data
int first = getFirstBlock();
int last = getLastBlock();
int requiredBlocks = (data.length + 511) / 512;
if (data.length > (last - first) * 512) {
// we won't fit into the existing allocated space, time to recompute
first = 0;
for (PascalFileEntry fe : disk.getDirectory()) {
if (fe.getFirstBlock() != this.getFirstBlock()) { // ignore this file
first = Math.max(first, fe.getLastBlock());
}
}
last = first+requiredBlocks;
if (first == 0 || last >= disk.getBlocksOnDisk()) {
// this throws an exception
storageError(textBundle.get("PascalFileEntry.NotEnoughRoom")); //$NON-NLS-1$
}
byte[] buf = new byte[512];
int blocks = data.length / 512;
int bytes = data.length % 512;
for (int i = 0; i < blocks; i++) {
System.arraycopy(data, i * 512, buf, 0, 512);
disk.writeBlock(first + i, buf);
}
if (bytes > 0) {
Arrays.fill(buf, (byte) 0);
System.arraycopy(data, blocks * 512, buf, 0, bytes);
disk.writeBlock(first + blocks, buf);
setLastBlock(first + blocks + 1);
setBytesUsedInLastBlock(bytes);
} else {
setLastBlock(first + blocks);
setBytesUsedInLastBlock(512);
}
}
// update this directory entry
if (this.index > 0) {
List<PascalFileEntry> dir = disk.getDirectory();
dir.set(this.index, this);
disk.putDirectory(dir);
byte[] buf = new byte[512];
int blocks = data.length / 512;
int bytes = data.length % 512;
for (int i = 0; i < blocks; i++) {
System.arraycopy(data, i * 512, buf, 0, 512);
disk.writeBlock(first + i, buf);
}
if (bytes > 0) {
Arrays.fill(buf, (byte) 0);
System.arraycopy(data, blocks * 512, buf, 0, bytes);
disk.writeBlock(first + blocks, buf);
setFirstBlock(first);
setLastBlock(first + blocks + 1);
setBytesUsedInLastBlock(bytes);
} else {
setFirstBlock(first);
setLastBlock(first + blocks);
setBytesUsedInLastBlock(512);
}
}
@@ -522,16 +541,6 @@ public class PascalFileEntry implements FileEntry {
return false;
}
/**
* Remember the index of a newly created file entry.
* Required to update the entry after setFileData,
* which may change any or all of the new entry's fields.
* author John B. Matthews
*/
public void setEntryIndex(int index) {
this.index = index;
}
/**
* Set the file count in a volume entry.
* Use only on the volume entry: dir.get(0).
@@ -69,11 +69,7 @@ public class PascalFormatDisk extends FormattedDisk {
DATAFILE, "BIN",
"GRAF", "BIN",
"FOTO", "BIN",
"securedir", "BIN",
// Prodos => Pascal
"BIN", DATAFILE,
"TXT", TEXTFILE
"securedir", "BIN"
);
/**
@@ -156,7 +152,8 @@ public class PascalFormatDisk extends FormattedDisk {
for (int i=0; i<count; i++) {
byte[] entry = new byte[entrySize];
System.arraycopy(directory, offset, entry, 0, entry.length);
list.add(new PascalFileEntry(entry, this));
// we skipped the "volume" entry, so the actual index value is +1:
list.add(new PascalFileEntry(entry, i+1, this));
offset+= entrySize;
}
return list;
@@ -173,7 +170,7 @@ public class PascalFormatDisk extends FormattedDisk {
for (int i = 0; i <= count; i++) {
byte[] entry = new byte[ENTRY_SIZE];
System.arraycopy(directory, offset, entry, 0, entry.length);
list.add(new PascalFileEntry(entry, this));
list.add(new PascalFileEntry(entry, i,this));
offset += ENTRY_SIZE;
}
return list;
@@ -223,7 +220,7 @@ public class PascalFormatDisk extends FormattedDisk {
volEntry.setFileCount(count);
dir.set(0, volEntry);
// add new entry to list
dir.add(index, new PascalFileEntry(new byte[ENTRY_SIZE], this));
dir.add(index, new PascalFileEntry(new byte[ENTRY_SIZE], index, this));
PascalFileEntry entry = (PascalFileEntry) dir.get(index);
// fill in plausible values; will rely index, first and last
first = ((PascalFileEntry) dir.get(index - 1)).getLastBlock();
@@ -233,10 +230,7 @@ public class PascalFormatDisk extends FormattedDisk {
entry.setFilename("x"); //$NON-NLS-1$
entry.setBytesUsedInLastBlock(512);
entry.setModificationDate(new Date());
entry.setEntryIndex(index);
dir.set(index, entry);
// write it back to disk
putDirectory(dir);
// Note that each "set" does an implicit write
return entry;
} else {
throw new DiskFullException(
@@ -689,8 +683,17 @@ public class PascalFormatDisk extends FormattedDisk {
* many archiving tools.
*/
@Override
public String fromProdosFiletype(String prodosFiletype) {
return FILE_TYPE_MAP.getOrDefault(prodosFiletype, DATAFILE);
public String toNativeFiletype(String prodosFiletype) {
// Note: Can't use the map due to the 1:N relationship, so this is somewhat manual.
for (String pascalFiletype : FILE_TYPES) {
if (prodosFiletype.equalsIgnoreCase(pascalFiletype)) {
return pascalFiletype;
}
}
if ("TXT".equalsIgnoreCase(prodosFiletype)) {
return TEXTFILE;
}
return DATAFILE;
}
/**
* Provides conversation to a given ProDOS file type since as it is common across
@@ -1463,7 +1463,7 @@ public class ProdosFormatDisk extends FormattedDisk {
* many archiving tools.
*/
@Override
public String fromProdosFiletype(String prodosFiletype) {
public String toNativeFiletype(String prodosFiletype) {
return prodosFiletype;
};
/**
@@ -577,10 +577,10 @@ public class RdosFormatDisk extends FormattedDisk {
* many archiving tools.
*/
@Override
public String fromProdosFiletype(String prodosFiletype) {
public String toNativeFiletype(String prodosFiletype) {
return FILE_TYPE_MAPPING.entrySet()
.stream()
.filter(e -> e.getValue().equals(prodosFiletype))
.filter(e -> prodosFiletype.equalsIgnoreCase(e.getKey()) || prodosFiletype.equalsIgnoreCase(e.getValue()))
.map(Map.Entry::getKey)
.findFirst()
.orElse("B");
@@ -52,7 +52,7 @@ public class DosFileEntryReaderWriter implements FileEntryReader, FileEntryWrite
}
@Override
public void setProdosFiletype(String filetype) {
String dosFiletype = fileEntry.getFormattedDisk().fromProdosFiletype(filetype);
String dosFiletype = fileEntry.getFormattedDisk().toNativeFiletype(filetype);
fileEntry.setFiletype(dosFiletype);
}
@@ -50,7 +50,7 @@ public class PascalFileEntryReaderWriter implements FileEntryReader, FileEntryWr
}
@Override
public void setProdosFiletype(String filetype) {
fileEntry.setFiletype(fileEntry.getFormattedDisk().fromProdosFiletype(filetype));
fileEntry.setFiletype(fileEntry.getFormattedDisk().toNativeFiletype(filetype));
}
@Override
@@ -135,6 +135,7 @@ PascalFormatDisk.LastBlock=Last Block
PascalFormatDisk.VolumeDate=Volume Date
PascalFormatDisk.IncorrectFileEntryError=Must have a Pascal file entry\!
PascalFormatDisk.DiskFull=Disk full.
PascalFormatDisk.UnexpectedDirectoryIndex=Pascal file entry out of expected directory bounds. Index {0} expected to be in range 0..{1}+1.
# PascalFileEntry
PascalFileEntry.UnknownFiletype=unknown ({0})
@@ -211,7 +211,7 @@ public class ImportSelectFilesWizardPane extends WizardPane {
AppleSingle as = AppleSingle.read(data);
suggestedFilename = Optional.ofNullable(as.getRealName())
.orElse(suggestedFilename);
suggestedFiletype = wizard.getDisk().fromProdosFiletype(
suggestedFiletype = wizard.getDisk().toNativeFiletype(
ProdosFormatDisk.getFiletype(as.getProdosFileInfo().getFileType()));
suggestedAddress = Optional.ofNullable(as.getProdosFileInfo())
.map(ProdosFileInfo::getAuxType)