mirror of
https://github.com/AppleCommander/AppleCommander.git
synced 2024-09-15 18:54:52 +00:00
Merging in 'acx' tool, merging gradle builds
This commit is contained in:
parent
7469d90c8a
commit
acf3874185
150
app/ac-acx/README.md
Normal file
150
app/ac-acx/README.md
Normal 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
36
app/ac-acx/build.gradle
Normal 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')
|
||||
}
|
BIN
app/ac-acx/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
app/ac-acx/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
app/ac-acx/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
app/ac-acx/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
|
101
app/ac-acx/src/main/java/io/github/applecommander/acx/Main.java
Normal file
101
app/ac-acx/src/main/java/io/github/applecommander/acx/Main.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||