Merging in 'acx' tool, merging gradle builds

This commit is contained in:
Rob Greene 2022-01-03 18:24:39 -06:00
parent 7469d90c8a
commit acf3874185
61 changed files with 3196 additions and 7 deletions

150
app/ac-acx/README.md Normal file
View File

@ -0,0 +1,150 @@
# `ac`, extended
This is a revamp of the venerable `ac` command-line utility with a more modern command-line interface.
# Sample Run
All of the commands use `-d` to designate the disk. If there are many commands, setting the environment variable `ACX_DISK_NAME` can be used to simplify commands.
For example, this sequence:
1. Creates a ProDOS disk image (pulling ProDOS from the 2.4.2 system master).
2. Imports and tokenizes the sample `startup.bas` program and places it on the disk as `STARTUP`.
3. Lists the resulting disk.
```
$ cat > startup.bas
10 TEXT:HOME:GR
20 FOR Y=0 TO 3
30 FOR X=0 TO 3
40 COLOR=Y*4+X
50 FOR A=0 TO 9
60 HLIN X*10,X*10+9 AT Y*10+A
70 NEXT A,X,Y
80 END
<ctrl+D>
$ export ACX_DISK_NAME=sample.po
$ acx create --format=ProDOS_2_4_2.dsk --name=SAMPLES --size=140k --type=prodos
$ acx import --basic startup.bas --name=STARTUP
$ acx list --native
File: sample.po
Name: /SAMPLES/
* PRODOS SYS 035 01/02/2022 01/13/2018 17,128
* BASIC.SYSTEM SYS 021 01/02/2022 01/13/2018 10,240 A=$2000
STARTUP BAS 001 01/02/2022 01/02/2022 97 A=$0801
ProDOS format; 110592 bytes free; 32768 bytes used.
```
# Usage
```
$ acx --help
Usage: acx [-hVv] [--debug] [--quiet] [COMMAND]
'ac' experimental utility
Options:
--debug Show detailed stack traces.
-h, --help Show this help message and exit.
--quiet Turn off all logging.
-v, --verbose Be verbose. Multiple occurrences increase logging.
-V, --version Print version information and exit.
Commands:
convert Uncompress a ShrinkIt or Binary II file;
copy, cp Copy files between disks.
create, mkdisk Rename volume of a disk image.
delete, del, rm Delete file(s) from a disk image.
diskmap, map Show disk usage map.
export, x, get Export file(s) from a disk image.
help Displays help information about the specified command
import, put Import file onto disk.
info, i Show information on a disk image(s).
list, ls List directory of disk image(s).
lock Lock file(s) on a disk image.
mkdir, md Create a directory on disk.
rename, ren Rename file on a disk image.
rename-disk Rename volume of a disk image.
rmdir, rd Remove a directory on disk.
unlock Unlock file(s) on a disk image.
```
## Info
```
$ acx info --help
Usage: acx info [-h] <paths>...
Show information on a disk image(s).
Parameters:
<paths>... Image(s) to process.
Options:
-h, --help Show help for subcommand.
```
```
$ acx info "Beagle Graphics.dsk"
File Name: Beagle Graphics.dsk
Disk Name: DISK VOLUME #254
Physical Size (bytes): 143360
Free Space (bytes): 20480
Used Space (bytes): 122880
Physical Size (KB): 140
Free Space (KB): 20
Used Space (KB): 120
Archive Order: DOS
Disk Format: DOS 3.3
Total Sectors: 560
Free Sectors: 80
Used Sectors: 480
Tracks On Disk: 35
Sectors On Disk: 16
```
## List
```
$ acx list --help
Usage: acx list [-hr] [--[no-]column] [--deleted] [--[no-]footer] [--[no-]
header] [--globs=<globs>[,<globs>...]]... [-n | -s | -l]
[--file | --directory] <paths>...
List directory of disk image(s).
Parameters:
<paths>... Image(s) to process.
Options:
--[no-]column Show column headers.
--deleted Show deleted files.
--directory Only include directories.
--file Only include files.
--[no-]footer Show footer.
--globs=<globs>[,<globs>...]
File glob(s) to match.
-h, --help Show help for subcommand.
--[no-]header Show header.
-r, --[no-]recursive Display directory recursively.
File display formatting:
-l, --long, --detail Use long/detailed directory format.
-n, --native Use native directory format (default).
-s, --short, --standard Use brief directory format.
```
```
$ acx list --no-recursive DEVCD.HDV
File: DEVCD.HDV
Name: /DEV.CD/
TOOLS DIR 002 06/25/1990 04/13/1989 1,024
II.DISK.CENTRAL DIR 001 06/25/1990 04/13/1989 512
UTILITIES DIR 002 06/25/1990 04/13/1989 1,024
READ.ME.FIRST DIR 001 06/25/1990 04/21/1989 512
GUIDED.TOURS DIR 001 06/25/1990 04/13/1989 512
FINDER.DATA FND 001 06/25/1990 10/12/1989 172
DEVELOP DIR 001 07/05/1990 06/25/1990 512
ProDOS format; 1701376 bytes free; 19270144 bytes used.
```

36
app/ac-acx/build.gradle Normal file
View File

@ -0,0 +1,36 @@
plugins {
id 'org.springframework.boot' version "$springBoot"
id 'java'
id 'application'
}
sourceCompatibility = 11
targetCompatibility = 11
repositories {
mavenCentral()
}
dependencies {
implementation "info.picocli:picocli:$picocliVersion"
implementation project(':lib:ac-api')
implementation "net.sf.applecommander:ShrinkItArchive:$shkVersion"
implementation "net.sf.applecommander:applesingle-api:$asVersion"
implementation "net.sf.applecommander:bastools-api:$btVersion"
testImplementation "junit:junit:$junitVersion"
}
application {
mainClass = 'io.github.applecommander.acx.Main'
}
bootJar {
archiveBaseName = 'AppleCommander'
archiveAppendix = 'acx'
manifest {
attributes 'Implementation-Title': "AppleCommander 'acx'",
'Implementation-Version': archiveVersion
}
from('../../LICENSE')
}

Binary file not shown.

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -0,0 +1,101 @@
package io.github.applecommander.acx;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import io.github.applecommander.acx.command.ConvertCommand;
import io.github.applecommander.acx.command.CopyFileCommand;
import io.github.applecommander.acx.command.CreateDiskCommand;
import io.github.applecommander.acx.command.DeleteCommand;
import io.github.applecommander.acx.command.DiskMapCommand;
import io.github.applecommander.acx.command.ExportCommand;
import io.github.applecommander.acx.command.ImportCommand;
import io.github.applecommander.acx.command.InfoCommand;
import io.github.applecommander.acx.command.ListCommand;
import io.github.applecommander.acx.command.LockCommand;
import io.github.applecommander.acx.command.MkdirCommand;
import io.github.applecommander.acx.command.RenameDiskCommand;
import io.github.applecommander.acx.command.RenameFileCommand;
import io.github.applecommander.acx.command.RmdirCommand;
import io.github.applecommander.acx.command.UnlockCommand;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.HelpCommand;
import picocli.CommandLine.Option;
/**
* Primary entry point into the 'acx' utility.
*/
@Command(name = "acx", mixinStandardHelpOptions = true, versionProvider = VersionProvider.class,
descriptionHeading = "%n",
commandListHeading = "%nCommands:%n",
optionListHeading = "%nOptions:%n",
description = "'ac' experimental utility",
subcommands = {
ConvertCommand.class,
CopyFileCommand.class,
CreateDiskCommand.class,
DeleteCommand.class,
DiskMapCommand.class,
ExportCommand.class,
HelpCommand.class,
ImportCommand.class,
InfoCommand.class,
ListCommand.class,
LockCommand.class,
MkdirCommand.class,
RenameFileCommand.class,
RenameDiskCommand.class,
RmdirCommand.class,
UnlockCommand.class
})
public class Main {
private static Logger LOG = Logger.getLogger(Main.class.getName());
private static final Level LOG_LEVELS[] = { Level.OFF, Level.SEVERE, Level.WARNING, Level.INFO,
Level.CONFIG, Level.FINE, Level.FINER, Level.FINEST };
static {
System.setProperty("java.util.logging.SimpleFormatter.format", "%4$s: %5$s%n");
setAllLogLevels(Level.WARNING);
}
private static void setAllLogLevels(Level level) {
Logger rootLogger = LogManager.getLogManager().getLogger("");
rootLogger.setLevel(level);
for (Handler handler : rootLogger.getHandlers()) {
handler.setLevel(level);
}
}
// This flag is read in PrintExceptionMessageHandler.
@Option(names = { "--debug" }, description = "Show detailed stack traces.")
static boolean enableStackTrace;
@Option(names = { "-v", "--verbose" }, description = "Be verbose. Multiple occurrences increase logging.")
public void setVerbosity(boolean[] flag) {
// The "+ 2" is due to the default of the levels
int loglevel = Math.min(flag.length + 2, LOG_LEVELS.length);
Level level = LOG_LEVELS[loglevel-1];
setAllLogLevels(level);
}
@Option(names = { "--quiet" }, description = "Turn off all logging.")
public void setQuiet(boolean flag) {
setAllLogLevels(Level.OFF);
}
public static void main(String[] args) {
CommandLine cmd = new CommandLine(new Main());
cmd.setExecutionExceptionHandler(new PrintExceptionMessageHandler());
if (args.length == 0) {
cmd.usage(System.out);
System.exit(1);
}
LOG.info(() -> String.format("Log level set to %s.", Logger.getGlobal().getLevel()));
int exitCode = cmd.execute(args);
LOG.fine("Exiting with code " + exitCode);
System.exit(exitCode);
}
}

View File

@ -0,0 +1,25 @@
package io.github.applecommander.acx;
import picocli.CommandLine;
import picocli.CommandLine.IExecutionExceptionHandler;
import picocli.CommandLine.ParseResult;
// Note: Taken from https://picocli.info/#_business_logic_exceptions
public class PrintExceptionMessageHandler implements IExecutionExceptionHandler {
public int handleExecutionException(Exception ex,
CommandLine cmd,
ParseResult parseResult) {
if (Main.enableStackTrace) {
ex.printStackTrace(System.err);
}
else {
// bold red error message
cmd.getErr().println(cmd.getColorScheme().errorText(ex.getMessage()));
}
return cmd.getExitCodeExceptionMapper() != null
? cmd.getExitCodeExceptionMapper().getExitCode(ex)
: cmd.getCommandSpec().exitCodeOnExecutionException();
}
}

View File

@ -0,0 +1,96 @@
package io.github.applecommander.acx;
import java.util.Arrays;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.logging.Logger;
import com.webcodepro.applecommander.storage.DiskException;
import com.webcodepro.applecommander.storage.FileEntry;
import com.webcodepro.applecommander.storage.FormattedDisk;
import com.webcodepro.applecommander.storage.os.dos33.DosFormatDisk;
import com.webcodepro.applecommander.storage.physical.ByteArrayImageLayout;
import com.webcodepro.applecommander.storage.physical.DosOrder;
import com.webcodepro.applecommander.storage.physical.ImageOrder;
import com.webcodepro.applecommander.storage.physical.ProdosOrder;
import io.github.applecommander.acx.converter.DataSizeConverter;
import io.github.applecommander.acx.fileutil.FileUtils;
public enum SystemType {
DOS(SystemType::createDosImageOrder, SystemType::copyDosSystemTracks),
OZDOS(SystemType::create800kDosImageOrder, SystemType::copyDosSystemTracks),
UNIDOS(SystemType::create800kDosImageOrder, SystemType::copyDosSystemTracks),
PRODOS(SystemType::createProdosImageOrder, SystemType::copyProdosSystemFiles),
PASCAL(SystemType::createProdosImageOrder, SystemType::copyPascalSystemFiles);
private static Logger LOG = Logger.getLogger(SystemType.class.getName());
private Function<Integer,ImageOrder> createImageOrderFn;
private BiConsumer<FormattedDisk,FormattedDisk> copySystemFn;
private SystemType(Function<Integer,ImageOrder> createImageOrderFn,
BiConsumer<FormattedDisk,FormattedDisk> copySystemFn) {
this.createImageOrderFn = createImageOrderFn;
this.copySystemFn = copySystemFn;
}
public ImageOrder createImageOrder(int size) {
return createImageOrderFn.apply(size);
}
public void copySystem(FormattedDisk target, FormattedDisk source) {
copySystemFn.accept(target, source);
}
private static ImageOrder createDosImageOrder(int size) {
ByteArrayImageLayout layout = new ByteArrayImageLayout(new byte[size]);
return new DosOrder(layout);
}
private static ImageOrder create800kDosImageOrder(int size) {
if (size != 800 * DataSizeConverter.KB) {
LOG.warning("Setting image size to 800KB.");
}
ByteArrayImageLayout layout = new ByteArrayImageLayout(new byte[800 * DataSizeConverter.KB]);
return new DosOrder(layout);
}
private static ImageOrder createProdosImageOrder(int size) {
ByteArrayImageLayout layout = new ByteArrayImageLayout(size);
return new ProdosOrder(layout);
}
private static void copyDosSystemTracks(FormattedDisk targetDisk, FormattedDisk source) {
DosFormatDisk target = (DosFormatDisk)targetDisk;
// FIXME messing with the VTOC should be handled elsewhere
byte[] vtoc = source.readSector(DosFormatDisk.CATALOG_TRACK, DosFormatDisk.VTOC_SECTOR);
int sectorsPerTrack = vtoc[0x35];
// Note that this also patches T0 S0 for BOOT0
for (int t=0; t<3; t++) {
for (int s=0; s<sectorsPerTrack; s++) {
target.writeSector(t, s, source.readSector(t, s));
target.setSectorUsed(t, s, vtoc);
}
}
}
private static void copyProdosSystemFiles(FormattedDisk target, FormattedDisk source) {
// We need to explicitly fix the boot block
target.writeBlock(0, source.readBlock(0));
target.writeBlock(1, source.readBlock(1));
try {
FileUtils copier = new FileUtils(false);
for (String filename : Arrays.asList("PRODOS", "BASIC.SYSTEM")) {
FileEntry sourceFile = source.getFile(filename);
copier.copy(target, sourceFile);
}
} catch (DiskException e) {
throw new RuntimeException(e);
}
}
private static void copyPascalSystemFiles(FormattedDisk target, FormattedDisk source) {
// We need to explicitly fix the boot block
target.writeBlock(0, source.readBlock(0));
target.writeBlock(1, source.readBlock(1));
// TODO; uncertain what files Pascal disks require for booting
}
}

View File

@ -0,0 +1,17 @@
package io.github.applecommander.acx;
import com.webcodepro.applecommander.ui.AppleCommander;
import io.github.applecommander.applesingle.AppleSingle;
import picocli.CommandLine.IVersionProvider;
/** Display version information. Note that this is dependent on the Spring Boot Gradle plugin configuration. */
public class VersionProvider implements IVersionProvider {
public String[] getVersion() {
return new String[] {
String.format("acx: %s", Main.class.getPackage().getImplementationVersion()),
String.format("AppleCommander API: %s", AppleCommander.VERSION),
String.format("AppleSingle API: %s", AppleSingle.VERSION)
};
}
}

View File

@ -0,0 +1,12 @@
package io.github.applecommander.acx.base;
import com.webcodepro.applecommander.storage.Disk;
import io.github.applecommander.acx.converter.DiskConverter;
import picocli.CommandLine.Option;
public abstract class ReadOnlyDiskImageCommandOptions extends ReusableCommandOptions {
@Option(names = { "-d", "--disk" }, description = "Image to process [$ACX_DISK_NAME].", required = true,
converter = DiskConverter.class, defaultValue = "${ACX_DISK_NAME}")
protected Disk disk;
}

View File

@ -0,0 +1,14 @@
package io.github.applecommander.acx.base;
public abstract class ReadWriteDiskCommandOptions extends ReadOnlyDiskImageCommandOptions {
@Override
public Integer call() throws Exception {
int returnCode = handleCommand();
if (returnCode == 0) {
saveDisk(disk);
}
return returnCode;
}
}

View File

@ -0,0 +1,38 @@
package io.github.applecommander.acx.base;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import io.github.applecommander.filestreamer.FileStreamer;
import io.github.applecommander.filestreamer.FileTuple;
import io.github.applecommander.filestreamer.TypeOfFile;
import picocli.CommandLine.Parameters;
public abstract class ReadWriteDiskCommandWithGlobOptions extends ReadWriteDiskCommandOptions {
private static Logger LOG = Logger.getLogger(ReadWriteDiskCommandWithGlobOptions.class.getName());
@Parameters(arity = "1..*", description = "File glob(s) to unlock (default = '*') - be cautious of quoting!")
private List<String> globs = Arrays.asList("*");
@Override
public int handleCommand() throws Exception {
List<FileTuple> files = FileStreamer.forDisk(disk)
.ignoreErrors(true)
.includeTypeOfFile(TypeOfFile.FILE)
.matchGlobs(globs)
.stream()
.collect(Collectors.toList());
if (files.isEmpty()) {
LOG.warning(() -> String.format("No matches found for %s.", String.join(",", globs)));
} else {
files.forEach(this::fileHandler);
}
return 0;
}
public abstract void fileHandler(FileTuple tuple);
}

View File

@ -0,0 +1,41 @@
package io.github.applecommander.acx.base;
import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.logging.Logger;
import com.webcodepro.applecommander.storage.Disk;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
@Command(descriptionHeading = "%n",
optionListHeading = "%nOptions:%n",
parameterListHeading = "%nParameters:%n")
public abstract class ReusableCommandOptions implements Callable<Integer> {
private static Logger LOG = Logger.getLogger(ReusableCommandOptions.class.getName());
@Option(names = { "-h", "--help" }, description = "Show help for subcommand.", usageHelp = true)
private boolean helpFlag;
@Override
public Integer call() throws Exception {
return handleCommand();
}
public abstract int handleCommand() throws Exception;
public void saveDisk(Disk disk) {
try {
// Only save if there are changes.
if (disk.getDiskImageManager().hasChanged()) {
LOG.fine(() -> String.format("Saving disk '%s'", disk.getFilename()));
disk.save();
} else {
LOG.fine(() -> String.format("Disk '%s' has not changed; not saving.", disk.getFilename()));
}
} catch (IOException e) {
LOG.severe(e.getMessage());
}
}
}

View File

@ -0,0 +1,39 @@
package io.github.applecommander.acx.command;
import java.io.File;
import com.webcodepro.applecommander.storage.Disk;
import io.github.applecommander.acx.base.ReusableCommandOptions;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
@Command(name = "convert", description = {
"Uncompress a ShrinkIt or Binary II file; ",
"or convert a DiskCopy 4.2 image into a ProDOS disk image." })
public class ConvertCommand extends ReusableCommandOptions {
@Option(names = { "-d", "--disk" }, description = "Image to create [$ACX_DISK_NAME].", required = true,
defaultValue = "${ACX_DISK_NAME}")
private String diskName;
@Option(names = { "-f", "--force" }, description = "Allow existing disk image to be replaced.")
private boolean overwriteFlag;
@Parameters(description = "Archive to convert.", arity = "1")
private String archiveName;
@Override
public int handleCommand() throws Exception {
File targetFile = new File(diskName);
if (targetFile.exists() && !overwriteFlag) {
throw new RuntimeException("File exists and overwriting not enabled.");
}
Disk disk = new Disk(archiveName);
disk.setFilename(diskName);
saveDisk(disk);
return 0;
}
}

View File

@ -0,0 +1,75 @@
package io.github.applecommander.acx.command;
import java.util.List;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import com.webcodepro.applecommander.storage.Disk;
import com.webcodepro.applecommander.storage.DiskException;
import com.webcodepro.applecommander.storage.FormattedDisk;
import io.github.applecommander.acx.base.ReadWriteDiskCommandOptions;
import io.github.applecommander.acx.converter.DiskConverter;
import io.github.applecommander.acx.fileutil.FileUtils;
import io.github.applecommander.filestreamer.FileStreamer;
import io.github.applecommander.filestreamer.FileTuple;
import io.github.applecommander.filestreamer.TypeOfFile;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
@Command(name = "copy", description = "Copy files between disks.",
aliases = { "cp" })
public class CopyFileCommand extends ReadWriteDiskCommandOptions {
private static Logger LOG = Logger.getLogger(CopyFileCommand.class.getName());
@Option(names = { "-r", "--recursive" }, description = "Copy files recursively.")
private boolean recursiveFlag;
@Option(names = { "-f", "--force" }, description = "Overwrite existing files.")
private boolean overwriteFlag;
@Option(names = { "--to", "--directory" }, description = "Specify which directory to place files.")
private String targetPath;
@Option(names = { "-s", "--from", "--source" }, description = "Source disk for files.",
converter = DiskConverter.class, required = true)
private Disk sourceDisk;
@Parameters(arity = "*", description = "File glob(s) to copy (default = '*')",
defaultValue = "*")
private List<String> globs;
@Override
public int handleCommand() throws Exception {
List<FileTuple> files = FileStreamer.forDisk(sourceDisk)
.ignoreErrors(true)
.includeTypeOfFile(TypeOfFile.BOTH)
.recursive(recursiveFlag)
.matchGlobs(globs)
.stream()
.collect(Collectors.toList());
if (files.isEmpty()) {
LOG.warning(() -> String.format("No matches found for %s.", String.join(",", globs)));
} else {
files.forEach(this::fileHandler);
}
return 0;
}
private void fileHandler(FileTuple tuple) {
try {
FormattedDisk formattedDisk = disk.getFormattedDisks()[0];
if (!recursiveFlag && tuple.fileEntry.isDirectory()) {
formattedDisk.createDirectory(tuple.fileEntry.getFilename());
} else {
FileUtils copier = new FileUtils(overwriteFlag);
copier.copy(formattedDisk, tuple.fileEntry);
}
} catch (DiskException ex) {
LOG.severe(ex.getMessage());
throw new RuntimeException(ex);
}
}
}

View File

@ -0,0 +1,79 @@
package io.github.applecommander.acx.command;
import java.util.logging.Logger;
import com.webcodepro.applecommander.storage.Disk;
import com.webcodepro.applecommander.storage.FormattedDisk;
import com.webcodepro.applecommander.storage.os.dos33.DosFormatDisk;
import com.webcodepro.applecommander.storage.os.dos33.OzDosFormatDisk;
import com.webcodepro.applecommander.storage.os.dos33.UniDosFormatDisk;
import com.webcodepro.applecommander.storage.os.pascal.PascalFormatDisk;
import com.webcodepro.applecommander.storage.os.prodos.ProdosFormatDisk;
import com.webcodepro.applecommander.storage.physical.ImageOrder;
import io.github.applecommander.acx.SystemType;
import io.github.applecommander.acx.base.ReusableCommandOptions;
import io.github.applecommander.acx.converter.DataSizeConverter;
import io.github.applecommander.acx.converter.SystemTypeConverter;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
@Command(name = "create", description = "Create a disk image.",
aliases = { "mkdisk" })
public class CreateDiskCommand extends ReusableCommandOptions {
private static Logger LOG = Logger.getLogger(CreateDiskCommand.class.getName());
@Option(names = { "-d", "--disk" }, description = "Image to create [$ACX_DISK_NAME].", required = true,
defaultValue = "${ACX_DISK_NAME}")
private String imageName;
@Option(names = { "-t", "--type" }, required = true, converter = SystemTypeConverter.class,
description = "Select system type (DOS, ProDOS, Pascal.")
private SystemType type;
@Option(names = { "-s", "--size" }, defaultValue = "140kb", converter = DataSizeConverter.class,
description = "Select disk size (140K, 800K, 10M).")
private int size;
@Option(names = { "-f", "--format" },
description = "Disk to copy system files/tracks/boot sector from.")
private String formatSource;
@Option(names = { "-n", "--name" }, defaultValue = "NEW.DISK",
description = "Disk Volume name (ProDOS/Pascal).")
private String diskName;
@Override
public int handleCommand() throws Exception {
LOG.info(() -> String.format("Creating %s image of type %s.", DataSizeConverter.format(size), type));
ImageOrder order = type.createImageOrder(size);
FormattedDisk[] disks = null;
switch (type) {
case DOS:
disks = DosFormatDisk.create(imageName, order);
break;
case OZDOS:
disks = OzDosFormatDisk.create(imageName, order);
break;
case UNIDOS:
disks = UniDosFormatDisk.create(imageName, order);
break;
case PRODOS:
disks = ProdosFormatDisk.create(imageName, diskName, order);
break;
case PASCAL:
disks = PascalFormatDisk.create(imageName, diskName, order);
break;
}
if (formatSource != null) {
Disk systemSource = new Disk(formatSource);
type.copySystem(disks[0], systemSource.getFormattedDisks()[0]);
}
saveDisk(disks[0]);
return 0;
}
}

View File

@ -0,0 +1,31 @@
package io.github.applecommander.acx.command;
import java.util.logging.Logger;
import io.github.applecommander.acx.base.ReadWriteDiskCommandWithGlobOptions;
import io.github.applecommander.filestreamer.FileTuple;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
@Command(name = "delete", description = "Delete file(s) from a disk image.",
aliases = { "del", "rm" })
public class DeleteCommand extends ReadWriteDiskCommandWithGlobOptions {
private static Logger LOG = Logger.getLogger(DeleteCommand.class.getName());
@Option(names = { "-f", "--force" }, description = "Force delete locked files.")
private boolean forceFlag;
public void fileHandler(FileTuple tuple) {
if (tuple.fileEntry.isLocked()) {
if (forceFlag) {
LOG.info(() -> String.format("File '%s' is locked, but 'force' specified; ignoring lock.",
tuple.fileEntry.getFilename()));
} else {
LOG.warning(() -> String.format("File '%s' is locked.", tuple.fileEntry.getFilename()));
return;
}
}
tuple.fileEntry.delete();
LOG.info(() -> String.format("File '%s' deleted.", tuple.fileEntry.getFilename()));
}
}

View File

@ -0,0 +1,96 @@
package io.github.applecommander.acx.command;
import java.util.Arrays;
import java.util.function.Function;
import com.webcodepro.applecommander.storage.FormattedDisk;
import com.webcodepro.applecommander.storage.FormattedDisk.DiskUsage;
import io.github.applecommander.acx.base.ReadOnlyDiskImageCommandOptions;
import picocli.CommandLine.Command;
@Command(name = "diskmap", description = "Show disk usage map.",
aliases = { "map" })
public class DiskMapCommand extends ReadOnlyDiskImageCommandOptions {
@Override
public int handleCommand() throws Exception {
Arrays.asList(disk.getFormattedDisks()).forEach(this::showDiskMap);
return 0;
}
public void showDiskMap(FormattedDisk formattedDisk) {
final int[] dimensions = formattedDisk.getBitmapDimensions();
final int length = formattedDisk.getBitmapLength();
final int width,height;
final Function<Integer,Integer> leftNumFn, rightNumFn;
if (dimensions != null && dimensions.length == 2) {
height = dimensions[0];
width = dimensions[1];
// This is expected to be Track, so same number on left and right.
leftNumFn = rightNumFn = i -> i;
} else {
width = 70;
height= (length + width - 1) / width;
// This is expected to be blocks, so show start of range of
// left and end of range on right.
leftNumFn = i -> i * width;
rightNumFn = i -> (i + 1) * width - 1;
}
title(formattedDisk.getBitmapLabels());
header1(width); // 10's position
header2(width); // 1's position
header3(width); // divider
DiskUsage diskUsage = formattedDisk.getDiskUsage();
for (int y=0; y<height; y++) {
System.out.printf("%5d|", leftNumFn.apply(y));
for (int x=0; x<width; x++) {
if (diskUsage.hasNext()) {
diskUsage.next();
System.out.print(diskUsage.isUsed() ? '*' : '.');
} else {
System.out.print(" ");
}
}
System.out.printf("|%d", rightNumFn.apply(y));
System.out.println();
}
header3(width);
header2(width);
header1(width);
}
void title(String[] labels) {
System.out.print(" ");
if (labels.length == 2) {
System.out.printf("X=%s, Y=%s", labels[1], labels[0]);
}
else {
System.out.printf("By %s", String.join(", ", labels));
}
System.out.println();
}
void header1(final int width) {
System.out.print(" ");
for (int i=0; i<width; i++) {
System.out.print(i % 10 == 0 ? Character.forDigit(i%10, 10) : ' ');
}
System.out.println();
}
void header2(final int width) {
System.out.print(" ");
for (int i=0; i<width; i++) {
System.out.print(i%10);
}
System.out.println();
}
void header3(final int width) {
System.out.print(" ");
for (int i=0; i<width; i++) {
System.out.print(i%5 == 0 ? '+' : '-');
}
System.out.println();
}
}

View File

@ -0,0 +1,161 @@
package io.github.applecommander.acx.command;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.logging.Logger;
import com.webcodepro.applecommander.storage.FileEntry;
import com.webcodepro.applecommander.storage.FileFilter;
import com.webcodepro.applecommander.storage.filters.BinaryFileFilter;
import com.webcodepro.applecommander.storage.filters.HexDumpFileFilter;
import io.github.applecommander.acx.base.ReadOnlyDiskImageCommandOptions;
import io.github.applecommander.filestreamer.FileStreamer;
import io.github.applecommander.filestreamer.FileTuple;
import io.github.applecommander.filestreamer.TypeOfFile;
import io.github.applecommander.filters.AppleSingleFileFilter;
import io.github.applecommander.filters.RawFileFilter;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.ParameterException;
import picocli.CommandLine.Parameters;
import picocli.CommandLine.Spec;
@Command(name = "export", description = "Export file(s) from a disk image.",
aliases = { "x", "get" })
public class ExportCommand extends ReadOnlyDiskImageCommandOptions {
private static Logger LOG = Logger.getLogger(ExportCommand.class.getName());
@Spec
private CommandSpec spec;
@ArgGroup(exclusive = true, heading = "%nFile extract methods:%n")
private FileExtractMethods extraction = new FileExtractMethods();
@Option(names = { "--deleted" }, description = "Include deleted files (use at your own risk!)")
private boolean deletedFlag;
@Option(names = { "-o", "--output" }, description = "Extract to file or to directory (default is stdout).")
private File outputFile;
@Parameters(arity = "*", description = "File glob(s) to extract (default = '*') - be cautious of quoting!")
private List<String> globs = Arrays.asList("*");
public void validate() {
List<String> errors = new ArrayList<>();
// multiple files require --output
if (isMultipleFiles()) {
if (outputFile == null) {
errors.add("--output directory must be specified with multiple files");
} else if (!outputFile.isDirectory()) {
errors.add("--output must be a directory");
}
}
if (!errors.isEmpty()) {
throw new ParameterException(spec.commandLine(), String.join(", ", errors));
}
}
@Override
public int handleCommand() throws Exception {
validate();
Consumer<FileTuple> fileHandler =
(outputFile == null) ? this::writeToStdout : this::writeToOutput;
FileStreamer.forDisk(disk)
.ignoreErrors(true)
.includeDeleted(deletedFlag)
.includeTypeOfFile(TypeOfFile.FILE)
.matchGlobs(globs)
.stream()
.forEach(fileHandler);
return 0;
}
public boolean hasFiles() {
return globs != null && globs.size() > 1;
}
public boolean isAllFiles() {
return globs == null || globs.isEmpty();
}
public boolean isMultipleFiles() {
return hasFiles() || isAllFiles();
}
public void writeToStdout(FileTuple tuple) {
try {
FileFilter ff = extraction.extractFunction.apply(tuple.fileEntry);
System.out.write(ff.filter(tuple.fileEntry));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public void writeToOutput(FileTuple tuple) {
File file = outputFile;
FileFilter ff = extraction.extractFunction.apply(tuple.fileEntry);
if (file.isDirectory()) {
if (!tuple.paths.isEmpty()) {
file = new File(outputFile, String.join(File.pathSeparator, tuple.paths));
boolean created = file.mkdirs();
if (created) LOG.info(String.format("Directory created: %s", file.getPath()));
}
file = new File(file, ff.getSuggestedFileName(tuple.fileEntry));
}
LOG.info(String.format("Writing to '%s'", file.getPath()));
try (OutputStream out = new FileOutputStream(file)) {
out.write(ff.filter(tuple.fileEntry));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static class FileExtractMethods {
private Function<FileEntry,FileFilter> extractFunction = this::asSuggestedFile;
@Option(names = { "--raw", "--binary" }, description = "Extract file in native format.")
public void setBinaryExtraction(boolean flag) {
this.extractFunction = this::asRawFile;
}
@Option(names = { "--hex", "--dump" }, description = "Extract file in hex dump format.")
public void setHexDumpExtraction(boolean flag) {
this.extractFunction = this::asHexDumpFile;
}
@Option(names = { "--suggested" }, description = "Extract file as suggested by AppleCommander (default)")
public void setSuggestedExtraction(boolean flag) {
this.extractFunction = this::asSuggestedFile;
}
@Option(names = { "--as", "--applesingle" }, description = "Extract file to AppleSingle file.")
public void setAppleSingleExtraction(boolean flag) {
this.extractFunction = this::asAppleSingleFile;
}
public FileFilter asRawFile(FileEntry entry) {
return new RawFileFilter();
}
public FileFilter asSuggestedFile(FileEntry entry) {
FileFilter ff = entry.getSuggestedFilter();
if (ff instanceof BinaryFileFilter) {
ff = new HexDumpFileFilter();
}
return ff;
}
public FileFilter asHexDumpFile(FileEntry entry) {
return new HexDumpFileFilter();
}
public FileFilter asAppleSingleFile(FileEntry entry) {
return new AppleSingleFileFilter();
}
}
}

View File

@ -0,0 +1,330 @@
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.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
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;
import com.webcodepro.applecommander.util.AppleUtil;
import com.webcodepro.applecommander.util.StreamUtil;
import com.webcodepro.applecommander.util.TranslatorStream;
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.FileEntryReader;
import io.github.applecommander.acx.fileutil.FileUtils;
import io.github.applecommander.acx.fileutil.OverrideFileEntryReader;
import io.github.applecommander.applesingle.AppleSingle;
import io.github.applecommander.applesingle.FileDatesInfo;
import io.github.applecommander.applesingle.ProdosFileInfo;
import io.github.applecommander.bastools.api.Configuration;
import io.github.applecommander.bastools.api.Parser;
import io.github.applecommander.bastools.api.TokenReader;
import io.github.applecommander.bastools.api.Visitors;
import io.github.applecommander.bastools.api.model.Program;
import io.github.applecommander.bastools.api.model.Token;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
@Command(name = "import", description = "Import file onto disk.",
aliases = { "put" })
public class ImportCommand extends ReadWriteDiskCommandOptions {
private static Logger LOG = Logger.getLogger(ImportCommand.class.getName());
@ArgGroup(heading = "%nInput source:%n", multiplicity = "1")
private InputData inputData;
@ArgGroup(heading = "%nProcessing options:%n")
private Processor processor;
@ArgGroup(heading = "%nGeneral overrides:%n", exclusive = false)
private Overrides overrides = new Overrides();
@Option(names = { "--dir" }, description = "Write file(s) to directory.")
private Optional<String> directoryName;
@Option(names = { "-f", "--force" }, description = "Over-write existing files.")
private boolean overwriteFlag;
@Override
public int handleCommand() throws Exception {
DirectoryEntry directory = disk.getFormattedDisks()[0];
if (directoryName.isPresent()) {
String[] dirs = directoryName.get().split("/");
for (String dir : dirs) {
Optional<FileEntry> fileEntry = directory.getFiles().stream()
.filter(f -> dir.equalsIgnoreCase(f.getFilename()))
.findFirst();
Optional<DirectoryEntry> dirEntry = fileEntry
.filter(FileEntry::isDirectory)
.map(DirectoryEntry.class::cast);
directory = dirEntry.orElseThrow(() ->
new IOException(String.format("Directory '%s' not found.", dir)));
}
}
FileUtils copier = new FileUtils(overwriteFlag);
FileEntryReader inputReader = inputData.get();
for (FileEntryReader processorReader : processor.apply(inputReader)) {
FileEntryReader reader = OverrideFileEntryReader.builder()
.filename(overrides.fileName)
.prodosFiletype(overrides.fileType)
.binaryAddress(overrides.fileAddress)
.build(processorReader);
copier.copyFile(directory, reader);
}
return 0;
}
public static class InputData {
private FileEntryReader fileEntryReader;
public FileEntryReader get() {
return fileEntryReader;
}
@Option(names = { "--stdin" }, description = "Import from standard input.")
public void stdinFlag(boolean flag) {
try {
byte[] data = System.in.readAllBytes();
fileEntryReader = OverrideFileEntryReader.builder()
.fileData(data)
.filename("UNKNOWN")
.prodosFiletype("BIN")
.build();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Parameters(description = "File to import.")
public void fromFile(final String filename) {
try {
Path path = Path.of(filename);
byte[] data = Files.readAllBytes(path);
fileEntryReader = OverrideFileEntryReader.builder()
.fileData(data)
.filename(filename)
.prodosFiletype("BIN")
.build();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
public static class Processor {
private Function<FileEntryReader,List<FileEntryReader>> fileEntryReaderFn;
public List<FileEntryReader> apply(FileEntryReader reader) {
return fileEntryReaderFn.apply(reader);
}
@Option(names = { "--text", "--text-high" }, description = {
"Import as a text file, setting high bit and ",
"replacing newline characters with $8D." })
public void setTextModeSetHighBit(boolean textFlag) {
fileEntryReaderFn = this::handleTextModeSetHighBit;
}
@Option(names = { "--text-low" }, description = {
"Import as a text file, clearing high bit and ",
"replacing newline characters with $8D." })
public void setTextModeClearHighBit(boolean textFlag) {
fileEntryReaderFn = this::handleTextModeClearHighBit;
}
@Option(names = { "--dos" }, description = "Use standard 4-byte DOS header.")
public void setDosMode(boolean dosFlag) {
fileEntryReaderFn = this::handleDosMode;
}
@Option(names = { "--geos" }, description = "Interpret as a GEOS conversion file.")
public void setGeosMode(boolean geosFlag) {
fileEntryReaderFn = this::handleGeosMode;
}
@Option(names = { "--basic" }, description = "Tokenize an AppleSoft BASIC program.")
public void setApplesoftTokenizerMode(boolean tokenizeMode) {
fileEntryReaderFn = this::handleApplesoftTokenizeMode;
}
@Option(names = { "--as", "--applesingle" }, description = "Import Apple Single file.")
public void setAppleSingleMode(boolean applesingleMode) {
fileEntryReaderFn = this::handleAppleSingleMode;
}
@Option(names = { "--shk", "--nufx", "--shrinkit", "--bxy" },
description = "Import files from SHK archive.")
public void setShrinkitMode(boolean shrinkitMode) {
fileEntryReaderFn = this::handleShrinkitMode;
}
private List<FileEntryReader> handleTextModeSetHighBit(FileEntryReader reader) {
InputStream inputStream = new ByteArrayInputStream(reader.getFileData().get());
return handleTextMode(TranslatorStream.builder(inputStream)
.lfToCr().setHighBit().get(), reader);
}
private List<FileEntryReader> handleTextModeClearHighBit(FileEntryReader reader) {
InputStream inputStream = new ByteArrayInputStream(reader.getFileData().get());
return handleTextMode(TranslatorStream.builder(inputStream)
.lfToCr().clearHighBit().get(), reader);
}
private List<FileEntryReader> handleTextMode(InputStream inputStream, FileEntryReader reader) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
StreamUtil.copy(inputStream, outputStream);
final byte[] translatedData = outputStream.toByteArray();
return OverrideFileEntryReader.builder()
.fileData(translatedData)
.prodosFiletype("TXT")
.buildList(reader);
} catch (IOException cause) {
throw new UncheckedIOException(cause);
}
}
private List<FileEntryReader> handleDosMode(FileEntryReader reader) {
try {
ByteArrayInputStream inputStream = new ByteArrayInputStream(reader.getFileData().get());
byte[] header = new byte[4];
if (inputStream.read(header) != 4) {
throw new IOException("Unable to read DOS header.");
}
int address = AppleUtil.getWordValue(header, 0);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
inputStream.transferTo(outputStream);
return OverrideFileEntryReader.builder()
.fileData(outputStream.toByteArray())
.binaryAddress(address)
.buildList(reader);
} catch (IOException cause) {
throw new UncheckedIOException(cause);
}
}
private List<FileEntryReader> handleGeosMode(FileEntryReader reader) {
return OverrideFileEntryReader.builder()
.prodosFiletype("GEO")
.binaryAddress(0)
.buildList(reader);
}
private List<FileEntryReader> handleApplesoftTokenizeMode(FileEntryReader reader) {
try {
File fakeTempSource = File.createTempFile("ac-", "bas");
fakeTempSource.deleteOnExit();
Configuration config = Configuration.builder().sourceFile(fakeTempSource).build();
Queue<Token> tokens = TokenReader.tokenize(new ByteArrayInputStream(reader.getFileData().get()));
Parser parser = new Parser(tokens);
Program program = parser.parse();
byte[] tokenData = Visitors.byteVisitor(config).dump(program);
return OverrideFileEntryReader.builder()
.fileData(tokenData)
.prodosFiletype("BAS")
.binaryAddress(config.startAddress)
.buildList(reader);
} catch (IOException cause) {
throw new UncheckedIOException(cause);
}
}
private List<FileEntryReader> handleAppleSingleMode(FileEntryReader reader) {
try {
AppleSingle as = AppleSingle.read(reader.getFileData().get());
if (as.getProdosFileInfo() == null) {
throw new IOException("This AppleSingle does not contain a ProDOS file.");
}
if (as.getDataFork() == null || as.getDataFork().length == 0) {
throw new IOException("This AppleSingle does not contain a data fork.");
}
ProdosFileInfo info = as.getProdosFileInfo();
String fileType = ProdosFormatDisk.getFiletype(info.getFileType());
OverrideFileEntryReader.Builder builder = OverrideFileEntryReader.builder();
builder.filename(Optional.ofNullable(as.getRealName()));
builder.fileData(as.getDataFork());
builder.resourceData(Optional.ofNullable(as.getResourceFork()));
builder.prodosFiletype(fileType);
builder.locked(info.getAccess() == 0xc3);
builder.auxiliaryType(info.getAuxType());
if (as.getFileDatesInfo() != null) {
FileDatesInfo dates = as.getFileDatesInfo();
builder.creationDate(Date.from(dates.getCreationInstant()));
builder.lastModificationDate(Date.from(dates.getModificationInstant()));
}
return builder.buildList(reader);
} catch (IOException cause) {
throw new UncheckedIOException(cause);
}
}
private List<FileEntryReader> handleShrinkitMode(FileEntryReader reader) {
try {
List<FileEntryReader> files = new ArrayList<>();
ByteArrayInputStream inputStream = new ByteArrayInputStream(reader.getFileData().get());
NuFileArchive nufx = new NuFileArchive(inputStream);
for (HeaderBlock header : nufx.getHeaderBlocks()) {
OverrideFileEntryReader.Builder builder = OverrideFileEntryReader.builder()
.filename(header.getFilename())
.prodosFiletype(ProdosFormatDisk.getFiletype((int)header.getFileType()))
.auxiliaryType((int)header.getExtraType())
.creationDate(header.getCreateWhen())
.lastModificationDate(header.getModWhen());
ThreadRecord dataFork = header.getDataForkThreadRecord();
ThreadRecord resourceFork = header.getResourceForkThreadRecord();
if (dataFork == null) {
LOG.info(() -> String.format("No data fork for '%s', skipping it.",
header.getFilename()));
continue;
}
builder.fileData(dataFork.getBytes());
if (resourceFork != null) {
builder.resourceData(resourceFork.getBytes());
}
files.add(builder.build());
}
return files;
} catch (IOException cause) {
throw new UncheckedIOException(cause);
}
}
}
public static class Overrides {
@Option(names = { "-t", "--type" }, description = "File type.")
private Optional<String> fileType;
@Option(names = { "-a", "--addr" }, description = "File address.",
converter = IntegerTypeConverter.class)
private Optional<Integer> fileAddress;
@Option(names = { "-n", "--name" }, description = "File name.")
private Optional<String> fileName;
}
}

View File

@ -0,0 +1,30 @@
package io.github.applecommander.acx.command;
import java.util.logging.Logger;
import com.webcodepro.applecommander.storage.FormattedDisk;
import com.webcodepro.applecommander.storage.FormattedDisk.DiskInformation;
import io.github.applecommander.acx.base.ReadOnlyDiskImageCommandOptions;
import picocli.CommandLine.Command;
@Command(name = "info", description = "Show information on a disk image(s).",
aliases = "i")
public class InfoCommand extends ReadOnlyDiskImageCommandOptions {
private static Logger LOG = Logger.getLogger(InfoCommand.class.getName());
@Override
public int handleCommand() throws Exception {
LOG.info(() -> "Path: " + disk.getFilename());
FormattedDisk[] formattedDisks = disk.getFormattedDisks();
for (int i = 0; i < formattedDisks.length; i++) {
FormattedDisk formattedDisk = formattedDisks[i];
LOG.info(() -> String.format("Disk: %s (%s)", formattedDisk.getDiskName(), formattedDisk.getFormat()));
for (DiskInformation diskinfo : formattedDisk.getDiskInformation()) {
System.out.printf("%s: %s\n", diskinfo.getLabel(), diskinfo.getValue());
}
System.out.println();
}
return 0;
}
}

View File

@ -0,0 +1,143 @@
package io.github.applecommander.acx.command;
import java.util.ArrayList;
import java.util.List;
import com.webcodepro.applecommander.storage.FormattedDisk;
import com.webcodepro.applecommander.storage.FormattedDisk.FileColumnHeader;
import io.github.applecommander.acx.base.ReadOnlyDiskImageCommandOptions;
import io.github.applecommander.filestreamer.FileStreamer;
import io.github.applecommander.filestreamer.FileTuple;
import io.github.applecommander.filestreamer.TypeOfFile;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
@Command(name = "list", description = "List directory of disk image(s).",
aliases = { "ls" })
public class ListCommand extends ReadOnlyDiskImageCommandOptions {
@ArgGroup(exclusive = true, multiplicity = "0..1", heading = "%nFile display formatting:%n")
private FileDisplay fileDisplay = new FileDisplay();
@Option(names = { "-r", "--recursive"}, description = "Display directory recursively.", negatable = true, defaultValue = "false")
private boolean recursiveFlag;
@Option(names = { "--deleted" }, description = "Show deleted files.")
private boolean deletedFlag;
@ArgGroup(exclusive = true, multiplicity = "0..1")
private TypeOfFileSelection typeOfFile = new TypeOfFileSelection();
@Option(names = "--header", negatable = true, description = "Show header.")
private boolean headerFlag = true;
@Option(names = "--column", negatable = true, description = "Show column headers.")
private boolean columnFlag = true;
@Option(names = "--footer", negatable = true, description = "Show footer.")
private boolean footerFlag = true;
@Option(names = "--globs", defaultValue = "*", split = ",", description = "File glob(s) to match.")
private List<String> globs = new ArrayList<String>();
private List<String> fmtSpec;
@Override
public int handleCommand() throws Exception {
FileStreamer.forDisk(disk)
.ignoreErrors(true)
.includeDeleted(deletedFlag)
.recursive(recursiveFlag)
.includeTypeOfFile(typeOfFile.typeOfFile())
.matchGlobs(globs)
.beforeDisk(this::header)
.afterDisk(this::footer)
.stream()
.forEach(this::list);
return 0;
}
protected void header(FormattedDisk disk) {
List<FileColumnHeader> headers = disk.getFileColumnHeaders(fileDisplay.format());
fmtSpec = createFormatSpec(headers);
System.out.println();
System.out.printf("File: %s\n", disk.getFilename());
System.out.printf("Name: %s\n", disk.getDiskName());
}
protected void list(FileTuple tuple) {
if (!deletedFlag && tuple.fileEntry.isDeleted()) {
return;
}
List<String> data = tuple.fileEntry.getFileColumnData(fileDisplay.format());
for (int i=0; i<tuple.paths.size(); i++) {
System.out.print(" ");
}
for (int d = 0; d < data.size(); d++) {
System.out.printf(fmtSpec.get(d), data.get(d));
}
if (tuple.fileEntry.isDeleted()) {
System.out.print("[deleted]");
}
System.out.println();
}
protected void footer(FormattedDisk disk) {
System.out.printf("%s format; %d bytes free; %d bytes used.\n",
disk.getFormat(),
disk.getFreeSpace(),
disk.getUsedSpace());
}
private List<String> createFormatSpec(List<FileColumnHeader> fileColumnHeaders) {
List<String> fmtSpec = new ArrayList<>();
for (FileColumnHeader h : fileColumnHeaders) {
String spec = String.format("%%%s%ds ", h.isRightAlign() ? "" : "-",
h.getMaximumWidth());
fmtSpec.add(spec);
}
return fmtSpec;
}
public static class FileDisplay {
public int format() {
if (standardFormat) {
return FormattedDisk.FILE_DISPLAY_STANDARD;
}
if (longFormat) {
return FormattedDisk.FILE_DISPLAY_DETAIL;
}
return FormattedDisk.FILE_DISPLAY_NATIVE;
}
@Option(names = { "-n", "--native" }, description = "Use native directory format (default).")
private boolean nativeFormat;
@Option(names = { "-s", "--short", "--standard" }, description = "Use brief directory format.")
private boolean standardFormat;
@Option(names = { "-l", "--long", "--detail" }, description = "Use long/detailed directory format.")
private boolean longFormat;
}
public static class TypeOfFileSelection {
public TypeOfFile typeOfFile() {
if (filesOnly) {
return TypeOfFile.FILE;
}
if (directoriesOnly) {
return TypeOfFile.DIRECTORY;
}
return TypeOfFile.BOTH;
}
@Option(names = "--file", description = "Only include files.")
private boolean filesOnly;
@Option(names = "--directory", description = "Only include directories.")
private boolean directoriesOnly;
}
}

View File

@ -0,0 +1,17 @@
package io.github.applecommander.acx.command;
import java.util.logging.Logger;
import io.github.applecommander.acx.base.ReadWriteDiskCommandWithGlobOptions;
import io.github.applecommander.filestreamer.FileTuple;
import picocli.CommandLine.Command;
@Command(name = "lock", description = "Lock file(s) on a disk image.")
public class LockCommand extends ReadWriteDiskCommandWithGlobOptions {
private static Logger LOG = Logger.getLogger(LockCommand.class.getName());
public void fileHandler(FileTuple tuple) {
tuple.fileEntry.setLocked(true);
LOG.info(() -> String.format("File '%s' locked.", tuple.fileEntry.getFilename()));
}
}

View File

@ -0,0 +1,55 @@
package io.github.applecommander.acx.command;
import java.util.Optional;
import com.webcodepro.applecommander.storage.DirectoryEntry;
import com.webcodepro.applecommander.storage.FileEntry;
import com.webcodepro.applecommander.storage.FormattedDisk;
import io.github.applecommander.acx.base.ReadWriteDiskCommandOptions;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
@Command(name = "mkdir", description = "Create a directory on disk.",
aliases = { "md" })
public class MkdirCommand extends ReadWriteDiskCommandOptions {
@Option(names = { "-p" }, description = "Create intermediate subdirectories.")
private boolean prefixFlag;
@Parameters(description = "Directory name to create (use '/' as divider).")
private String fullPath;
@Override
public int handleCommand() throws Exception {
FormattedDisk formattedDisk = disk.getFormattedDisks()[0];
DirectoryEntry directory = formattedDisk;
String[] paths = fullPath.split("/");
for (int i=0; i<paths.length; i++) {
final String pathName = formattedDisk.getSuggestedFilename(paths[i]);
Optional<FileEntry> optEntry = directory.getFiles().stream()
.filter(entry -> entry.getFilename().equalsIgnoreCase(pathName))
.findFirst();
if (optEntry.isPresent()) {
FileEntry fileEntry = optEntry.get();
if (fileEntry instanceof DirectoryEntry) {
directory = (DirectoryEntry)fileEntry;
}
else {
throw new RuntimeException(String.format("Not a directory: '%s'", pathName));
}
}
else {
if (prefixFlag || i == paths.length-1) {
directory = directory.createDirectory(pathName);
}
else {
throw new RuntimeException(String.format("Directory does not exist: '%s'", pathName));
}
}
}
return 0;
}
}

View File

@ -0,0 +1,32 @@
package io.github.applecommander.acx.command;
import java.util.logging.Logger;
import com.webcodepro.applecommander.storage.FormattedDisk;
import com.webcodepro.applecommander.storage.os.pascal.PascalFormatDisk;
import com.webcodepro.applecommander.storage.os.prodos.ProdosFormatDisk;
import io.github.applecommander.acx.base.ReadWriteDiskCommandOptions;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;
@Command(name = "rename-disk", description = "Rename volume of a disk image.")
public class RenameDiskCommand extends ReadWriteDiskCommandOptions {
private static Logger LOG = Logger.getLogger(RenameDiskCommand.class.getName());
@Parameters(description = "Disk name.")
private String diskName;
@Override
public int handleCommand() throws Exception {
FormattedDisk[] formattedDisks = disk.getFormattedDisks();
FormattedDisk formattedDisk = formattedDisks[0];
if (formattedDisk instanceof ProdosFormatDisk || formattedDisk instanceof PascalFormatDisk) {
formattedDisk.setDiskName(diskName);
return 0;
} else {
LOG.warning("Disk must be ProDOS or Pascal.");
return 1;
}
}
}

View File

@ -0,0 +1,67 @@
package io.github.applecommander.acx.command;
import java.util.List;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import io.github.applecommander.acx.base.ReadWriteDiskCommandOptions;
import io.github.applecommander.filestreamer.FileStreamer;
import io.github.applecommander.filestreamer.FileTuple;
import io.github.applecommander.filestreamer.TypeOfFile;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
@Command(name = "rename", description = "Rename file on a disk image.",
aliases = { "ren" })
public class RenameFileCommand extends ReadWriteDiskCommandOptions {
private static Logger LOG = Logger.getLogger(RenameFileCommand.class.getName());
@Option(names = { "-m", "--multiple" }, description = "Force rename when multiple files found.")
private boolean multipleOverride;
@Option(names = { "-f", "--force" }, description = "Rename locked files.")
private boolean lockOverride;
@Parameters(index = "0", description = "Original file name (include path).")
private String originalFilename;
@Parameters(index = "1", description = "New file name (just the new filename).")
private String newFilename;
@Override
public int handleCommand() throws Exception {
List<FileTuple> files = FileStreamer.forDisk(disk)
.ignoreErrors(true)
.includeTypeOfFile(TypeOfFile.FILE)
.matchGlobs(originalFilename)
.stream()
.collect(Collectors.toList());
if (files.isEmpty()) {
LOG.warning(() -> String.format("File not found for %s.", originalFilename));
}
else if (!multipleOverride && files.size() > 1) {
LOG.severe(() -> String.format("Multile files with %s found (count = %d).",
originalFilename, files.size()));
}
else {
files.forEach(this::fileHandler);
}
return 0;
}
public void fileHandler(FileTuple tuple) {
if (tuple.fileEntry.isLocked()) {
if (lockOverride) {
LOG.info(() -> String.format("File '%s' is locked, but 'force' specified; ignoring lock.",
tuple.fileEntry.getFilename()));
} else {
LOG.warning(() -> String.format("File '%s' is locked.", tuple.fileEntry.getFilename()));
return;
}
}
tuple.fileEntry.setFilename(newFilename);
LOG.info(() -> String.format("File '%s' renamed to '%s'.", originalFilename, newFilename));
}
}

View File

@ -0,0 +1,79 @@
package io.github.applecommander.acx.command;
import java.util.Optional;
import com.webcodepro.applecommander.storage.DirectoryEntry;
import com.webcodepro.applecommander.storage.DiskException;
import com.webcodepro.applecommander.storage.FileEntry;
import com.webcodepro.applecommander.storage.FormattedDisk;
import io.github.applecommander.acx.base.ReadWriteDiskCommandOptions;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
@Command(name = "rmdir", description = "Remove a directory on disk.",
aliases = { "rd" })
public class RmdirCommand extends ReadWriteDiskCommandOptions {
@Option(names = { "-r", "--recursive" }, description = "Recursively delete subdirectories.")
private boolean recursiveFlag;
@Option(names = { "-f", "--force" }, description = "Force files to be deleted as well.")
private boolean forceFlag;
@Parameters(description = "Directory name to delete (use '/' as divider).")
private String fullPath;
@Override
public int handleCommand() throws Exception {
FormattedDisk formattedDisk = disk.getFormattedDisks()[0];
// Locate directory
DirectoryEntry directory = formattedDisk;
String[] paths = fullPath.split("/");
for (int i=0; i<paths.length; i++) {
final String pathName = formattedDisk.getSuggestedFilename(paths[i]);
Optional<FileEntry> optEntry = directory.getFiles().stream()
.filter(entry -> entry.getFilename().equalsIgnoreCase(pathName))
.findFirst();
if (optEntry.isPresent()) {
FileEntry fileEntry = optEntry.get();
if (fileEntry instanceof DirectoryEntry) {
directory = (DirectoryEntry)fileEntry;
}
else {
throw new RuntimeException(String.format("Not a directory: '%s'", pathName));
}
}
else {
throw new RuntimeException(String.format("Directory does not exist: '%s'", pathName));
}
}
deleteDirectory(directory);
return 0;
}
public void deleteDirectory(DirectoryEntry directory) throws DiskException {
for (FileEntry file : directory.getFiles()) {
if (file.isDeleted()) {
// skip
} else if (recursiveFlag && file.isDirectory()) {
deleteDirectory((DirectoryEntry)file);
}
else if (forceFlag && !file.isDirectory()) {
file.delete();
}
else {
String message = String.format("Encountered %s '%s'",
file.isDirectory() ? "directory" : "file",
file.getFilename());
throw new RuntimeException(message);
}
}
FileEntry file = (FileEntry)directory;
file.delete();
}
}

View File

@ -0,0 +1,17 @@
package io.github.applecommander.acx.command;
import java.util.logging.Logger;
import io.github.applecommander.acx.base.ReadWriteDiskCommandWithGlobOptions;
import io.github.applecommander.filestreamer.FileTuple;
import picocli.CommandLine.Command;
@Command(name = "unlock", description = "Unlock file(s) on a disk image.")
public class UnlockCommand extends ReadWriteDiskCommandWithGlobOptions {
private static Logger LOG = Logger.getLogger(UnlockCommand.class.getName());
public void fileHandler(FileTuple tuple) {
tuple.fileEntry.setLocked(false);
LOG.info(() -> String.format("File '%s' unlocked.", tuple.fileEntry.getFilename()));
}
}

View File

@ -0,0 +1,48 @@
package io.github.applecommander.acx.converter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import picocli.CommandLine.ITypeConverter;
import picocli.CommandLine.TypeConversionException;
public class DataSizeConverter implements ITypeConverter<Integer> {
public static final int KB = 1024;
public static final int MB = KB * 1024;
@Override
public Integer convert(String value) throws Exception {
Pattern pattern = Pattern.compile("([0-9]+)([km]b?)?", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(value);
if (matcher.matches()) {
String number = matcher.group(1);
String kmb = matcher.group(2);
if (kmb != null) {
kmb = kmb.toLowerCase();
}
int bytes = Integer.parseInt(number);
if (kmb.startsWith("k")) {
bytes *= KB;
}
else if (kmb.startsWith("m")) {
bytes *= MB;
}
else {
throw new TypeConversionException(String.format("Unexpected data size '%s'", kmb));
}
return bytes;
}
throw new TypeConversionException("Expecting format like '140kb' or '5mb'");
}
public static String format(int value) {
if (value < KB) {
return String.format("%,dB", value);
}
if (value < MB) {
return String.format("%,dKB", value / KB);
}
return String.format("%,dMB", value / MB);
}
}

View File

@ -0,0 +1,19 @@
package io.github.applecommander.acx.converter;
import java.nio.file.Files;
import java.nio.file.Path;
import com.webcodepro.applecommander.storage.Disk;
import picocli.CommandLine.ITypeConverter;
import picocli.CommandLine.TypeConversionException;
public class DiskConverter implements ITypeConverter<Disk> {
@Override
public Disk convert(String filename) throws Exception {
if (Files.exists(Path.of(filename))) {
return new Disk(filename);
}
throw new TypeConversionException(String.format("Disk '%s' not found", filename));
}
}

View File

@ -0,0 +1,19 @@
package io.github.applecommander.acx.converter;
import picocli.CommandLine.ITypeConverter;
/** Add support for "$801" and "0x801" instead of just decimal like 2049. */
public class IntegerTypeConverter implements ITypeConverter<Integer> {
@Override
public Integer convert(String value) {
if (value == null) {
return null;
} else if (value.startsWith("$")) {
return Integer.valueOf(value.substring(1), 16);
} else if (value.startsWith("0x") || value.startsWith("0X")) {
return Integer.valueOf(value.substring(2), 16);
} else {
return Integer.valueOf(value);
}
}
}

View File

@ -0,0 +1,11 @@
package io.github.applecommander.acx.converter;
import io.github.applecommander.acx.SystemType;
import picocli.CommandLine.ITypeConverter;
public class SystemTypeConverter implements ITypeConverter<SystemType> {
@Override
public SystemType convert(String value) throws Exception {
return SystemType.valueOf(value.replaceAll("[^a-zA-Z]", "").toUpperCase());
}
}

View File

@ -0,0 +1,113 @@
package io.github.applecommander.acx.fileutil;
import java.util.Map;
import java.util.Optional;
import com.webcodepro.applecommander.storage.DiskFullException;
import com.webcodepro.applecommander.storage.os.dos33.DosFileEntry;
import com.webcodepro.applecommander.storage.os.dos33.DosFormatDisk;
import com.webcodepro.applecommander.util.AppleUtil;
public class DosFileEntryReaderWriter implements FileEntryReader, FileEntryWriter {
private static final Map<String,String> FILE_TYPES;
static {
FILE_TYPES = Map.of(
"T", "TXT",
"I", "INT",
"A", "BAS",
"B", "BIN",
"S", "$F1",
"R", "REL",
"a", "$F2",
"b", "$F3"
);
}
private DosFileEntry fileEntry;
public DosFileEntryReaderWriter(DosFileEntry fileEntry) {
if (fileEntry.isDeleted()) {
throw new RuntimeException("Unable to copy deleted files.");
}
this.fileEntry = fileEntry;
}
@Override
public Optional<String> getFilename() {
return Optional.of(fileEntry.getFilename());
}
@Override
public void setFilename(String filename) {
fileEntry.setFilename(filename);
}
@Override
public Optional<String> getProdosFiletype() {
return Optional.ofNullable(FILE_TYPES.get(fileEntry.getFiletype()));
}
@Override
public void setProdosFiletype(String filetype) {
String dosFileType = FILE_TYPES.entrySet()
.stream()
.filter(e -> e.getValue().equals(filetype))
.map(Map.Entry::getKey)
.findFirst()
.orElse("B");
fileEntry.setFiletype(dosFileType);
}
@Override
public Optional<Boolean> isLocked() {
return Optional.of(fileEntry.isLocked());
}
@Override
public void setLocked(boolean flag) {
fileEntry.setLocked(flag);
}
@Override
public Optional<byte[]> getFileData() {
return Optional.ofNullable(fileEntry.getFileData());
}
@Override
public void setFileData(byte[] data) {
try {
fileEntry.setFileData(data);
} catch (DiskFullException e) {
throw new RuntimeException(e);
}
}
@Override
public Optional<Integer> getBinaryAddress() {
if (fileEntry.isBinaryFile()) {
byte[] rawdata = getRawFileData();
return Optional.of(AppleUtil.getWordValue(rawdata, 0));
}
return Optional.empty();
}
@Override
public void setBinaryAddress(int address) {
if (fileEntry.needsAddress()) {
fileEntry.setAddress(address);
}
}
@Override
public Optional<Integer> getBinaryLength() {
if (fileEntry.isBinaryFile() || fileEntry.isApplesoftBasicFile() || fileEntry.isIntegerBasicFile()) {
// DosFileEntry pulls the address
return Optional.of(fileEntry.getSize());
}
return Optional.empty();
}
@Override
public void setBinaryLength(int length) {
// Nothing to do
}
private byte[] getRawFileData() {
DosFormatDisk disk = (DosFormatDisk) fileEntry.getFormattedDisk();
return disk.getFileData(fileEntry);
}
}

View File

@ -0,0 +1,54 @@
package io.github.applecommander.acx.fileutil;
import java.util.Date;
import java.util.Optional;
import com.webcodepro.applecommander.storage.FileEntry;
import com.webcodepro.applecommander.storage.os.dos33.DosFileEntry;
import com.webcodepro.applecommander.storage.os.nakedos.NakedosFileEntry;
import com.webcodepro.applecommander.storage.os.pascal.PascalFileEntry;
import com.webcodepro.applecommander.storage.os.prodos.ProdosFileEntry;
import com.webcodepro.applecommander.storage.os.rdos.RdosFileEntry;
public interface FileEntryReader {
// FileEntry common
public default Optional<String> getFilename() { return Optional.empty(); }
public default Optional<String> getProdosFiletype() { return Optional.empty(); }
public default Optional<Boolean> isLocked() { return Optional.empty(); }
public default Optional<byte[]> getFileData() { return Optional.empty(); }
public default Optional<byte[]> getResourceData() { return Optional.empty(); }
/**
* The address embedded in binary objects.
* This varies by DOS's so is split apart.
*/
public default Optional<Integer> getBinaryAddress() { return Optional.empty(); }
/**
* The length embedded in binary, Applesoft, Integer BASIC objects.
* This varies by DOS's so is split apart.
*/
public default Optional<Integer> getBinaryLength() { return Optional.empty(); }
// ProdosFileEntry specific
public default Optional<Integer> getAuxiliaryType() { return Optional.empty(); }
public default Optional<Date> getCreationDate() { return Optional.empty(); }
// ProdosFileEntry / PascalFileEntry specific
public default Optional<Date> getLastModificationDate() { return Optional.empty(); }
public static FileEntryReader get(FileEntry fileEntry) {
if (fileEntry instanceof DosFileEntry) {
return new DosFileEntryReaderWriter((DosFileEntry)fileEntry);
}
else if (fileEntry instanceof NakedosFileEntry) {
return new NakedosFileEntryReader((NakedosFileEntry)fileEntry);
}
else if (fileEntry instanceof PascalFileEntry) {
return new PascalFileEntryReaderWriter((PascalFileEntry)fileEntry);
}
else if (fileEntry instanceof ProdosFileEntry) {
return new ProdosFileEntryReaderWriter((ProdosFileEntry)fileEntry);
}
else if (fileEntry instanceof RdosFileEntry) {
return new RdosFileEntryReader((RdosFileEntry)fileEntry);
}
throw new RuntimeException(String.format("No reader for %s", fileEntry.getClass().getName()));
}
}

View File

@ -0,0 +1,46 @@
package io.github.applecommander.acx.fileutil;
import java.util.Date;
import com.webcodepro.applecommander.storage.FileEntry;
import com.webcodepro.applecommander.storage.os.dos33.DosFileEntry;
import com.webcodepro.applecommander.storage.os.pascal.PascalFileEntry;
import com.webcodepro.applecommander.storage.os.prodos.ProdosFileEntry;
public interface FileEntryWriter {
// FileEntry common
public default void setFilename(String filename) { }
public default void setProdosFiletype(String filetype) { }
public default void setLocked(boolean flag) { }
public default void setFileData(byte[] data) { }
// Special case for GS/OS files (uglifies API; sets 0x05)
public default void setFileData(byte[] data, byte[] resource) { }
/**
* The address embedded in binary objects.
* This varies by DOS's so is split apart.
*/
public default void setBinaryAddress(int address) { }
/**
* The length embedded in binary, Applesoft, Integer BASIC objects.
* This varies by DOS's so is split apart.
*/
public default void setBinaryLength(int length) { }
// ProdosFileEntry specific
public default void setAuxiliaryType(int auxType) { }
public default void setCreationDate(Date date) { }
// ProdosFileEntry / PascalFileEntry specific
public default void setLastModificationDate(Date date) { }
public static FileEntryWriter get(FileEntry fileEntry) {
if (fileEntry instanceof DosFileEntry) {
return new DosFileEntryReaderWriter((DosFileEntry)fileEntry);
}
else if (fileEntry instanceof ProdosFileEntry) {
return new ProdosFileEntryReaderWriter((ProdosFileEntry)fileEntry);
}
else if (fileEntry instanceof PascalFileEntry) {
return new PascalFileEntryReaderWriter((PascalFileEntry)fileEntry);
}
throw new RuntimeException(String.format("No writer for %s", fileEntry.getClass().getName()));
}
}

View File

@ -0,0 +1,101 @@
package io.github.applecommander.acx.fileutil;
import java.util.Optional;
import java.util.logging.Logger;
import com.webcodepro.applecommander.storage.DirectoryEntry;
import com.webcodepro.applecommander.storage.DiskException;
import com.webcodepro.applecommander.storage.FileEntry;
import io.github.applecommander.acx.command.CopyFileCommand;
public class FileUtils {
private static Logger LOG = Logger.getLogger(CopyFileCommand.class.getName());
private boolean overwrite;
public FileUtils(boolean overwrite) {
this.overwrite = overwrite;
}
public void copy(DirectoryEntry directory, FileEntry file) throws DiskException {
LOG.fine(() -> String.format("Copying '%s'", file.getFilename()));
if (file.isDeleted()) {
// Skip deleted files
}
else if (file.isDirectory()) {
copyDirectory(directory, (DirectoryEntry)file, file.getFilename());
}
else {
copyFile(directory, file);
}
}
void copyDirectory(DirectoryEntry targetParent, DirectoryEntry sourceDir, String name) throws DiskException {
Optional<FileEntry> targetFile = targetParent.getFiles()
.stream()
.filter(fileEntry -> name.equals(fileEntry.getFilename()))
.findFirst();
Optional<DirectoryEntry> targetDir = targetFile
.filter(FileEntry::isDirectory)
.map(DirectoryEntry.class::cast);
if (targetDir.isPresent()) {
// Fall through to general logic
}
else if (targetFile.isPresent()) {
// This is an abstract class, so faking it for now.
throw new DiskException("Unable to create directory", name) {
private static final long serialVersionUID = 4726414295404986677L;
};
}
else {
targetDir = Optional.of(targetParent.createDirectory(name));
}
for (FileEntry fileEntry : sourceDir.getFiles()) {
copy(targetDir.get(), fileEntry);
}
}
void copyFile(DirectoryEntry directory, FileEntry sourceFile) throws DiskException {
FileEntryReader source = FileEntryReader.get(sourceFile);
copyFile(directory, source);
}
public void copyFile(DirectoryEntry directory, FileEntryReader source) throws DiskException {
String sourceName = source.getFilename().get();
String sanitizedName = directory.getFormattedDisk().getSuggestedFilename(sourceName);
final Optional<FileEntry> fileEntry = directory.getFiles().stream()
.filter(entry -> entry.getFilename().equals(sanitizedName))
.findFirst();
final FileEntry targetFile;
if (fileEntry.isPresent()) {
targetFile = fileEntry
.filter(entry -> overwrite)
.orElseThrow(() -> new RuntimeException(String.format("File '%s' exists.",
source.getFilename().get())));
}
else {
targetFile = directory.createFile();
}
FileEntryWriter target = FileEntryWriter.get(targetFile);
source.getFilename().ifPresent(target::setFilename);
source.getProdosFiletype().ifPresent(target::setProdosFiletype);
source.isLocked().ifPresent(target::setLocked);
source.getBinaryAddress().ifPresent(target::setBinaryAddress);
source.getBinaryLength().ifPresent(target::setBinaryLength);
source.getAuxiliaryType().ifPresent(target::setAuxiliaryType);
source.getCreationDate().ifPresent(target::setCreationDate);
source.getLastModificationDate().ifPresent(target::setLastModificationDate);
if (source.getFileData().isPresent() && source.getResourceData().isPresent()) {
target.setFileData(source.getFileData().get(), source.getResourceData().get());
} else {
source.getFileData().ifPresent(target::setFileData);
}
}
}

View File

@ -0,0 +1,22 @@
package io.github.applecommander.acx.fileutil;
import java.util.Optional;
import com.webcodepro.applecommander.storage.os.nakedos.NakedosFileEntry;
public class NakedosFileEntryReader implements FileEntryReader {
private NakedosFileEntry fileEntry;
public NakedosFileEntryReader(NakedosFileEntry fileEntry) {
this.fileEntry = fileEntry;
}
@Override
public Optional<String> getFilename() {
return Optional.of(fileEntry.getFilename());
}
@Override
public Optional<byte[]> getFileData() {
return Optional.of(fileEntry.getFileData());
}
}

View File

@ -0,0 +1,188 @@
package io.github.applecommander.acx.fileutil;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* Allow programmatic control of what is in the results of the file entry.
* Useful for translating from raw data.
* It can also be used to layer in results and overrides.
*/
public class OverrideFileEntryReader implements FileEntryReader {
private Optional<FileEntryReader> parent = Optional.empty();
private Optional<String> filename = Optional.empty();
private Optional<String> prodosFiletype = Optional.empty();
private Optional<Boolean> locked = Optional.empty();
private Optional<byte[]> fileData = Optional.empty();
private Optional<byte[]> resourceData = Optional.empty();
private Optional<Integer> binaryAddress = Optional.empty();
private Optional<Integer> binaryLength = Optional.empty();
private Optional<Integer> auxiliaryType = Optional.empty();
private Optional<Date> creationDate = Optional.empty();
private Optional<Date> lastModificationDate = Optional.empty();
@Override
public Optional<String> getFilename() {
return filename.or(() -> parent.map(FileEntryReader::getFilename).filter(Optional::isPresent).map(Optional::get));
}
@Override
public Optional<String> getProdosFiletype() {
return prodosFiletype.or(() -> parent.map(FileEntryReader::getProdosFiletype).filter(Optional::isPresent).map(Optional::get));
}
@Override
public Optional<Boolean> isLocked() {
return locked.or(() -> parent.map(FileEntryReader::isLocked).filter(Optional::isPresent).map(Optional::get));
}
@Override
public Optional<byte[]> getFileData() {
return fileData.or(() -> parent.map(FileEntryReader::getFileData).filter(Optional::isPresent).map(Optional::get));
}
@Override
public Optional<byte[]> getResourceData() {
// Special case, the AppleCommander API does not really handle resource forks.
return resourceData;
}
@Override
public Optional<Integer> getBinaryAddress() {
return binaryAddress.or(() -> parent.map(FileEntryReader::getBinaryAddress).filter(Optional::isPresent).map(Optional::get));
}
@Override
public Optional<Integer> getBinaryLength() {
return binaryLength.or(() -> parent.map(FileEntryReader::getBinaryLength).filter(Optional::isPresent).map(Optional::get));
}
@Override
public Optional<Integer> getAuxiliaryType() {
return auxiliaryType.or(() -> parent.map(FileEntryReader::getBinaryLength).filter(Optional::isPresent).map(Optional::get));
}
@Override
public Optional<Date> getCreationDate() {
return creationDate.or(() -> parent.map(FileEntryReader::getCreationDate).filter(Optional::isPresent).map(Optional::get));
}
@Override
public Optional<Date> getLastModificationDate() {
return lastModificationDate.or(() -> parent.map(FileEntryReader::getLastModificationDate).filter(Optional::isPresent).map(Optional::get));
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private OverrideFileEntryReader fileEntryReader = new OverrideFileEntryReader();
public FileEntryReader build(FileEntryReader parent) {
Objects.requireNonNull(parent);
fileEntryReader.parent = Optional.of(parent);
return fileEntryReader;
}
public List<FileEntryReader> buildList(FileEntryReader parent) {
return Arrays.asList(build(parent));
}
public FileEntryReader build() {
return fileEntryReader;
}
public List<FileEntryReader> buildList() {
return Arrays.asList(build());
}
public Builder filename(String filename) {
Objects.requireNonNull(filename);
fileEntryReader.filename = Optional.of(filename);
return this;
}
public Builder filename(Optional<String> filename) {
Objects.requireNonNull(filename);
fileEntryReader.filename = filename;
return this;
}
public Builder prodosFiletype(String filetype) {
Objects.requireNonNull(filetype);
fileEntryReader.prodosFiletype = Optional.of(filetype);
return this;
}
public Builder prodosFiletype(Optional<String> filetype) {
Objects.requireNonNull(filetype);
fileEntryReader.prodosFiletype = filetype;
return this;
}
public Builder locked(boolean locked) {
fileEntryReader.locked = Optional.of(locked);
return this;
}
public Builder locked(Optional<Boolean> locked) {
Objects.requireNonNull(locked);
fileEntryReader.locked = locked;
return this;
}
public Builder fileData(byte[] fileData) {
Objects.requireNonNull(fileData);
fileEntryReader.fileData = Optional.of(fileData);
return this;
}
public Builder fileData(Optional<byte[]> fileData) {
Objects.requireNonNull(fileData);
fileEntryReader.fileData = fileData;
return this;
}
public Builder resourceData(byte[] resourceData) {
Objects.requireNonNull(resourceData);
fileEntryReader.resourceData = Optional.of(resourceData);
return this;
}
public Builder resourceData(Optional<byte[]> resourceData) {
Objects.requireNonNull(resourceData);
fileEntryReader.resourceData = resourceData;
return this;
}
public Builder binaryAddress(int binaryAddress) {
fileEntryReader.binaryAddress = Optional.of(binaryAddress);
return this;
}
public Builder binaryAddress(Optional<Integer> binaryAddress) {
Objects.requireNonNull(binaryAddress);
fileEntryReader.binaryAddress = binaryAddress;
return this;
}
public Builder binaryLength(int binaryLength) {
fileEntryReader.binaryLength = Optional.of(binaryLength);
return this;
}
public Builder binaryLength(Optional<Integer> binaryLength) {
Objects.requireNonNull(binaryLength);
fileEntryReader.binaryLength = binaryLength;
return this;
}
public Builder auxiliaryType(int auxiliaryType) {
fileEntryReader.auxiliaryType = Optional.of(auxiliaryType);
return this;
}
public Builder auxiliaryType(Optional<Integer> auxiliaryType) {
Objects.requireNonNull(auxiliaryType);
fileEntryReader.auxiliaryType = auxiliaryType;
return this;
}
public Builder creationDate(Date creationDate) {
Objects.requireNonNull(creationDate);
fileEntryReader.creationDate = Optional.of(creationDate);
return this;
}
public Builder creationDate(Optional<Date> creationDate) {
Objects.requireNonNull(creationDate);
fileEntryReader.creationDate = creationDate;
return this;
}
public Builder lastModificationDate(Date lastModificationDate) {
Objects.requireNonNull(lastModificationDate);
fileEntryReader.lastModificationDate = Optional.of(lastModificationDate);
return this;
}
public Builder lastModificationDate(Optional<Date> lastModificationDate) {
Objects.requireNonNull(lastModificationDate);
fileEntryReader.lastModificationDate = lastModificationDate;
return this;
}
}
}

View File

@ -0,0 +1,82 @@
package io.github.applecommander.acx.fileutil;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
import com.webcodepro.applecommander.storage.DiskFullException;
import com.webcodepro.applecommander.storage.filters.PascalTextFileFilter;
import com.webcodepro.applecommander.storage.os.pascal.PascalFileEntry;
public class PascalFileEntryReaderWriter implements FileEntryReader, FileEntryWriter {
private static final PascalTextFileFilter TEXT_FILTER = new PascalTextFileFilter();
private static final Map<String,String> FILE_TYPES;
static {
FILE_TYPES = Map.of(
// Pascal => Prodos
"xdskfile", "BAD", // TODO we should skip bad block files
"CODE", "BIN", // TODO is there an address?
"TEXT", "TXT",
"INFO", "TXT", // TODO We should skip debugger info
"DATA", "BIN",
"GRAF", "BIN", // TODO compressed graphics image
"FOTO", "BIN", // TODO screen image
"securedir", "BIN", // TODO is this even implemented
// Prodos => Pascal
"BIN", "DATA",
"TXT", "TEXT"
);
}
private PascalFileEntry fileEntry;
public PascalFileEntryReaderWriter(PascalFileEntry fileEntry) {
this.fileEntry = fileEntry;
}
@Override
public Optional<String> getFilename() {
return Optional.ofNullable(fileEntry.getFilename());
}
@Override
public void setFilename(String filename) {
fileEntry.setFilename(filename);
}
@Override
public Optional<String> getProdosFiletype() {
return Optional.ofNullable(FILE_TYPES.get(fileEntry.getFiletype()));
}
@Override
public void setProdosFiletype(String filetype) {
fileEntry.setFiletype(FILE_TYPES.getOrDefault(filetype, "DATA"));
}
@Override
public Optional<Date> getLastModificationDate() {
return Optional.ofNullable(fileEntry.getModificationDate());
}
@Override
public void setLastModificationDate(Date date) {
fileEntry.setModificationDate(date);
}
@Override
public Optional<byte[]> getFileData() {
if ("TEXT".equals(fileEntry.getFiletype())) {
return Optional.ofNullable(TEXT_FILTER.filter(fileEntry));
}
else {
return Optional.ofNullable(fileEntry.getFileData());
}
}
@Override
public void setFileData(byte[] data) {
try {
fileEntry.setFileData(data);
} catch (DiskFullException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,116 @@
package io.github.applecommander.acx.fileutil;
import java.util.Date;
import java.util.Optional;
import com.webcodepro.applecommander.storage.DiskFullException;
import com.webcodepro.applecommander.storage.os.prodos.ProdosFileEntry;
public class ProdosFileEntryReaderWriter implements FileEntryReader, FileEntryWriter {
private ProdosFileEntry fileEntry;
public ProdosFileEntryReaderWriter(ProdosFileEntry fileEntry) {
this.fileEntry = fileEntry;
}
@Override
public Optional<String> getFilename() {
return Optional.ofNullable(fileEntry.getFilename());
}
@Override
public void setFilename(String filename) {
fileEntry.setFilename(filename);
}
@Override
public Optional<String> getProdosFiletype() {
return Optional.ofNullable(fileEntry.getFiletype());
}
@Override
public void setProdosFiletype(String filetype) {
fileEntry.setFiletype(filetype);
}
@Override
public Optional<Boolean> isLocked() {
return Optional.ofNullable(fileEntry.isLocked());
}
@Override
public void setLocked(boolean flag) {
fileEntry.setLocked(flag);
}
@Override
public Optional<byte[]> getFileData() {
return Optional.ofNullable(fileEntry.getFileData());
}
@Override
public void setFileData(byte[] data) {
try {
fileEntry.setFileData(data);
} catch (DiskFullException e) {
throw new RuntimeException(e);
}
}
@Override
public void setFileData(byte[] data, byte[] resource) {
try {
// If we have a resource fork in addition to a data fork,
// then we've got a GSOS storage type $5.
fileEntry.setFileData(data, resource);
fileEntry.setStorageType(0x05);
} catch (DiskFullException e) {
throw new RuntimeException(e);
}
}
@Override
public Optional<Integer> getBinaryAddress() {
if (fileEntry.needsAddress()) {
return Optional.ofNullable(fileEntry.getAuxiliaryType());
}
return Optional.empty();
}
@Override
public void setBinaryAddress(int address) {
if (fileEntry.needsAddress()) {
fileEntry.setAddress(address);
}
}
@Override
public Optional<Integer> getBinaryLength() {
return Optional.ofNullable(fileEntry.getSize());
}
@Override
public void setBinaryLength(int length) {
// Nothing to do
}
@Override
public Optional<Integer> getAuxiliaryType() {
return Optional.ofNullable(fileEntry.getAuxiliaryType());
}
@Override
public void setAuxiliaryType(int auxType) {
fileEntry.setAuxiliaryType(auxType);
}
@Override
public Optional<Date> getCreationDate() {
return Optional.ofNullable(fileEntry.getCreationDate());
}
@Override
public void setCreationDate(Date date) {
fileEntry.setCreationDate(date);
}
@Override
public Optional<Date> getLastModificationDate() {
return Optional.ofNullable(fileEntry.getLastModificationDate());
}
@Override
public void setLastModificationDate(Date date) {
fileEntry.setLastModificationDate(date);
}
}

View File

@ -0,0 +1,43 @@
package io.github.applecommander.acx.fileutil;
import java.util.Map;
import java.util.Optional;
import com.webcodepro.applecommander.storage.os.rdos.RdosFileEntry;
public class RdosFileEntryReader implements FileEntryReader {
private static final Map<String,String> FILE_TYPES;
static {
FILE_TYPES = Map.of(
"T", "TXT",
"A", "BAS",
"B", "BIN"
);
}
private RdosFileEntry fileEntry;
public RdosFileEntryReader(RdosFileEntry fileEntry) {
this.fileEntry = fileEntry;
}
@Override
public Optional<String> getFilename() {
return Optional.ofNullable(fileEntry.getFilename());
}
@Override
public Optional<Integer> getBinaryAddress() {
return Optional.ofNullable(fileEntry.getAddress());
}
@Override
public Optional<String> getProdosFiletype() {
return Optional.ofNullable(FILE_TYPES.get(fileEntry.getFiletype()));
}
@Override
public Optional<byte[]> getFileData() {
return Optional.ofNullable(fileEntry.getFileData());
}
}

View File

@ -0,0 +1,205 @@
package io.github.applecommander.filestreamer;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Spliterators;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import com.webcodepro.applecommander.storage.Disk;
import com.webcodepro.applecommander.storage.DiskException;
import com.webcodepro.applecommander.storage.DiskUnrecognizedException;
import com.webcodepro.applecommander.storage.FileEntry;
import com.webcodepro.applecommander.storage.FormattedDisk;
/**
* FileStreamer is utility class that will (optionally) recurse through all directories and
* feed a Java Stream of useful directory walking detail (disk, directory, file, and the
* textual path to get there).
* <p>
* Sample usage:
* <pre>
* FileStreamer.forDisk(image)
* .ignoreErrors(true)
* .stream()
* .filter(this::fileFilter)
* .forEach(fileHandler);
* </pre>
*
* @author rob
*/
public class FileStreamer {
private static final Consumer<FormattedDisk> NOOP_CONSUMER = d -> {};
public static FileStreamer forDisk(File file) throws IOException, DiskUnrecognizedException {
return forDisk(file.getPath());
}
public static FileStreamer forDisk(String fileName) throws IOException, DiskUnrecognizedException {
return new FileStreamer(new Disk(fileName));
}
public static FileStreamer forDisk(Disk disk) throws DiskUnrecognizedException {
return new FileStreamer(disk);
}
private FormattedDisk[] formattedDisks = null;
// Processor flags (used in gathering)
private boolean ignoreErrorsFlag = false;
private boolean recursiveFlag = true;
// Processor events
private Consumer<FormattedDisk> beforeDisk = NOOP_CONSUMER;
private Consumer<FormattedDisk> afterDisk = NOOP_CONSUMER;
// Filters
private Predicate<FileTuple> filters = this::deletedFileFilter;
private boolean includeDeletedFlag = false;
private List<PathMatcher> pathMatchers = new ArrayList<>();
private FileStreamer(Disk disk) throws DiskUnrecognizedException {
this.formattedDisks = disk.getFormattedDisks();
}
public FileStreamer ignoreErrors(boolean flag) {
this.ignoreErrorsFlag = flag;
return this;
}
public FileStreamer recursive(boolean flag) {
this.recursiveFlag = flag;
return this;
}
public FileStreamer matchGlobs(List<String> globs) {
if (globs != null && !globs.isEmpty()) {
FileSystem fs = FileSystems.getDefault();
for (String glob : globs) {
pathMatchers.add(fs.getPathMatcher("glob:" + glob));
}
this.filters = filters.and(this::globFilter);
}
return this;
}
public FileStreamer matchGlobs(String... globs) {
return matchGlobs(Arrays.asList(globs));
}
public FileStreamer includeTypeOfFile(TypeOfFile type) {
this.filters = filters.and(type.predicate);
return this;
}
public FileStreamer includeDeleted(boolean flag) {
this.includeDeletedFlag = flag;
return this;
}
public FileStreamer beforeDisk(Consumer<FormattedDisk> consumer) {
this.beforeDisk = consumer;
return this;
}
public FileStreamer afterDisk(Consumer<FormattedDisk> consumer) {
this.afterDisk = consumer;
return this;
}
public Stream<FileTuple> stream() {
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator(), 0), false)
.filter(filters);
}
public Iterator<FileTuple> iterator() {
return new FileTupleIterator();
}
protected boolean deletedFileFilter(FileTuple tuple) {
return includeDeletedFlag || !tuple.fileEntry.isDeleted();
}
protected boolean globFilter(FileTuple tuple) {
if (tuple.fileEntry.isDirectory()) {
// If we don't match directories, no files can be listed.
return true;
}
// This may cause issues, but Path is a "real" filesystem construct, so the delimiters
// vary by OS (likely just "/" and "\"). However, Java also erases them to some degree,
// so using "/" (as used in ProDOS) will likely work out.
// Also note that we check the single file "PARMS.S" and full path "SOURCE/PARMS.S" since
// the user might have entered "*.S" or something like "SOURCE/PARMS.S".
FileSystem fs = FileSystems.getDefault();
Path filePath = Paths.get(tuple.fileEntry.getFilename());
Path fullPath = Paths.get(String.join(fs.getSeparator(), tuple.paths),
tuple.fileEntry.getFilename());
for (PathMatcher pathMatcher : pathMatchers) {
if (pathMatcher.matches(filePath) || pathMatcher.matches(fullPath)) return true;
}
return false;
}
private class FileTupleIterator implements Iterator<FileTuple> {
private LinkedList<FileTuple> files = new LinkedList<>();
private FormattedDisk currentDisk;
private FileTupleIterator() {
for (FormattedDisk formattedDisk : formattedDisks) {
files.addAll(toTupleList(FileTuple.of(formattedDisk)));
}
}
@Override
public boolean hasNext() {
boolean hasNext = !files.isEmpty();
if (hasNext) {
FileTuple tuple = files.peek();
// Was there a disk switch?
if (tuple.formattedDisk != currentDisk) {
if (currentDisk != null) {
afterDisk.accept(currentDisk);
}
currentDisk = tuple.formattedDisk;
beforeDisk.accept(currentDisk);
}
} else {
if (currentDisk != null) {
afterDisk.accept(currentDisk);
}
currentDisk = null;
}
return hasNext;
}
@Override
public FileTuple next() {
if (hasNext()) {
FileTuple tuple = files.removeFirst();
if (recursiveFlag && tuple.fileEntry.isDirectory()) {
FileTuple newTuple = tuple.pushd(tuple.fileEntry);
files.addAll(0, toTupleList(newTuple));
}
return tuple;
} else {
throw new NoSuchElementException();
}
}
private List<FileTuple> toTupleList(FileTuple tuple) {
List<FileTuple> list = new ArrayList<>();
try {
for (FileEntry fileEntry : tuple.directoryEntry.getFiles()) {
list.add(tuple.of(fileEntry));
}
} catch (DiskException e) {
if (!ignoreErrorsFlag) {
throw new RuntimeException(e);
}
}
return list;
}
}
}

View File

@ -0,0 +1,42 @@
package io.github.applecommander.filestreamer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
import com.webcodepro.applecommander.storage.DirectoryEntry;
import com.webcodepro.applecommander.storage.FileEntry;
import com.webcodepro.applecommander.storage.FormattedDisk;
public class FileTuple {
private static final Logger LOG = Logger.getLogger(FileTuple.class.getName());
public final FormattedDisk formattedDisk;
public final List<String> paths;
public final DirectoryEntry directoryEntry;
public final FileEntry fileEntry;
private FileTuple(FormattedDisk formattedDisk,
List<String> paths,
DirectoryEntry directoryEntry,
FileEntry fileEntry) {
this.formattedDisk = formattedDisk;
this.paths = Collections.unmodifiableList(paths);
this.directoryEntry = directoryEntry;
this.fileEntry = fileEntry;
}
public FileTuple pushd(FileEntry directoryEntry) {
LOG.fine("Adding directory " + directoryEntry.getFilename());
List<String> newPaths = new ArrayList<>(paths);
newPaths.add(directoryEntry.getFilename());
return new FileTuple(formattedDisk, newPaths, (DirectoryEntry)directoryEntry, null);
}
public FileTuple of(FileEntry fileEntry) {
return new FileTuple(formattedDisk, paths, directoryEntry, fileEntry);
}
public static FileTuple of(FormattedDisk disk) {
return new FileTuple(disk, new ArrayList<String>(), (DirectoryEntry)disk, null);
}
}

View File

@ -0,0 +1,15 @@
package io.github.applecommander.filestreamer;
import java.util.function.Predicate;
public enum TypeOfFile {
FILE(tuple -> !tuple.fileEntry.isDirectory()),
DIRECTORY(tuple -> tuple.fileEntry.isDirectory()),
BOTH(tuple -> true);
public final Predicate<FileTuple> predicate;
private TypeOfFile(Predicate<FileTuple> predicate) {
this.predicate = predicate;
}
}

View File

@ -0,0 +1,84 @@
package io.github.applecommander.filters;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import com.webcodepro.applecommander.storage.FileEntry;
import com.webcodepro.applecommander.storage.FileFilter;
import com.webcodepro.applecommander.storage.os.dos33.DosFileEntry;
import com.webcodepro.applecommander.storage.os.prodos.ProdosFileEntry;
import io.github.applecommander.applesingle.AppleSingle;
/**
* A FileFilter to write each file to an independent AppleSingle file.
*/
public class AppleSingleFileFilter implements FileFilter {
@Override
public byte[] filter(FileEntry fileEntry) {
try {
AppleSingle.Builder builder = AppleSingle.builder()
.dataFork(fileEntry.getFileData())
.realName(fileEntry.getFilename());
if (fileEntry instanceof ProdosFileEntry) {
handleProDOS(builder, (ProdosFileEntry)fileEntry);
}
else if (fileEntry instanceof DosFileEntry) {
handleDOS(builder, (DosFileEntry)fileEntry);
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
builder.build().save(baos);
return baos.toByteArray();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
protected void handleProDOS(AppleSingle.Builder builder, ProdosFileEntry prodos) {
// We can't get the "access" byte so reconstructing it...
int access = (prodos.canDestroy() ? 0x80 : 0x00)
| (prodos.canRename() ? 0x40 : 0x00)
| (prodos.canWrite() ? 0x02 : 0x00)
| (prodos.canRead() ? 0x01 : 0x00);
builder.access(access);
builder.auxType(prodos.getAuxiliaryType());
builder.creationDate(prodos.getCreationDate().toInstant());
builder.fileType(prodos.getFiletypeByte());
builder.modificationDate(prodos.getLastModificationDate().toInstant());
}
protected void handleDOS(AppleSingle.Builder builder, DosFileEntry dos) {
switch (dos.getFiletype()) {
// 0x00 T
case "T":
builder.fileType(0x04); // TXT
break;
// 0x01 I
case "I":
builder.fileType(0xfa); // INT
break;
// 0x02 A
case "A":
builder.fileType(0xfc); // BAS
break;
// 0x04 B
case "B":
builder.fileType(0x06); // BIN
//builder.auxType(???) // FIXME address is not exposed?
break;
// The rest we just default
default:
builder.fileType(0xf1); // $F1 (???)
break;
}
builder.access(dos.isLocked() ? 0x01 : 0xc3);
}
@Override
public String getSuggestedFileName(FileEntry fileEntry) {
String fileName = fileEntry.getFilename().trim();
if (!fileName.toLowerCase().endsWith(".as")) {
fileName = fileName + ".as";
}
return fileName;
}
}

View File

@ -0,0 +1,25 @@
package io.github.applecommander.filters;
import com.webcodepro.applecommander.storage.FileEntry;
import com.webcodepro.applecommander.storage.FileFilter;
/**
* A custom FileFilter to dump "raw" data from the disk.
* This filter uses the filename as given on the Disk with
* no additional extensions.
*
* @author rob
*/
public class RawFileFilter implements FileFilter {
@Override
public byte[] filter(FileEntry fileEntry) {
return fileEntry.getFileData();
}
@Override
public String getSuggestedFileName(FileEntry fileEntry) {
return fileEntry.getFilename();
}
}

View File

@ -0,0 +1,27 @@
package io.github.applecommander.acx.converter;
import static io.github.applecommander.acx.converter.DataSizeConverter.KB;
import static io.github.applecommander.acx.converter.DataSizeConverter.MB;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class DataSizeConverterTest {
@Test
public void testFormat() {
assertEquals("1B", DataSizeConverter.format(1));
assertEquals("100B", DataSizeConverter.format(100));
assertEquals("2KB", DataSizeConverter.format(2*KB));
assertEquals("140KB", DataSizeConverter.format(140*KB));
assertEquals("800KB", DataSizeConverter.format(800*KB));
assertEquals("5MB", DataSizeConverter.format(5*MB));
}
@Test
public void testConvert() throws Exception {
DataSizeConverter converter = new DataSizeConverter();
assertEquals(140*KB, (int)converter.convert("140kb"));
assertEquals(800*KB, (int)converter.convert("800KB"));
assertEquals(5*MB, (int)converter.convert("5Mb"));
}
}

View File

@ -0,0 +1,85 @@
package io.github.applecommander.filestreamer;
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import com.webcodepro.applecommander.storage.DiskUnrecognizedException;
import org.junit.Test;
public class FileStreamerTest {
private static final List<String> EXPECTED_MERLIN = Arrays.asList(
"PRODOS", "MERLIN.SYSTEM", "PARMS", "ED", "ED.16",
"SOURCEROR", "SOURCEROR/OBJ", "SOURCEROR/LABELS", "SOURCEROR/LABELS.S",
"LIBRARY", "LIBRARY/SENDMSG.S", "LIBRARY/PRDEC.S", "LIBRARY/FPMACROS.S",
"LIBRARY/MACROS.S", "LIBRARY/ROCKWELL.S",
"SOURCE", "SOURCE/PARMS.S", "SOURCE/EDMAC.S", "SOURCE/KEYMAC.S",
"SOURCE/PRINTFILER.S", "SOURCE/MAKE.DUMP.S", "SOURCE/CLOCK.S",
"SOURCE/PI.START.S", "SOURCE/PI.MAIN.S", "SOURCE/PI.LOOK.S",
"SOURCE/PI.DIV.S", "SOURCE/PI.ADD.S", "SOURCE/PI.MACS.S",
"SOURCE/PI.NAMES.S",
"UTILITIES", "UTILITIES/REMOVE.ED", "UTILITIES/EDMAC", "UTILITIES/CLOCK.12.ED",
"UTILITIES/XREF", "UTILITIES/XREFA", "UTILITIES/FORMATTER",
"UTILITIES/PRINTFILER", "UTILITIES/MON.65C02", "UTILITIES/MAKE.DUMP",
"UTILITIES/CONV.REL.LNK", "UTILITIES/CONV.LNK.REL",
"UTILITIES/CLR.HI.BIT", "UTILITIES/KEYMAC",
"PI", "PI/NAMES", "PI/START", "PI/MAIN", "PI/LOOK", "PI/DIV", "PI/ADD", "PI/OBJ"
);
private static final List<String> EXPECTED_UNIDOS = Arrays.asList(
"HELLO", "FORMATTER", "FORMATTER.OBJ", "MFID", "FUD", // Disk #1
"HELLO", "MFID", "FUD" // Disk #2
);
@Test
public void testRecursiveListMerlin() throws DiskUnrecognizedException, IOException {
List<String> actual =
FileStreamer.forDisk("./src/test/resources/disks/MERLIN8PRO1.DSK")
.recursive(true)
.stream()
.map(this::makeFullPath)
.collect(Collectors.toList());
assertEquals(EXPECTED_MERLIN, actual);
}
@Test
public void testNonRecursiveListMerlin() throws DiskUnrecognizedException, IOException {
List<String> actual =
FileStreamer.forDisk("./src/test/resources/disks/MERLIN8PRO1.DSK")
.recursive(false)
.stream()
.map(this::makeFullPath)
.collect(Collectors.toList());
List<String> expected = EXPECTED_MERLIN.stream()
.filter(s -> !s.contains("/"))
.collect(Collectors.toList());
assertEquals(expected, actual);
}
@Test
public void testListUnidos() throws DiskUnrecognizedException, IOException {
List<String> actual =
FileStreamer.forDisk("./src/test/resources/disks/UniDOS_3.3.dsk")
.recursive(true)
.stream()
.map(this::makeFullPath)
.collect(Collectors.toList());
assertEquals(EXPECTED_UNIDOS, actual);
}
private String makeFullPath(FileTuple tuple) {
if (tuple.paths == null || tuple.paths.isEmpty()) {
return tuple.fileEntry.getFilename();
} else {
return String.join("/", String.join("/", tuple.paths), tuple.fileEntry.getFilename());
}
}
}

View File

@ -0,0 +1,31 @@
package io.github.applecommander.filestreamer;
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import java.util.Arrays;
import com.webcodepro.applecommander.storage.Disk;
import com.webcodepro.applecommander.storage.DiskException;
import com.webcodepro.applecommander.storage.FileEntry;
import com.webcodepro.applecommander.storage.FormattedDisk;
import org.junit.Test;
public class FileTupleTest {
@Test
public void test() throws IOException, DiskException {
Disk disk = new Disk("./src/test/resources/disks/MERLIN8PRO1.DSK");
FormattedDisk formattedDisk = disk.getFormattedDisks()[0];
FileTuple tuple = FileTuple.of(formattedDisk);
FileEntry sourcerorDir = tuple.formattedDisk.getFile("SOURCEROR");
tuple = tuple.pushd(sourcerorDir);
FileEntry labelsSource = tuple.directoryEntry.getFiles().get(2);
tuple = tuple.of(labelsSource);
assertEquals(Arrays.asList("SOURCEROR"), tuple.paths);
assertEquals(formattedDisk, tuple.formattedDisk);
assertEquals(sourcerorDir, tuple.directoryEntry);
assertEquals(labelsSource, tuple.fileEntry);
}
}

Binary file not shown.

Binary file not shown.

View File

@ -1,8 +1,11 @@
plugins {
id 'org.springframework.boot' version '2.6.1'
id 'org.springframework.boot' version "$springBoot"
id 'application'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}

View File

@ -1,5 +1,5 @@
plugins {
id 'org.springframework.boot' version '2.6.1'
id 'org.springframework.boot' version "$springBoot"
id 'application'
}

View File

@ -1,8 +1,11 @@
plugins {
id 'org.springframework.boot' version '2.6.1'
id 'org.springframework.boot' version "$springBoot"
id 'application'
}
sourceCompatibility = 11
targetCompatibility = 11
repositories {
mavenCentral()
}

View File

@ -1,8 +1,11 @@
plugins {
id 'org.springframework.boot' version '2.6.1'
id 'org.springframework.boot' version "$springBoot"
id 'application'
}
sourceCompatibility = 11
targetCompatibility = 11
repositories {
mavenCentral()
}

View File

@ -1,8 +1,11 @@
plugins {
id 'org.springframework.boot' version '2.6.1'
id 'org.springframework.boot' version "$springBoot"
id 'application'
}
sourceCompatibility = 11
targetCompatibility = 11
repositories {
mavenCentral()
}

View File

@ -1,8 +1,11 @@
plugins {
id 'org.springframework.boot' version '2.6.1'
id 'org.springframework.boot' version "$springBoot"
id 'application'
}
sourceCompatibility = 11
targetCompatibility = 11
repositories {
mavenCentral()
}

View File

@ -1,8 +1,11 @@
plugins {
id 'org.springframework.boot' version '2.6.1'
id 'org.springframework.boot' version "$springBoot"
id 'application'
}
sourceCompatibility = 11
targetCompatibility = 11
repositories {
mavenCentral()
}

View File

@ -13,3 +13,5 @@ antVersion=1.8.2
commonsLang3Version=3.7
commonsCsvVersion=1.8
gsonVersion=2.8.6
picocliVersion=4.6.2
springBoot=2.6.1

View File

@ -7,6 +7,9 @@ plugins {
ext.isSnapshotVersion = version.endsWith("SNAPSHOT")
ext.isReleaseVersion = !ext.isSnapshotVersion
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}

View File

@ -2,6 +2,9 @@ plugins {
id 'java-library'
}
sourceCompatibility = 11
targetCompatibility = 11
repositories {
mavenCentral()
}

View File

@ -2,6 +2,7 @@ rootProject.name = "AppleCommander"
include ':lib:ac-api'
include ':lib:ac-swt-common'
include ':app:ac-acx'
include ':app:ac-cli'
include ':app:ac-swing'
include ':app:ac-swt-macosx-aarch64'