mirror of
https://github.com/AppleCommander/applesingle.git
synced 2024-12-27 21:31:34 +00:00
Adding CLI; fixed a few glitches.
This commit is contained in:
parent
307c5eab3f
commit
70b476b561
125
README.md
125
README.md
@ -3,6 +3,131 @@
|
|||||||
This project is an off-shoot of AppleCommander's support for the AppleSingle format in that there are multiple
|
This project is an off-shoot of AppleCommander's support for the AppleSingle format in that there are multiple
|
||||||
Java-based tools that can benefit from a pre-built library to support the AppleSingle format.
|
Java-based tools that can benefit from a pre-built library to support the AppleSingle format.
|
||||||
|
|
||||||
|
# Java Usage Examples
|
||||||
|
|
||||||
|
## Read AppleSingle
|
||||||
|
|
||||||
|
Use the factory method to...
|
||||||
|
|
||||||
|
Reading from standard input:
|
||||||
|
|
||||||
|
```java
|
||||||
|
AppleSingle as = AppleSingle.read(System.in);
|
||||||
|
```
|
||||||
|
|
||||||
|
Reading from a file:
|
||||||
|
|
||||||
|
```java
|
||||||
|
File file = new File("myfile.as");
|
||||||
|
AppleSingle as = AppleSingle.read(file);
|
||||||
|
```
|
||||||
|
|
||||||
|
The AppleSingle file can be read from an `InputStream`, `File`, `Path`, or just a byte array.
|
||||||
|
|
||||||
|
## Create AppleSingle
|
||||||
|
|
||||||
|
Use the builder to create a new AppleSingle file and then save it...
|
||||||
|
|
||||||
|
```java
|
||||||
|
AppleSingle as = AppleSingle.builder()
|
||||||
|
.dataFork(dataFork)
|
||||||
|
.realName(realName)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Path file = Paths.get("mynewfile.as");
|
||||||
|
as.save(file);
|
||||||
|
```
|
||||||
|
|
||||||
|
The `save(...)` method can save to a `File`, `Path`, or an `OutputStream`.
|
||||||
|
|
||||||
|
# Command-Line Examples
|
||||||
|
|
||||||
|
For the included command-line utility, we are using `asu` for the name.
|
||||||
|
`as` is the GNU Assembler while `applesingle` is already on Macintoshes.
|
||||||
|
Hopefully that will prevent some confusion!
|
||||||
|
|
||||||
|
Note that all runs are with the `asu` alias defined as `alias asu='java -jar build/libs/applesingle-1.0.0.jar'`
|
||||||
|
(adjust as necessary).
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ asu
|
||||||
|
Usage: asu [-hV] [--debug] [COMMAND]
|
||||||
|
|
||||||
|
AppleSingle utility
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--debug Dump full stack trackes if an error occurs
|
||||||
|
-h, --help Show this help message and exit.
|
||||||
|
-V, --version Print version information and exit.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
help Displays help information about the specified command
|
||||||
|
info Display information about an AppleSingle file
|
||||||
|
create Create an AppleSingle file
|
||||||
|
extract Extract contents of an AppleSingle file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subcommand help
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ asu info --help
|
||||||
|
Usage: asu info [-h] [--stdin] [<file>]
|
||||||
|
|
||||||
|
Display information about an AppleSingle file
|
||||||
|
Please include a file name or indicate stdin should be read, but not both.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
[<file>] File to process
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--stdin Read AppleSingle from stdin.
|
||||||
|
-h, --help Show help for subcommand
|
||||||
|
```
|
||||||
|
|
||||||
|
## Info subcommand
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ asu info src/test/resources/hello.applesingle.bin
|
||||||
|
Real Name: -Unknown-
|
||||||
|
ProDOS info:
|
||||||
|
Access: 0xC3
|
||||||
|
File Type: 0x06
|
||||||
|
Auxtype: 0x0803
|
||||||
|
Data Fork: Present, 2,912 bytes
|
||||||
|
Resource Fork: Not present
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sample runs
|
||||||
|
|
||||||
|
Using pipes to create a text file and display information. Note that the invalid `my-text-file` was changed to `MY.TEXT.FILE`.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ echo "Hello World!" | asu create --name my-text-file --stdout --filetype 0x06 --stdin-fork=data --fix-text | asu info --stdin
|
||||||
|
Real Name: MY.TEXT.FILE
|
||||||
|
ProDOS info:
|
||||||
|
Access: 0xC3
|
||||||
|
File Type: 0x06
|
||||||
|
Auxtype: 0x0000
|
||||||
|
Data Fork: Present, 13 bytes
|
||||||
|
Resource Fork: Not present
|
||||||
|
```
|
||||||
|
|
||||||
|
The `--fix-text` file flips the high-bit and translates the newline character.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ echo "Hello World!" | asu create --name my-text-file --stdout --filetype 0x06 --stdin-fork=data --fix-text | hexdump -C
|
||||||
|
00000000 00 05 16 00 00 02 00 00 00 00 00 00 00 00 00 00 |................|
|
||||||
|
00000010 00 00 00 00 00 00 00 00 00 03 00 00 00 03 00 00 |................|
|
||||||
|
00000020 00 3e 00 00 00 0c 00 00 00 0b 00 00 00 4a 00 00 |.>...........J..|
|
||||||
|
00000030 00 08 00 00 00 01 00 00 00 52 00 00 00 0d 4d 59 |.........R....MY|
|
||||||
|
00000040 2e 54 45 58 54 2e 46 49 4c 45 00 c3 00 06 00 00 |.TEXT.FILE......|
|
||||||
|
00000050 00 00 c8 e5 ec ec ef a0 d7 ef f2 ec e4 a1 8d |...............|
|
||||||
|
0000005f
|
||||||
|
```
|
||||||
|
(The message is at 0x52 through 0x5e.)
|
||||||
|
|
||||||
# Resources
|
# Resources
|
||||||
|
|
||||||
* [AppleSingle spec](http://kaiser-edv.de/documents/AppleSingle_AppleDouble.pdf)
|
* [AppleSingle spec](http://kaiser-edv.de/documents/AppleSingle_AppleDouble.pdf)
|
||||||
|
19
build.gradle
19
build.gradle
@ -1,3 +1,7 @@
|
|||||||
|
plugins {
|
||||||
|
id 'org.springframework.boot' version '2.0.2.RELEASE'
|
||||||
|
}
|
||||||
|
|
||||||
apply plugin: 'java'
|
apply plugin: 'java'
|
||||||
apply plugin: 'application'
|
apply plugin: 'application'
|
||||||
apply plugin: 'maven'
|
apply plugin: 'maven'
|
||||||
@ -7,7 +11,18 @@ repositories {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
mainClassName = "io.github.applecommander.applesingle.tools.asu.Main"
|
||||||
testImplementation 'junit:junit:4.12'
|
|
||||||
|
bootJar {
|
||||||
|
manifest {
|
||||||
|
attributes(
|
||||||
|
'Implementation-Title': 'applesingle',
|
||||||
|
'Implementation-Version': "${version} (${new Date().format('yyyy-MM-dd HH:mm')})"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile 'info.picocli:picocli:3.0.2'
|
||||||
|
testImplementation 'junit:junit:4.12'
|
||||||
|
}
|
||||||
|
8
gradle.properties
Normal file
8
gradle.properties
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Universal applesingle version number. Used for:
|
||||||
|
# - Naming JAR file.
|
||||||
|
# - The build will insert this into a file that is read at run time as well.
|
||||||
|
version=1.0.0
|
||||||
|
|
||||||
|
# Maven Central Repository G and A of GAV coordinate. :-)
|
||||||
|
group=net.sf.applecommander
|
||||||
|
archivesBaseName=AppleCommander
|
@ -114,22 +114,22 @@ public class AppleSingle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void save(OutputStream outputStream) throws IOException {
|
public void save(OutputStream outputStream) throws IOException {
|
||||||
// Support real name, prodos file info, resource fork (if present), data fork (assumed)
|
|
||||||
final boolean hasResourceFork = resourceFork == null ? false : true;
|
final boolean hasResourceFork = resourceFork == null ? false : true;
|
||||||
final int entries = 3 + (hasResourceFork ? 1 : 0);
|
final boolean hasRealName = realName == null ? false : true;
|
||||||
|
final int entries = 2 + (hasRealName ? 1 : 0) + (hasResourceFork ? 1 : 0);
|
||||||
|
|
||||||
int realNameOffset = 26 + (12 * entries);;
|
int realNameOffset = 26 + (12 * entries);
|
||||||
int prodosFileInfoOffset = realNameOffset + realName.length();
|
int prodosFileInfoOffset = realNameOffset + (hasRealName ? realName.length() : 0);
|
||||||
int resourceForkOffset = prodosFileInfoOffset + 8;
|
int resourceForkOffset = prodosFileInfoOffset + 8;
|
||||||
int dataForkOffset = resourceForkOffset + (hasResourceFork ? resourceFork.length : 0);
|
int dataForkOffset = resourceForkOffset + (hasResourceFork ? resourceFork.length : 0);
|
||||||
|
|
||||||
writeFileHeader(outputStream, entries);
|
writeFileHeader(outputStream, entries);
|
||||||
writeHeader(outputStream, 3, realNameOffset, realName.length());
|
if (hasRealName) writeHeader(outputStream, 3, realNameOffset, realName.length());
|
||||||
writeHeader(outputStream, 11, prodosFileInfoOffset, 8);
|
writeHeader(outputStream, 11, prodosFileInfoOffset, 8);
|
||||||
if (hasResourceFork) writeHeader(outputStream, 2, resourceForkOffset, resourceFork.length);
|
if (hasResourceFork) writeHeader(outputStream, 2, resourceForkOffset, resourceFork.length);
|
||||||
writeHeader(outputStream, 1, dataForkOffset, dataFork.length);
|
writeHeader(outputStream, 1, dataForkOffset, dataFork.length);
|
||||||
|
|
||||||
writeRealName(outputStream);
|
if (hasRealName) writeRealName(outputStream);
|
||||||
writeProdosFileInfo(outputStream);
|
writeProdosFileInfo(outputStream);
|
||||||
if (hasResourceFork) writeResourceFork(outputStream);
|
if (hasResourceFork) writeResourceFork(outputStream);
|
||||||
writeDataFork(outputStream);
|
writeDataFork(outputStream);
|
||||||
@ -168,7 +168,7 @@ public class AppleSingle {
|
|||||||
ByteBuffer buf = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer buf = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN);
|
||||||
buf.putShort((short)prodosFileInfo.access);
|
buf.putShort((short)prodosFileInfo.access);
|
||||||
buf.putShort((short)prodosFileInfo.fileType);
|
buf.putShort((short)prodosFileInfo.fileType);
|
||||||
buf.putInt(prodosFileInfo.fileType);
|
buf.putInt(prodosFileInfo.auxType);
|
||||||
outputStream.write(buf.array());
|
outputStream.write(buf.array());
|
||||||
}
|
}
|
||||||
private void writeResourceFork(OutputStream outputStream) throws IOException {
|
private void writeResourceFork(OutputStream outputStream) throws IOException {
|
||||||
@ -179,22 +179,15 @@ public class AppleSingle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static AppleSingle read(InputStream inputStream) throws IOException {
|
public static AppleSingle read(InputStream inputStream) throws IOException {
|
||||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
Objects.requireNonNull(inputStream, "Please supply an input stream");
|
||||||
while (true) {
|
return read(AppleSingle.toByteArray(inputStream));
|
||||||
byte[] buf = new byte[1024];
|
|
||||||
int len = inputStream.read(buf);
|
|
||||||
if (len == -1) break;
|
|
||||||
outputStream.write(buf, 0, len);
|
|
||||||
}
|
|
||||||
outputStream.flush();
|
|
||||||
return read(outputStream.toByteArray());
|
|
||||||
}
|
}
|
||||||
public static AppleSingle read(File file) throws IOException {
|
public static AppleSingle read(File file) throws IOException {
|
||||||
Objects.requireNonNull(file);
|
Objects.requireNonNull(file, "Please supply a file");
|
||||||
return read(file.toPath());
|
return read(file.toPath());
|
||||||
}
|
}
|
||||||
public static AppleSingle read(Path path) throws IOException {
|
public static AppleSingle read(Path path) throws IOException {
|
||||||
Objects.requireNonNull(path);
|
Objects.requireNonNull(path, "Please supply a file");
|
||||||
return new AppleSingle(Files.readAllBytes(path));
|
return new AppleSingle(Files.readAllBytes(path));
|
||||||
}
|
}
|
||||||
public static AppleSingle read(byte[] data) throws IOException {
|
public static AppleSingle read(byte[] data) throws IOException {
|
||||||
@ -208,9 +201,22 @@ public class AppleSingle {
|
|||||||
public static class Builder {
|
public static class Builder {
|
||||||
private AppleSingle as = new AppleSingle();
|
private AppleSingle as = new AppleSingle();
|
||||||
public Builder realName(String realName) {
|
public Builder realName(String realName) {
|
||||||
as.realName = realName;
|
if (!Character.isAlphabetic(realName.charAt(0))) {
|
||||||
|
throw new IllegalArgumentException("ProDOS file names must begin with a letter");
|
||||||
|
}
|
||||||
|
as.realName = realName.chars()
|
||||||
|
.map(this::sanitize)
|
||||||
|
.limit(15)
|
||||||
|
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
|
||||||
|
.toString();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
private int sanitize(int ch) {
|
||||||
|
if (Character.isAlphabetic(ch) || Character.isDigit(ch)) {
|
||||||
|
return Character.toUpperCase(ch);
|
||||||
|
}
|
||||||
|
return '.';
|
||||||
|
}
|
||||||
public Builder dataFork(byte[] dataFork) {
|
public Builder dataFork(byte[] dataFork) {
|
||||||
as.dataFork = dataFork;
|
as.dataFork = dataFork;
|
||||||
return this;
|
return this;
|
||||||
@ -235,4 +241,17 @@ public class AppleSingle {
|
|||||||
return as;
|
return as;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Utility method to read all bytes from an InputStream. May move if more utility methods appear. */
|
||||||
|
public static byte[] toByteArray(InputStream inputStream) throws IOException {
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
while (true) {
|
||||||
|
byte[] buf = new byte[1024];
|
||||||
|
int len = inputStream.read(buf);
|
||||||
|
if (len == -1) break;
|
||||||
|
outputStream.write(buf, 0, len);
|
||||||
|
}
|
||||||
|
outputStream.flush();
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ public class ProdosFileInfo {
|
|||||||
int auxType;
|
int auxType;
|
||||||
|
|
||||||
public static ProdosFileInfo standardBIN() {
|
public static ProdosFileInfo standardBIN() {
|
||||||
return new ProdosFileInfo(0xc3, 0x04, 0x000);
|
return new ProdosFileInfo(0xc3, 0x06, 0x0000);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProdosFileInfo(int access, int fileType, int auxType) {
|
public ProdosFileInfo(int access, int fileType, int auxType) {
|
||||||
|
@ -1 +0,0 @@
|
|||||||
package io.github.applecommander.applesingle;
|
|
@ -0,0 +1,135 @@
|
|||||||
|
package io.github.applecommander.applesingle.tools.asu;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
import io.github.applecommander.applesingle.AppleSingle;
|
||||||
|
import picocli.CommandLine.Command;
|
||||||
|
import picocli.CommandLine.Option;
|
||||||
|
import picocli.CommandLine.Parameters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supports creation of AppleSingle archives.
|
||||||
|
*/
|
||||||
|
@Command(name = "create", description = { "Create an AppleSingle file" },
|
||||||
|
parameterListHeading = "%nParameters:%n",
|
||||||
|
descriptionHeading = "%n",
|
||||||
|
optionListHeading = "%nOptions:%n")
|
||||||
|
public class CreateCommand implements Callable<Void> {
|
||||||
|
@Option(names = { "-h", "--help" }, description = "Show help for subcommand", usageHelp = true)
|
||||||
|
private boolean helpFlag;
|
||||||
|
|
||||||
|
@Option(names = "--stdout", description = "Write AppleSingle file to stdout")
|
||||||
|
private boolean stdoutFlag;
|
||||||
|
|
||||||
|
@Option(names = "--stdin-fork", description = "Read fork from stdin (specify data or resource)")
|
||||||
|
private ForkType stdinForkType;
|
||||||
|
|
||||||
|
@Option(names = "--fix-text", description = "Set the high bit and fix line endings")
|
||||||
|
private boolean fixTextFlag;
|
||||||
|
|
||||||
|
@Option(names = "--data-fork", description = "Read data fork from file")
|
||||||
|
private Path dataForkFile;
|
||||||
|
|
||||||
|
@Option(names = "--resource-fork", description = "Read resource fork from file")
|
||||||
|
private Path resourceForkFile;
|
||||||
|
|
||||||
|
@Option(names = "--name", description = "Set the filename (defaults to name of data fork, if supplied)")
|
||||||
|
private String realName;
|
||||||
|
|
||||||
|
@Option(names = "--access", description = "Set the ProDOS access flags", converter = IntegerTypeConverter.class)
|
||||||
|
private Integer access;
|
||||||
|
|
||||||
|
@Option(names = "--filetype", description = "Set the ProDOS file type", converter = IntegerTypeConverter.class)
|
||||||
|
private Integer filetype;
|
||||||
|
|
||||||
|
@Option(names = "--auxtype", description = "Set the ProDOS auxtype", converter = IntegerTypeConverter.class)
|
||||||
|
private Integer auxtype;
|
||||||
|
|
||||||
|
@Parameters(arity = "0..1", description = "AppleSingle file to create")
|
||||||
|
private Path file;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Void call() throws IOException {
|
||||||
|
validateArguments();
|
||||||
|
|
||||||
|
byte[] dataFork = prepDataFork();
|
||||||
|
byte[] resourceFork = prepResourceFork();
|
||||||
|
|
||||||
|
AppleSingle applesingle = buildAppleSingle(dataFork, resourceFork);
|
||||||
|
writeAppleSingle(applesingle);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateArguments() throws IOException {
|
||||||
|
if ((stdoutFlag && file != null) || (!stdoutFlag && file == null)) {
|
||||||
|
throw new IOException("Please choose one of stdout or output file");
|
||||||
|
}
|
||||||
|
if ((dataForkFile != null && stdinForkType == ForkType.data)
|
||||||
|
|| (resourceForkFile != null && stdinForkType == ForkType.resource)) {
|
||||||
|
throw new IOException("Stdin only supports one type of fork for input");
|
||||||
|
}
|
||||||
|
if (dataForkFile == null && resourceForkFile == null && stdinForkType == null) {
|
||||||
|
throw new IOException("Please select at least one fork type");
|
||||||
|
}
|
||||||
|
if (stdinForkType == ForkType.both) {
|
||||||
|
throw new IOException("Unable to read two forks from stdin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] prepDataFork() throws IOException {
|
||||||
|
byte[] dataFork = null;
|
||||||
|
if (stdinForkType == ForkType.data) {
|
||||||
|
dataFork = AppleSingle.toByteArray(System.in);
|
||||||
|
} else if (dataForkFile != null) {
|
||||||
|
dataFork = Files.readAllBytes(dataForkFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fixTextFlag && dataFork != null) {
|
||||||
|
for (int i=0; i<dataFork.length; i++) {
|
||||||
|
if (dataFork[i] == '\n') dataFork[i] = 0x0d;
|
||||||
|
dataFork[i] = (byte)(dataFork[i] | 0x80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dataFork;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] prepResourceFork() throws IOException {
|
||||||
|
byte[] resourceFork = null;
|
||||||
|
if (stdinForkType == ForkType.resource) {
|
||||||
|
resourceFork = AppleSingle.toByteArray(System.in);
|
||||||
|
} else if (resourceForkFile != null) {
|
||||||
|
resourceFork = Files.readAllBytes(resourceForkFile);
|
||||||
|
}
|
||||||
|
return resourceFork;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppleSingle buildAppleSingle(byte[] dataFork, byte[] resourceFork) {
|
||||||
|
AppleSingle.Builder builder = AppleSingle.builder();
|
||||||
|
if (realName != null) {
|
||||||
|
builder.realName(realName);
|
||||||
|
} else if (dataForkFile != null) {
|
||||||
|
String name = dataForkFile.getFileName().toString();
|
||||||
|
builder.realName(name);
|
||||||
|
}
|
||||||
|
if (access != null) builder.access(access.intValue());
|
||||||
|
if (filetype != null) builder.fileType(filetype.intValue());
|
||||||
|
if (auxtype != null) builder.auxType(auxtype.intValue());
|
||||||
|
if (dataFork != null) builder.dataFork(dataFork);
|
||||||
|
if (resourceFork != null) builder.resourceFork(resourceFork);
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeAppleSingle(AppleSingle applesingle) throws IOException {
|
||||||
|
if (stdoutFlag) {
|
||||||
|
applesingle.save(System.out);
|
||||||
|
} else {
|
||||||
|
applesingle.save(file);
|
||||||
|
System.out.printf("Saved to '%s'.\n", file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
package io.github.applecommander.applesingle.tools.asu;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
import io.github.applecommander.applesingle.AppleSingle;
|
||||||
|
import picocli.CommandLine.Command;
|
||||||
|
import picocli.CommandLine.Help.Visibility;
|
||||||
|
import picocli.CommandLine.Option;
|
||||||
|
import picocli.CommandLine.Parameters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supports extracting components of an AppleSingle archive.
|
||||||
|
*/
|
||||||
|
@Command(name = "extract", description = { "Extract contents of an AppleSingle file" },
|
||||||
|
parameterListHeading = "%nParameters:%n",
|
||||||
|
descriptionHeading = "%n",
|
||||||
|
optionListHeading = "%nOptions:%n")
|
||||||
|
public class ExtractCommand implements Callable<Void> {
|
||||||
|
@Option(names = { "-h", "--help" }, description = "Show help for subcommand", usageHelp = true)
|
||||||
|
private boolean helpFlag;
|
||||||
|
|
||||||
|
@Option(names = "--stdout", description = "Write selected fork to stdout")
|
||||||
|
private boolean stdoutFlag;
|
||||||
|
|
||||||
|
@Option(names = "--stdin", description = "Read AppleSingle from stdin")
|
||||||
|
private boolean stdinFlag;
|
||||||
|
|
||||||
|
@Option(names = "--fix-text", description = "Clear the high bit and fix line endings")
|
||||||
|
private boolean fixTextFlag;
|
||||||
|
|
||||||
|
@Option(names = "--fork", description = "Extract which fork type (specify data, resource, or both",
|
||||||
|
showDefaultValue = Visibility.ALWAYS)
|
||||||
|
private ForkType forkType = ForkType.data;
|
||||||
|
|
||||||
|
@Option(names = { "-o", "--output" }, description = "Write fork(s) to base filename")
|
||||||
|
private String baseFilename;
|
||||||
|
|
||||||
|
@Parameters(arity = "0..1", description = "File to process")
|
||||||
|
private Path file;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Void call() throws IOException {
|
||||||
|
validateArguments();
|
||||||
|
|
||||||
|
AppleSingle applesingle = stdinFlag ? AppleSingle.read(System.in) : AppleSingle.read(file);
|
||||||
|
if (!stdoutFlag && baseFilename == null && applesingle.getRealName() == null) {
|
||||||
|
throw new IOException("Please include an output base filename; this AppleSingle file does not contain a name");
|
||||||
|
}
|
||||||
|
if (baseFilename == null) {
|
||||||
|
baseFilename = applesingle.getRealName();
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFork(ForkType.data, applesingle.getDataFork());
|
||||||
|
writeFork(ForkType.resource, applesingle.getResourceFork());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateArguments() throws IOException {
|
||||||
|
if (stdoutFlag && baseFilename != null) {
|
||||||
|
throw new IOException("Please choose one of stdout or output file");
|
||||||
|
}
|
||||||
|
if (stdoutFlag && forkType == ForkType.both) {
|
||||||
|
throw new IOException("Stdout only supports one type of fork for output");
|
||||||
|
}
|
||||||
|
if ((stdinFlag && file != null) || (!stdinFlag && file == null)) {
|
||||||
|
throw new IOException("Please select ONE of stdin or file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeFork(ForkType forkType, byte[] data) throws IOException {
|
||||||
|
if (this.forkType != forkType && this.forkType != ForkType.both) return;
|
||||||
|
|
||||||
|
if (data == null || data.length == 0) {
|
||||||
|
throw new IOException(String.format("There is no data in the %s fork, aborting", forkType));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fixTextFlag) {
|
||||||
|
for (int i=0; i<data.length; i++) {
|
||||||
|
data[i] = (byte)(data[i] & 0x7f);
|
||||||
|
if (data[i] == 0x0d) data[i] = '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseFilename != null) {
|
||||||
|
String targetFilename = String.format("%s.%s", baseFilename, forkType.name());
|
||||||
|
System.out.printf("Writing %s fork to file '%s'...\n", forkType.name(), targetFilename);
|
||||||
|
Path path = Paths.get(targetFilename);
|
||||||
|
Files.write(path, data);
|
||||||
|
}
|
||||||
|
if (stdoutFlag) {
|
||||||
|
System.out.write(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package io.github.applecommander.applesingle.tools.asu;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic listing of fork types used in the command line tools.
|
||||||
|
*/
|
||||||
|
public enum ForkType {
|
||||||
|
data, resource, both
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package io.github.applecommander.applesingle.tools.asu;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
import io.github.applecommander.applesingle.AppleSingle;
|
||||||
|
import io.github.applecommander.applesingle.ProdosFileInfo;
|
||||||
|
import picocli.CommandLine.Command;
|
||||||
|
import picocli.CommandLine.Option;
|
||||||
|
import picocli.CommandLine.Parameters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display basic information from an AppleSingle archive.
|
||||||
|
*/
|
||||||
|
@Command(name = "info", description = { "Display information about an AppleSingle file",
|
||||||
|
"Please include a file name or indicate stdin should be read, but not both." },
|
||||||
|
parameterListHeading = "%nParameters:%n",
|
||||||
|
descriptionHeading = "%n",
|
||||||
|
optionListHeading = "%nOptions:%n")
|
||||||
|
public class InfoCommand implements Callable<Void> {
|
||||||
|
@Option(names = { "-h", "--help" }, description = "Show help for subcommand", usageHelp = true)
|
||||||
|
private boolean helpFlag;
|
||||||
|
|
||||||
|
@Option(names = "--stdin", description = "Read AppleSingle from stdin.")
|
||||||
|
private boolean stdinFlag;
|
||||||
|
|
||||||
|
@Parameters(arity = "0..1", description = "File to process")
|
||||||
|
private File file;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Void call() throws IOException {
|
||||||
|
AppleSingle applesingle = stdinFlag ? AppleSingle.read(System.in) : AppleSingle.read(file);
|
||||||
|
System.out.printf("Real Name: %s\n", Optional.ofNullable(applesingle.getRealName()).orElse("-Unknown-"));
|
||||||
|
System.out.printf("ProDOS info:\n");
|
||||||
|
if (applesingle.getProdosFileInfo() == null) {
|
||||||
|
System.out.println(" Not supplied.");
|
||||||
|
} else {
|
||||||
|
ProdosFileInfo prodosFileInfo = applesingle.getProdosFileInfo();
|
||||||
|
System.out.printf(" Access: 0x%02X\n", prodosFileInfo.getAccess());
|
||||||
|
System.out.printf(" File Type: 0x%02X\n", prodosFileInfo.getFileType());
|
||||||
|
System.out.printf(" Auxtype: 0x%04X\n", prodosFileInfo.getAuxType());
|
||||||
|
}
|
||||||
|
System.out.printf("Data Fork: Present, %,d bytes\n", applesingle.getDataFork().length);
|
||||||
|
System.out.printf("Resource Fork: %s\n",
|
||||||
|
Optional.ofNullable(applesingle.getResourceFork())
|
||||||
|
.map(d -> String.format("Present, %,d bytes", d.length))
|
||||||
|
.orElse("Not present"));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package io.github.applecommander.applesingle.tools.asu;
|
||||||
|
|
||||||
|
import picocli.CommandLine.ITypeConverter;
|
||||||
|
|
||||||
|
/** Add support for "$801" and "0x801" instead of just decimal like 2049. */
|
||||||
|
public class IntegerTypeConverter implements ITypeConverter<Integer> {
|
||||||
|
@Override
|
||||||
|
public Integer convert(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
} else if (value.startsWith("$")) {
|
||||||
|
return Integer.valueOf(value.substring(1), 16);
|
||||||
|
} else if (value.startsWith("0x") || value.startsWith("0X")) {
|
||||||
|
return Integer.valueOf(value.substring(2), 16);
|
||||||
|
} else {
|
||||||
|
return Integer.valueOf(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package io.github.applecommander.applesingle.tools.asu;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import picocli.CommandLine.Command;
|
||||||
|
import picocli.CommandLine.HelpCommand;
|
||||||
|
import picocli.CommandLine.Option;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary entry point into the AppleSingle utility.
|
||||||
|
*/
|
||||||
|
@Command(name = "asu", mixinStandardHelpOptions = true, versionProvider = VersionProvider.class,
|
||||||
|
descriptionHeading = "%n",
|
||||||
|
commandListHeading = "%nCommands:%n",
|
||||||
|
optionListHeading = "%nOptions:%n",
|
||||||
|
description = "AppleSingle utility",
|
||||||
|
subcommands = { HelpCommand.class, InfoCommand.class, CreateCommand.class, ExtractCommand.class })
|
||||||
|
public class Main implements Runnable {
|
||||||
|
@Option(names = "--debug", description = "Dump full stack trackes if an error occurs")
|
||||||
|
private static boolean debugFlag;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
try {
|
||||||
|
CommandLine.run(new Main(), args);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
if (Main.debugFlag) {
|
||||||
|
t.printStackTrace(System.err);
|
||||||
|
} else {
|
||||||
|
String message = t.getMessage();
|
||||||
|
while (t != null) {
|
||||||
|
message = t.getMessage();
|
||||||
|
t = t.getCause();
|
||||||
|
}
|
||||||
|
System.err.printf("Error: %s\n", Optional.ofNullable(message).orElse("An error occurred."));
|
||||||
|
}
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
CommandLine.usage(this, System.out);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package io.github.applecommander.applesingle.tools.asu;
|
||||||
|
|
||||||
|
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[] { Main.class.getPackage().getImplementationVersion() };
|
||||||
|
}
|
||||||
|
}
|
@ -39,7 +39,7 @@ public class AppleSingleTest {
|
|||||||
.realName(realName)
|
.realName(realName)
|
||||||
.build();
|
.build();
|
||||||
assertNotNull(createdAS);
|
assertNotNull(createdAS);
|
||||||
assertEquals(realName, createdAS.getRealName());
|
assertEquals(realName.toUpperCase(), createdAS.getRealName());
|
||||||
assertArrayEquals(dataFork, createdAS.getDataFork());
|
assertArrayEquals(dataFork, createdAS.getDataFork());
|
||||||
assertNull(createdAS.getResourceFork());
|
assertNull(createdAS.getResourceFork());
|
||||||
assertNotNull(createdAS.getProdosFileInfo());
|
assertNotNull(createdAS.getProdosFileInfo());
|
||||||
@ -50,9 +50,26 @@ public class AppleSingleTest {
|
|||||||
|
|
||||||
AppleSingle readAS = AppleSingle.read(actualBytes.toByteArray());
|
AppleSingle readAS = AppleSingle.read(actualBytes.toByteArray());
|
||||||
assertNotNull(readAS);
|
assertNotNull(readAS);
|
||||||
assertEquals(realName, readAS.getRealName());
|
assertEquals(realName.toUpperCase(), readAS.getRealName());
|
||||||
assertArrayEquals(dataFork, readAS.getDataFork());
|
assertArrayEquals(dataFork, readAS.getDataFork());
|
||||||
assertNull(readAS.getResourceFork());
|
assertNull(readAS.getResourceFork());
|
||||||
assertNotNull(readAS.getProdosFileInfo());
|
assertNotNull(readAS.getProdosFileInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProdosFileNameLengthRequirements() {
|
||||||
|
AppleSingle as = AppleSingle.builder().realName("superlongnamethatneedstobetruncated").build();
|
||||||
|
assertEquals(15, as.getRealName().length());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProdosFileNameCharacterRequirements() {
|
||||||
|
AppleSingle as = AppleSingle.builder().realName("bad-~@").build();
|
||||||
|
assertEquals("BAD...", as.getRealName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testProdosFileNameFirstCharacter() {
|
||||||
|
AppleSingle.builder().realName("1st-file").build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user