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);