Compare commits

...

32 Commits

Author SHA1 Message Date
Rob Greene
cebb3727b0 Adding an AppleSingle edit command. 2024-02-07 18:54:43 -06:00
Rob Greene
9d676a5f44 Updating Maven release notes with a copy from AppleCommander. 2023-10-28 12:00:24 -05:00
Rob Greene
45b57ce675 Bumping version: 1.2.2 2023-10-28 11:54:22 -05:00
Rob Greene
ab165c8953 Merge remote-tracking branch 'origin/master' 2023-10-28 11:49:14 -05:00
Rob Greene
eb6f080823 Upgrading PicoCLI version and adapting a bit. 2023-10-28 11:48:51 -05:00
A2 Geek
26b63b0277
Merge pull request #4 from AppleCommander/dependabot/gradle/junit-junit-4.13.2
Bump junit:junit from 4.13.1 to 4.13.2
2023-10-28 11:40:22 -05:00
A2 Geek
54416b5140
Adding automatic builds 2023-10-28 11:37:26 -05:00
dependabot[bot]
3f881a3467
Bump junit:junit from 4.13.1 to 4.13.2
Bumps [junit:junit](https://github.com/junit-team/junit4) from 4.13.1 to 4.13.2.
- [Release notes](https://github.com/junit-team/junit4/releases)
- [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.13.1.md)
- [Commits](https://github.com/junit-team/junit4/compare/r4.13.1...r4.13.2)

---
updated-dependencies:
- dependency-name: junit:junit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-28 16:34:38 +00:00
A2 Geek
6b3090c9c3
Create dependabot.yml 2023-10-28 11:34:17 -05:00
Rob Greene
516521d39a Bumping to Gradle 7.3.1 (same as AppleCommander). #1 2023-10-28 11:30:54 -05:00
Rob Greene
ea58bc88f8 AppleSingle entry can have a length of 0. Adjusting how 0 byte entries
are reported.
2022-06-05 16:47:31 -05:00
Rob Greene
0651919ca0 AppleSingle entry can have a length of 0. Adjusting how 0 byte entries
are reported.
2022-06-05 16:47:21 -05:00
Rob Greene
0b1459931b AppleSingle entry can have a length of 0. Adjusting the 'analyze'
related edit.
2022-06-05 16:46:42 -05:00
Rob Greene
b21a7bb341 Enforcing Java 11. 2022-06-05 16:46:04 -05:00
Rob Greene
b39d3a7b56 Setting snapshot version. 2022-06-05 16:45:50 -05:00
Rob Greene
d689b5e0d5 First round of updates. Leaving publication for later. #1 2020-11-24 19:37:27 -06:00
Rob Greene
2bf7de8a0d Typo. 2018-06-19 20:10:11 -05:00
Rob Greene
369f1e5eba Cleaning up imports. 2018-06-07 21:21:27 -05:00
Rob Greene
05ab07e323 Adding some file testing capabilities; tweaked comments. 2018-06-06 21:56:42 -05:00
Rob Greene
153e0aa220 Adding AppleSingle.VERSION. 2018-06-04 22:20:13 -05:00
Rob Greene
549ce45c3c Displaying details on FileDatesInfo if present. 2018-06-04 22:19:53 -05:00
Rob Greene
d9df400d7f Bumping version. 2018-06-04 22:19:24 -05:00
Rob Greene
c1349bade8 Changing notation to (hopefully) be more clear on ranges. 2018-06-04 22:19:03 -05:00
Rob Greene
0f6fceaa55 Fixing spelling. 2018-06-04 22:18:27 -05:00
Rob Greene
aae02d7323 Updating documentation and tests a bit. 2018-06-03 17:27:42 -05:00
Rob Greene
4627e08f9f Adding Filter and altering AppleSingle write mechanism to be more
reusable.
2018-06-03 15:42:07 -05:00
Rob Greene
4f34a8a289 Introducing Entry/EntryType/AppleSingleReader to make reading aspects
more common across tools.
2018-06-03 12:33:09 -05:00
Rob Greene
b72c9f9b74 Adding Utilities and shifting to use it. 2018-06-03 12:31:07 -05:00
Rob Greene
cd5d7ac142 Hex 0xff does not appear to be visible; adjusting for it. 2018-06-03 12:22:06 -05:00
Rob Greene
e4c9c60bfa Bumping version. 2018-06-03 12:21:24 -05:00
Rob Greene
2085559af7 Gradle tweaks: fix for Oracle javadoc doclint strictness; made the signing task a bit more intelligent. 2018-05-29 10:46:19 -05:00
Rob Greene
f8741f9480 Noticed that the tools subproject (which is empty) shows up in Eclipse.
This makes the name more sensible.
2018-05-28 22:38:57 -05:00
31 changed files with 1369 additions and 490 deletions

11
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gradle" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

40
.github/workflows/gradle.yml vendored Normal file
View File

@ -0,0 +1,40 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
name: Build AppleSingle 'asu' CLI and API
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
with:
arguments: build
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: build-artifacts
path: tools/asu/build/libs/applesingle-tools-asu-*.jar
if-no-files-found: error

View File

@ -24,26 +24,35 @@ Extract secret key for the Gradle signing plugin:
$ gpg2 --export-secret-keys > secring.gpg
```
## Build and publish to Central Repository
## Gradle build and publish to Central Repository
> Note that all of this can be run from the main 'applesingle' folder.
> NOTE: The build has been updated to allow snapshots to be published. These appear to be automatically available.
Ensure full build passes:
```bash
$ ./gradlew clean test javadoc assemble
<...lots of stuff, primarily Javadoc issues...>
BUILD SUCCESSFUL in 3s
13 actionable tasks: 13 executed
./gradlew clean build
BUILD SUCCESSFUL in 8s
91 actionable tasks: 91 executed
```
Upload:
Publish:
```bash
$ ./gradlew uploadArchives
$ ./gradlew publish
BUILD SUCCESSFUL in 10s
10 actionable tasks: 1 executed, 9 up-to-date
BUILD SUCCESSFUL in 8s
2 actionable tasks: 2 executed
```
The can also be combined:
```bash
$ ./gradlew clean build publish
BUILD SUCCESSFUL in 16s
93 actionable tasks: 93 executed
```
Then follow "releasing the deployment" below.
@ -54,6 +63,7 @@ Just a reminder!
# References
* http://central.sonatype.org/pages/gradle.html
* http://central.sonatype.org/pages/gradle.html (NOTE: Documentation is out of date)
* http://central.sonatype.org/pages/releasing-the-deployment.html
* For all the other little pieces, Google is your friend. ;-)
* https://docs.gradle.org/current/userguide/publishing_maven.html
* For all the other little pieces, Google is your friend. ;-)

View File

@ -10,7 +10,7 @@ To include in a Maven project:
<dependency>
<groupId>net.sf.applecommander</groupId>
<artifactId>applesingle-api</artifactId>
<version>1.0.0</version>
<version>1.2.0</version>
</dependency>
```
@ -19,7 +19,7 @@ To include in a Gradle project:
```groovy
dependencies {
// ...
compile "net.sf.applecommander:applesingle-api:1.0.0"
compile "net.sf.applecommander:applesingle-api:1.2.0"
// ...
}
```
@ -58,3 +58,31 @@ as.save(file);
```
The `save(...)` method can save to a `File`, `Path`, or an `OutputStream`.
## Entries
If the higher-level API is insufficient, the lower-level API does allow either tracking of the processing
(see code for the `analyze` subcommand) or alternate processing of `Entry` objects (see the `filter`
subcommand).
To tap into the `AppleSingleReader` events, add as many reporters as required. For example, the `analyze`
command uses these to display the details of the AppleSingle file as it is read:
```java
AppleSingleReader reader = AppleSingleReader.builder(fileData)
.readAtReporter((start,chunk,desc) -> used.add(IntRange.of(start, start + chunk.length)))
.readAtReporter((start,chunk,desc) -> dumper.dump(start, chunk, desc))
.versionReporter(this::reportVersion)
.numberOfEntriesReporter(this::reportNumberOfEntries)
.entryReporter(this::reportEntry)
.build();
```
To work with the raw `Entry` objects, use the various `AppleSingle#asEntries` methods. For instance, the
`filter` subcommand bypasses the `AppleSingle` object altogether to implement the filter:
```java
List<Entry> entries = stdinFlag ? AppleSingle.asEntries(System.in) : AppleSingle.asEntries(inputFile);
// ...
AppleSingle.write(outputStream, newEntries);
```

View File

@ -1,22 +1,43 @@
repositories {
jcenter()
plugins {
id 'java-library'
id 'maven-publish'
id 'signing'
}
apply plugin: 'java-library'
apply plugin: 'maven'
apply plugin: 'signing'
ext.isSnapshotVersion = version.endsWith("SNAPSHOT")
ext.isReleaseVersion = !ext.isSnapshotVersion
sourceCompatibility = '11'
targetCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13.2'
}
tasks.withType(Jar) {
manifest {
attributes 'Implementation-Title': 'AppleSingle',
'Implementation-Version': "${project.version} (${new Date().format('yyyy-MM-dd HH:mm')})"
}
}
javadoc {
title = "applesingle-api ${project.version}"
source = sourceSets.main.allJava
options.addStringOption('Xdoclint:none', '-quiet')
}
task javadocJar(type: Jar) {
classifier = 'javadoc'
archiveClassifier = 'javadoc'
from javadoc
}
task sourcesJar(type: Jar) {
classifier = 'sources'
archiveClassifier = 'sources'
from sourceSets.main.allSource
}
@ -24,47 +45,52 @@ artifacts {
archives javadocJar, sourcesJar
}
signing {
sign configurations.archives
}
uploadArchives {
repositories {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
authentication(userName: findProperty('ossrhUsername'), password: findProperty('ossrhPassword'))
}
snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") {
authentication(userName: findProperty('ossrhUsername'), password: findProperty('ossrhPassword'))
}
pom.project {
name archivesBaseName
packaging 'jar'
description 'A Java library for managing AppleSingle files.'
url 'https://applecommander.github.io/'
scm {
url 'https://github.com/AppleCommander/applesingle'
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
artifact sourcesJar
artifact javadocJar
pom {
groupId = "net.sf.applecommander"
artifactId = "applesingle-api"
name = 'applesingle-api'
description = 'A Java library for managing AppleSingle files.'
url = 'https://applecommander.github.io/'
licenses {
license {
name = 'The GNU General Public License (GPL) Version 2, June 1991'
url = 'https://www.gnu.org/licenses/gpl-2.0.html'
}
}
developers {
developer {
id = 'robgreene'
name = 'Rob Greene'
email = 'robgreene@gmail.com'
}
}
scm {
connection = 'scm:git:https://github.com/AppleCommander/applesingle.git'
developerConnection = 'scm:git:git@github.com:AppleCommander/applesingle.git'
url = 'https://github.com/AppleCommander/applesingle'
}
}
repositories {
maven {
def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2"
def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/"
url = isSnapshotVersion ? snapshotsRepoUrl : releasesRepoUrl
credentials {
username = findProperty('ossrhUsername')
password = findProperty('ossrhPassword')
}
}
}
}
licenses {
license {
name 'The GNU General Public License (GPL) Version 3, 29 June 2007'
url 'https://www.gnu.org/licenses/gpl-3.0.html'
}
}
developers {
developer {
id 'robgreene'
email 'robgreene@gmail.com'
}
}
}
}
}
}
signing {
sign publishing.publications.mavenJava
}

View File

@ -1,6 +1,5 @@
package io.github.applecommander.applesingle;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@ -11,7 +10,9 @@ import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@ -19,13 +20,14 @@ import java.util.function.Consumer;
/**
* Support reading of data from and AppleSingle source.
* Does not implement all components at this time, extend as required.
* Does not implement all components at this time, extend as required and/or understood.
* All construction has been deferred to the <code>read(...)</code> or {@link #builder()} methods.
* <p>
* Currently supports entries:<br/>
* 1. Data Fork<br/>
* 2. Resource Fork<br/>
* 3. Real Name<br/>
* 8. File Dates Info<br/>
* 11. ProDOS File Info<br/>
*
* @see <a href="https://github.com/AppleCommander/AppleCommander/issues/20">AppleCommander issue #20</a>
@ -34,35 +36,21 @@ public class AppleSingle {
public static final int MAGIC_NUMBER = 0x0051600;
public static final int VERSION_NUMBER1 = 0x00010000;
public static final int VERSION_NUMBER2 = 0x00020000;
public static final Map<Integer,String> ENTRY_TYPE_NAMES = new HashMap<Integer,String>() {
private static final long serialVersionUID = 7142066556402030814L;
{
put(1, "Data Fork");
put(2, "Resource Fork");
put(3, "Real Name");
put(4, "Comment");
put(5, "Icon, B&W");
put(6, "Icon, Color");
put(7, "File Info");
put(8, "File Dates Info");
put(9, "Finder Info");
put(10, "Macintosh File Info");
put(11, "ProDOS File Info");
put(12, "MS-DOS File Info");
put(13, "Short Name");
put(14, "AFP File Info");
put(15, "Directory ID");
}};
private Map<Integer,Consumer<byte[]>> entryConsumers = new HashMap<>();
public static final String VERSION;
static {
VERSION = AppleSingle.class.getPackage().getImplementationVersion();
}
private Map<Integer,Consumer<Entry>> entryConsumers = new HashMap<>();
{
entryConsumers.put(1, this::setDataFork);
entryConsumers.put(2, this::setResourceFork);
entryConsumers.put(3, this::setRealName);
entryConsumers.put(8, this::setFileDatesInfo);
entryConsumers.put(11, this::setProdosFileInfo);
entryConsumers.put(1, entry -> this.dataFork = entry.getData());
entryConsumers.put(2, entry -> this.resourceFork = entry.getData());
entryConsumers.put(3, entry -> this.realName = Utilities.entryToAsciiString(entry));
entryConsumers.put(8, entry -> this.fileDatesInfo = FileDatesInfo.fromEntry(entry));
entryConsumers.put(11, entry -> this.prodosFileInfo = ProdosFileInfo.fromEntry(entry));
}
private byte[] dataFork;
private byte[] resourceFork;
private String realName;
@ -72,66 +60,13 @@ public class AppleSingle {
private AppleSingle() {
// Allow Builder construction
}
private AppleSingle(byte[] data) throws IOException {
ByteBuffer buffer = ByteBuffer.wrap(data)
.order(ByteOrder.BIG_ENDIAN)
.asReadOnlyBuffer();
required(buffer, MAGIC_NUMBER, "Not an AppleSingle file - magic number does not match.");
required(buffer, VERSION_NUMBER2, "Only AppleSingle version 2 supported.");
buffer.position(buffer.position() + 16); // Skip filler
int entries = buffer.getShort();
for (int i = 0; i < entries; i++) {
int entryId = buffer.getInt();
int offset = buffer.getInt();
int length = buffer.getInt();
buffer.mark();
buffer.position(offset);
byte[] entryData = new byte[length];
buffer.get(entryData);
// Defer to the proper set method or crash if we don't support that type of entry
Optional.ofNullable(entryConsumers.get(entryId))
.orElseThrow(() -> new IOException(String.format("Unsupported entry type of %04X (%s)", entryId,
ENTRY_TYPE_NAMES.getOrDefault(entryId, "Unknown"))))
.accept(entryData);
buffer.reset();
}
}
private void required(ByteBuffer buffer, int expected, String message) throws IOException {
int actual = buffer.getInt();
if (actual != expected) {
throw new IOException(String.format("%s Expected 0x%08x but read 0x%08x.", message, expected, actual));
}
}
private void setDataFork(byte[] entryData) {
this.dataFork = entryData;
}
private void setResourceFork(byte[] entryData) {
this.resourceFork = entryData;
}
private void setRealName(byte[] entryData) {
for (int i=0; i<entryData.length; i++) {
entryData[i] = (byte)(entryData[i] & 0x7f);
}
this.realName = new String(entryData);
}
private void setProdosFileInfo(byte[] entryData) {
ByteBuffer infoData = ByteBuffer.wrap(entryData)
.order(ByteOrder.BIG_ENDIAN)
.asReadOnlyBuffer();
int access = infoData.getShort();
int fileType = infoData.getShort();
int auxType = infoData.getInt();
this.prodosFileInfo = new ProdosFileInfo(access, fileType, auxType);
}
private void setFileDatesInfo(byte[] entryData) {
ByteBuffer infoData = ByteBuffer.wrap(entryData)
.order(ByteOrder.BIG_ENDIAN)
.asReadOnlyBuffer();
int creation = infoData.getInt();
int modification = infoData.getInt();
int backup = infoData.getInt();
int access = infoData.getInt();
this.fileDatesInfo = new FileDatesInfo(creation, modification, backup, access);
private AppleSingle(List<Entry> entries) throws IOException {
entries.forEach(entry -> {
Optional.ofNullable(entry)
.map(Entry::getEntryId)
.map(entryConsumers::get)
.ifPresent(c -> c.accept(entry));
});
}
public byte[] getDataFork() {
@ -150,85 +85,66 @@ public class AppleSingle {
return fileDatesInfo;
}
/** Write this AppleSingle to the given output stream. Note that it only supports the "understood" components. */
public void save(OutputStream outputStream) throws IOException {
final boolean hasResourceFork = Objects.nonNull(resourceFork);
final boolean hasRealName = Objects.nonNull(realName);
final int entries = 3 + (hasRealName ? 1 : 0) + (hasResourceFork ? 1 : 0);
int realNameOffset = 26 + (12 * entries);
int prodosFileInfoOffset = realNameOffset + (hasRealName ? realName.length() : 0);
int fileDatesInfoOffset = prodosFileInfoOffset + 8;
int resourceForkOffset = fileDatesInfoOffset + 16;
int dataForkOffset = resourceForkOffset + (hasResourceFork ? resourceFork.length : 0);
writeFileHeader(outputStream, entries);
if (hasRealName) writeHeader(outputStream, 3, realNameOffset, realName.length());
writeHeader(outputStream, 11, prodosFileInfoOffset, 8);
writeHeader(outputStream, 8, fileDatesInfoOffset, 16);
if (hasResourceFork) writeHeader(outputStream, 2, resourceForkOffset, resourceFork.length);
writeHeader(outputStream, 1, dataForkOffset, dataFork.length);
if (hasRealName) writeRealName(outputStream);
writeProdosFileInfo(outputStream);
writeFileDatesInfo(outputStream);
if (hasResourceFork) writeResourceFork(outputStream);
writeDataFork(outputStream);
List<Entry> entries = new ArrayList<>();
Optional.ofNullable(this.realName)
.map(String::getBytes)
.map(Entry::realName)
.ifPresent(entries::add);
Optional.ofNullable(this.prodosFileInfo)
.map(ProdosFileInfo::toEntry)
.ifPresent(entries::add);
Optional.ofNullable(this.fileDatesInfo)
.map(FileDatesInfo::toEntry)
.ifPresent(entries::add);
Optional.ofNullable(this.resourceFork)
.map(Entry::resourceFork)
.ifPresent(entries::add);
Optional.ofNullable(this.dataFork)
.map(Entry::dataFork)
.ifPresent(entries::add);
write(outputStream, entries);
}
/** Save this AppleSingle to a File. */
public void save(File file) throws IOException {
try (FileOutputStream outputStream = new FileOutputStream(file)) {
save(outputStream);
}
}
/** Save this AppleSingle to a Path. */
public void save(Path path) throws IOException {
try (OutputStream outputStream = Files.newOutputStream(path)) {
save(outputStream);
}
}
private void writeFileHeader(OutputStream outputStream, int numberOfEntries) throws IOException {
/**
* Common write capability for an AppleSingle based on entries. Also can be used by external
* entities to write a properly formatted AppleSingle file without the ProDOS assumptions of AppleSingle.
*/
public static void write(OutputStream outputStream, List<Entry> entries) throws IOException {
final byte[] filler = new byte[16];
ByteBuffer buf = ByteBuffer.allocate(26).order(ByteOrder.BIG_ENDIAN);
buf.putInt(MAGIC_NUMBER);
buf.putInt(VERSION_NUMBER2);
buf.put(filler);
buf.putShort((short)numberOfEntries);
buf.putShort((short)entries.size());
outputStream.write(buf.array());
}
private void writeHeader(OutputStream outputStream, int entryId, int offset, int length) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(12).order(ByteOrder.BIG_ENDIAN);
buf.putInt(entryId);
buf.putInt(offset);
buf.putInt(length);
outputStream.write(buf.array());
}
private void writeRealName(OutputStream outputStream) throws IOException {
outputStream.write(realName.getBytes());
}
private void writeProdosFileInfo(OutputStream outputStream) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN);
buf.putShort((short)prodosFileInfo.access);
buf.putShort((short)prodosFileInfo.fileType);
buf.putInt(prodosFileInfo.auxType);
outputStream.write(buf.array());
}
private void writeFileDatesInfo(OutputStream outputStream) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(16).order(ByteOrder.BIG_ENDIAN);
buf.putInt(fileDatesInfo.getCreation());
buf.putInt(fileDatesInfo.getModification());
buf.putInt(fileDatesInfo.getBackup());
buf.putInt(fileDatesInfo.getAccess());
outputStream.write(buf.array());
}
private void writeResourceFork(OutputStream outputStream) throws IOException {
outputStream.write(resourceFork);
}
private void writeDataFork(OutputStream outputStream) throws IOException {
outputStream.write(dataFork);
}
int offset = 26 + (Entry.BYTES * entries.size());
for (Entry entry : entries) {
entry.writeHeader(outputStream, offset);
offset += entry.getLength();
}
for (Entry entry : entries) {
entry.writeData(outputStream);
}
}
public static AppleSingle read(InputStream inputStream) throws IOException {
Objects.requireNonNull(inputStream, "Please supply an input stream");
return read(AppleSingle.toByteArray(inputStream));
return read(Utilities.toByteArray(inputStream));
}
public static AppleSingle read(File file) throws IOException {
Objects.requireNonNull(file, "Please supply a file");
@ -236,18 +152,108 @@ public class AppleSingle {
}
public static AppleSingle read(Path path) throws IOException {
Objects.requireNonNull(path, "Please supply a file");
return new AppleSingle(Files.readAllBytes(path));
return read(Files.readAllBytes(path));
}
public static AppleSingle read(byte[] data) throws IOException {
Objects.requireNonNull(data);
return new AppleSingle(data);
return new AppleSingle(asEntries(data));
}
public static List<Entry> asEntries(InputStream inputStream) throws IOException {
Objects.requireNonNull(inputStream);
return asEntries(Utilities.toByteArray(inputStream));
}
public static List<Entry> asEntries(File file) throws IOException {
Objects.requireNonNull(file);
return asEntries(file.toPath());
}
public static List<Entry> asEntries(Path path) throws IOException {
Objects.requireNonNull(path);
return asEntries(Files.readAllBytes(path));
}
public static List<Entry> asEntries(byte[] data) throws IOException {
Objects.requireNonNull(data);
return asEntries(AppleSingleReader.builder(data).build());
}
public static List<Entry> asEntries(AppleSingleReader reader) throws IOException {
Objects.requireNonNull(reader);
List<Entry> entries = new ArrayList<>();
required(reader, "Magic number", "Not an AppleSingle file - magic number does not match.", MAGIC_NUMBER);
int version = required(reader, "Version", "Only AppleSingle version 1 and 2 supported.", VERSION_NUMBER1, VERSION_NUMBER2);
reader.reportVersion(version);
reader.read(16, "Filler");
int numberOfEntries = reader.read(Short.BYTES, "Number of entries").getShort();
reader.reportNumberOfEntries(numberOfEntries);
for (int i = 0; i < numberOfEntries; i++) {
Entry entry = Entry.create(reader);
entries.add(entry);
reader.reportEntry(entry);
}
return entries;
}
private static int required(AppleSingleReader reader, String description, String message, int... expecteds) throws IOException {
int actual = reader.read(Integer.BYTES, description).getInt();
for (int expected : expecteds) {
if (actual == expected) return actual;
}
List<String> versions = new ArrayList<>();
for (int expected : expecteds) versions.add(String.format("0x%08x", expected));
throw new IOException(String.format("%s Expected %s but read 0x%08x.",
message, String.join(",", versions), actual));
}
/** Perform a quick test against a File to see if it is an AppleSingle file. */
public static boolean test(File file) throws IOException {
Objects.requireNonNull(file);
return test(file.toPath());
}
/** Perform a quick test against a Path to see if it is an AppleSingle file. */
public static boolean test(Path path) throws IOException {
Objects.requireNonNull(path);
return test(Files.readAllBytes(path));
}
/** Perform a quick test against an InputStream to see if it is an AppleSingle file. */
public static boolean test(InputStream inputStream) throws IOException {
Objects.requireNonNull(inputStream);
return test(Utilities.toByteArray(inputStream));
}
/** Perform a quick test against a byte array to see if it is an AppleSingle file. */
public static boolean test(byte[] data) {
Objects.requireNonNull(data);
return test(AppleSingleReader.builder(data).build());
}
/** Perform a quick test against a reader to see if it is an AppleSingle file. */
public static boolean test(AppleSingleReader reader) {
Objects.requireNonNull(reader);
return check(reader, MAGIC_NUMBER) && check(reader, VERSION_NUMBER1, VERSION_NUMBER2);
}
private static boolean check(AppleSingleReader reader, int... expecteds) {
try {
final String message = ""; // Just needed for read.
int actual = reader.read(Integer.BYTES, message).getInt();
for (int expected : expecteds) {
if (actual == expected) return true;
}
} catch (ArrayIndexOutOfBoundsException ignored) {
// Bad file! Fall through.
}
return false;
}
public static Builder builder() {
return new Builder();
}
public static Builder builder(AppleSingle original) {
return new Builder(original);
}
public static class Builder {
private AppleSingle as = new AppleSingle();
final private AppleSingle as;
private Builder() {
this.as = new AppleSingle();
}
private Builder(AppleSingle original) {
this.as = original;
}
public Builder realName(String realName) {
if (!Character.isAlphabetic(realName.charAt(0))) {
throw new IllegalArgumentException("ProDOS file names must begin with a letter");
@ -324,17 +330,4 @@ public class AppleSingle {
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();
}
}

View File

@ -0,0 +1,118 @@
package io.github.applecommander.applesingle;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Objects;
import java.util.function.Consumer;
/**
* The AppleSingleReader is a component that allows tools to react to processing that
* goes on when an AppleSingle file is being read. The {@code Builder} allows multiple
* {@code Consumer}'s and {@code ReadAtReporter}'s to be defined.
*/
public final class AppleSingleReader {
private AppleSingleReader() { /* Prevent construction */ }
private byte[] data;
private int pos = 0;
private Consumer<Integer> versionReporter = v -> {};
private Consumer<Integer> numberOfEntriesReporter = n -> {};
private Consumer<Entry> entryReporter = e -> {};
private ReadAtReporter readAtReporter = (s,b,d) -> {};
public ByteBuffer read(int len, String description) {
try {
return readAt(pos, len, description);
} finally {
pos += len;
}
}
public ByteBuffer readAt(int start, int len, String description) {
byte[] chunk = new byte[len];
System.arraycopy(data, start, chunk, 0, len);
readAtReporter.accept(start, chunk, description);
ByteBuffer buffer = ByteBuffer.wrap(chunk)
.order(ByteOrder.BIG_ENDIAN);
return buffer;
}
public void reportVersion(int version) {
versionReporter.accept(version);
}
public void reportNumberOfEntries(int numberOfEntries) {
numberOfEntriesReporter.accept(numberOfEntries);
}
public void reportEntry(Entry entry) {
entryReporter.accept(entry);
}
/** Create a {@code Builder} for an {@code AppleSingleReader}. */
public static Builder builder(byte[] data) {
return new Builder(data);
}
public static class Builder {
private AppleSingleReader reader = new AppleSingleReader();
private Builder(byte[] data) {
Objects.requireNonNull(data, "You must supply a byte[] of data");
reader.data = data;
}
/** Add a version reporter. Note that multiple can be added. */
public Builder versionReporter(Consumer<Integer> consumer) {
Objects.requireNonNull(consumer);
reader.versionReporter = reader.versionReporter.andThen(consumer);
return this;
}
/** Add a number of entries reporter. Note that multiple can be added. */
public Builder numberOfEntriesReporter(Consumer<Integer> consumer) {
Objects.requireNonNull(consumer);
reader.numberOfEntriesReporter = reader.numberOfEntriesReporter.andThen(consumer);
return this;
}
/** Add an entry reporter. Note that multiple can be added. */
public Builder entryReporter(Consumer<Entry> consumer) {
Objects.requireNonNull(consumer);
reader.entryReporter = reader.entryReporter.andThen(consumer);
return this;
}
/** Add a read at reporter. Note that multiple can be added. */
public Builder readAtReporter(ReadAtReporter consumer) {
Objects.requireNonNull(consumer);
reader.readAtReporter = reader.readAtReporter.andThen(consumer);
return this;
}
public AppleSingleReader build() {
return reader;
}
}
/**
* A reporter for the {@code AppleSingleReader#readAt(int, int, String)} method,
* heavily modeled on the {@code Consumer} interface.
*/
public interface ReadAtReporter {
/**
* Performs this operation on the given arguments.
*
* @param start the offset into the file
* @param data the specific data being processed
* @param description descriptive text regarding the data
*/
public void accept(int start, byte[] data, String description);
/**
* Returns a composed {@code ReadAtReporter} that performs, in sequence, this
* operation followed by the {@code after} operation. If performing either
* operation throws an exception, it is relayed to the caller of the
* composed operation. If performing this operation throws an exception,
* the {@code after} operation will not be performed.
*
* @param after the operation to perform after this operation
* @return a composed {@code ReadAtReporter} that performs in sequence this
* operation followed by the {@code after} operation
* @throws NullPointerException if {@code after} is null
*/
public default ReadAtReporter andThen(ReadAtReporter after) {
Objects.requireNonNull(after);
return (s,b,d) -> { accept(s,b,d); after.accept(s,b,d); };
}
}
}

View File

@ -0,0 +1,83 @@
package io.github.applecommander.applesingle;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Objects;
/**
* Represents an AppleSingle entry.
*/
public class Entry {
public static final int BYTES = 12;
private int entryId;
private int offset;
private int length;
private byte[] data;
/** Create an Entry and read it's data from the reader. */
public static Entry create(AppleSingleReader reader) {
Objects.requireNonNull(reader);
ByteBuffer buffer = reader.read(BYTES, "Entry header");
Entry entry = new Entry();
entry.entryId = buffer.getInt();
entry.offset = buffer.getInt();
entry.length = buffer.getInt();
entry.data = reader.readAt(entry.offset, entry.length, EntryType.findNameOrUnknown(entry)).array();
return entry;
}
/** Create an Entry. */
public static Entry create(EntryType type, byte[] data) {
Objects.requireNonNull(type);
Objects.requireNonNull(data);
Entry entry = new Entry();
entry.entryId = type.entryId;
entry.offset = -1;
entry.length = data.length;
entry.data = data;
return entry;
}
/** Create a REAL_NAME entry. Primarily used for Java 8 streams. */
public static Entry realName(byte[] data) {
return create(EntryType.REAL_NAME, data);
}
/** Create a DATA_FORK entry. Primarily used for Java 8 streams. */
public static Entry dataFork(byte[] data) {
return create(EntryType.DATA_FORK, data);
}
/** Create a RESOURCE_FORK entry. Primarily used for Java 8 streams. */
public static Entry resourceFork(byte[] data) {
return create(EntryType.RESOURCE_FORK, data);
}
public int getEntryId() {
return entryId;
}
public int getOffset() {
return offset;
}
public int getLength() {
return length;
}
public byte[] getData() {
return data;
}
public ByteBuffer getBuffer() {
return ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN).asReadOnlyBuffer();
}
public void writeHeader(OutputStream outputStream, int offset) throws IOException {
this.offset = offset;
ByteBuffer buf = ByteBuffer.allocate(BYTES).order(ByteOrder.BIG_ENDIAN);
buf.putInt(this.entryId);
buf.putInt(this.offset);
buf.putInt(this.length);
outputStream.write(buf.array());
}
public void writeData(OutputStream outputStream) throws IOException {
outputStream.write(data);
}
}

View File

@ -0,0 +1,44 @@
package io.github.applecommander.applesingle;
public enum EntryType {
DATA_FORK(1, "Data Fork"),
RESOURCE_FORK(2, "Resource Fork"),
REAL_NAME(3, "Real Name"),
COMMENT(4, "Comment"),
ICON_BW(5, "Icon, B&W"),
ICON_COLOR(6, "Icon, Color"),
FILE_INFO(7, "File Info"),
FILE_DATES_INFO(8, "File Dates Info"),
FINDER_INFO(9, "Finder Info"),
MACINTOSH_FILE_INFO(10, "Macintosh File Info"),
PRODOS_FILE_INFO(11, "ProDOS File Info"),
MSDOS_FILE_INFO(12, "MS-DOS File Info"),
SHORT_NAME(13, "Short Name"),
AFP_FILE_INFO(14, "AFP File Info"),
DIRECTORY_ID(15, "Directory ID");
public static final String findNameOrUnknown(Entry entry) {
for (EntryType et : values()) {
if (et.entryId == entry.getEntryId()) {
return et.name;
}
}
return "Unknown";
}
public static final EntryType find(int entryId) {
for (EntryType et : values()) {
if (et.entryId == entryId) {
return et;
}
}
throw new IllegalArgumentException(String.format("Unable to find EntryType # %d", entryId));
}
public final int entryId;
public final String name;
private EntryType(int entryId, String name) {
this.entryId = entryId;
this.name= name;
}
}

View File

@ -1,15 +1,19 @@
package io.github.applecommander.applesingle;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.time.Instant;
import java.util.function.IntSupplier;
public class FileDatesInfo {
/** The number of seconds at the begining of the AppleSingle date epoch since the Unix epoch began. */
//public static final int EPOCH_2000 = 946684800;
/** The number of seconds at the beginning of the AppleSingle date epoch since the Unix epoch began. */
public static final Instant EPOCH_INSTANT = Instant.parse("2000-01-01T00:00:00.00Z");
/** Per the AppleSingle technical notes. */
public static final int UNKNOWN_DATE = 0x80000000;
/** Number of bytes a File Dates Info takes per AppleSingle spec. */
public static final int BYTES = 16;
// Package scoped so AppleSingle Builder is able to set
int creation;
int modification;
int backup;
@ -18,6 +22,14 @@ public class FileDatesInfo {
public static int fromInstant(Instant instant) {
return (int)(instant.getEpochSecond() - EPOCH_INSTANT.getEpochSecond());
}
public static FileDatesInfo fromEntry(Entry entry) {
ByteBuffer infoData = entry.getBuffer();
int creation = infoData.getInt();
int modification = infoData.getInt();
int backup = infoData.getInt();
int access = infoData.getInt();
return new FileDatesInfo(creation, modification, backup, access);
}
public FileDatesInfo() {
int current = FileDatesInfo.fromInstant(Instant.now());
@ -33,6 +45,15 @@ public class FileDatesInfo {
this.access = access;
}
public Entry toEntry() {
ByteBuffer buf = ByteBuffer.allocate(BYTES).order(ByteOrder.BIG_ENDIAN);
buf.putInt(creation);
buf.putInt(modification);
buf.putInt(backup);
buf.putInt(access);
return Entry.create(EntryType.FILE_DATES_INFO, buf.array());
}
public Instant getCreationInstant() {
return toInstant(this::getCreation);
}

View File

@ -1,5 +1,8 @@
package io.github.applecommander.applesingle;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import io.github.applecommander.applesingle.AppleSingle.Builder;
/**
@ -9,6 +12,10 @@ import io.github.applecommander.applesingle.AppleSingle.Builder;
* Note 2: Fields are package-private to allow {@link Builder} to have direct access.<br/>
*/
public class ProdosFileInfo {
/** Number of bytes a File Dates Info takes per AppleSingle spec. */
public static final int BYTES = 8;
// Package scoped so AppleSingle Builder is able to set
int access;
int fileType;
int auxType;
@ -16,13 +23,28 @@ public class ProdosFileInfo {
public static ProdosFileInfo standardBIN() {
return new ProdosFileInfo(0xc3, 0x06, 0x0000);
}
public static ProdosFileInfo fromEntry(Entry entry) {
ByteBuffer infoData = entry.getBuffer();
int access = infoData.getShort();
int fileType = infoData.getShort();
int auxType = infoData.getInt();
return new ProdosFileInfo(access, fileType, auxType);
}
public ProdosFileInfo(int access, int fileType, int auxType) {
this.access = access;
this.fileType = fileType;
this.auxType = auxType;
}
public Entry toEntry() {
ByteBuffer buf = ByteBuffer.allocate(BYTES).order(ByteOrder.BIG_ENDIAN);
buf.putShort((short)access);
buf.putShort((short)fileType);
buf.putInt(auxType);
return Entry.create(EntryType.PRODOS_FILE_INFO, buf.array());
}
public int getAccess() {
return access;
}

View File

@ -0,0 +1,31 @@
package io.github.applecommander.applesingle;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class Utilities {
private Utilities() { /* Prevent construction */ }
/** Utility method to read all bytes from an InputStream. */
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();
}
/** Convert bytes in an Entry to a 7-bit ASCII string. Emphasis on 7-bit in case Apple II high bit is along for the ride. */
public static String entryToAsciiString(Entry entry) {
byte[] data = entry.getData();
for (int i=0; i<data.length; i++) {
data[i] = (byte)(data[i] & 0x7f);
}
return new String(data);
}
}

View File

@ -0,0 +1,72 @@
package io.github.applecommander.applesingle;
import org.junit.Test;
import static org.junit.Assert.*;
import java.io.IOException;
public class AppleSingleReaderTest {
@Test(expected = NullPointerException.class)
public void testDoesNotAcceptNull() {
AppleSingleReader.builder(null);
}
@Test
public void testReporters() throws IOException {
Ticker versionCalled = new Ticker();
Ticker numberOfEntriesCalled = new Ticker();
Ticker entryReporterCalled = new Ticker();
Ticker readAtCalled = new Ticker();
// Intentionally calling ticker 2x to ensure events do get chained
AppleSingleReader r = AppleSingleReader.builder(SAMPLE_FILE)
.versionReporter(v -> versionCalled.tick())
.versionReporter(v -> assertEquals(AppleSingle.VERSION_NUMBER2, v.intValue()))
.versionReporter(v -> versionCalled.tick())
.numberOfEntriesReporter(n -> numberOfEntriesCalled.tick())
.numberOfEntriesReporter(n -> assertEquals(1, n.intValue()))
.numberOfEntriesReporter(n -> numberOfEntriesCalled.tick())
.readAtReporter((o,b,d) -> readAtCalled.tick())
.readAtReporter((o,b,d) -> readAtCalled.tick())
.entryReporter(e -> entryReporterCalled.tick())
.entryReporter(e -> assertEquals("Hello, World!\n", new String(e.getData())))
.entryReporter(e -> assertEquals(e.getEntryId(), EntryType.DATA_FORK.entryId))
.entryReporter(e -> entryReporterCalled.tick())
.build();
// Executes on the reader
AppleSingle.asEntries(r);
// Validate
assertEquals(2, versionCalled.count());
assertEquals(2, numberOfEntriesCalled.count());
assertEquals(2, entryReporterCalled.count());
assertTrue(readAtCalled.count() >= 2);
}
/**
* AppleSingle file with a simple Data Fork and nothing else.
* <br/>
* <code>
* $ echo "Hello, World!" | asu create --stdin-fork=data --filetype=txt --stdout | asu filter --include=1 --stdin --stdout | 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 01 00 00 00 01 00 00 |................|
* 00000020 00 26 00 00 00 0e 48 65 6c 6c 6f 2c 20 57 6f 72 |.&....Hello, Wor|
* 00000030 6c 64 21 0a |ld!.|
* </code>
*/
public static final byte[] SAMPLE_FILE = {
0x00, 0x05, 0x16, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x26, 0x00, 0x00, 0x00, 0x0e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x57, 0x6f, 0x72,
0x6c, 0x64, 0x21, 0x0a
};
public static class Ticker {
private int count;
public void tick() {
this.count++;
}
public int count() {
return this.count;
}
}
}

View File

@ -2,8 +2,10 @@ package io.github.applecommander.applesingle;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -80,6 +82,17 @@ public class AppleSingleTest {
@Test(expected = IllegalArgumentException.class)
public void testProdosFileNameFirstCharacter() {
// Fails due to the first character being a digit.
AppleSingle.builder().realName("1st-file").build();
}
@Test
public void testTest() throws IOException {
// Known valid
assertTrue(AppleSingle.test(getClass().getResourceAsStream(AS_HELLO_BIN)));
// Known invalid
assertFalse(AppleSingle.test(new byte[200]));
// Could/should generate error due to truncated data, but this method should just give us a false.
assertFalse(AppleSingle.test(new byte[3]));
}
}

View File

@ -1,7 +1,7 @@
# 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.1.0
version=1.3.0
# Maven Central Repository G and A of GAV coordinate. :-)
group=net.sf.applecommander

Binary file not shown.

View File

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

286
gradlew vendored
View File

@ -1,78 +1,129 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -81,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
@ -89,84 +140,95 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

43
gradlew.bat vendored
View File

@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -35,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@ -45,28 +64,14 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell

View File

@ -3,4 +3,5 @@ include 'tools:asu'
rootProject.name = 'applesingle'
project(":api").name = 'applesingle-api'
project(":tools").name = 'applesingle-tools'
project(":tools:asu").name = 'applesingle-tools-asu'

View File

@ -4,13 +4,13 @@ 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'`
Note that all runs are with the `asu` alias defined as `alias asu='java -jar build/libs/applesingle-1.2.0.jar'`
(adjust as necessary).
## Basic usage
```shell
$ asu
$ asu --help
Usage: asu [-hV] [--debug] [COMMAND]
AppleSingle utility
@ -21,10 +21,12 @@ Options:
-V, --version Print version information and exit.
Commands:
help Displays help information about the specified command
info Display information about an AppleSingle file
analyze Perform an analysis on an AppleSingle file
create Create an AppleSingle file
extract Extract contents of an AppleSingle file
filter Filter an AppleSingle file
help Displays help information about the specified command
info Display information about an AppleSingle file
```
## Subcommand help

View File

@ -1,25 +1,30 @@
plugins {
id 'org.springframework.boot' version '2.0.2.RELEASE'
id 'org.springframework.boot' version '2.6.1'
id 'java'
id 'application'
}
sourceCompatibility = '11'
targetCompatibility = '11'
repositories {
jcenter()
}
apply plugin: 'application'
mainClassName = "io.github.applecommander.applesingle.tools.asu.Main"
bootJar {
manifest {
attributes(
'Implementation-Title': 'applesingle',
'Implementation-Version': "${version} (${new Date().format('yyyy-MM-dd HH:mm')})"
)
}
mavenCentral()
}
dependencies {
compile 'info.picocli:picocli:3.0.2'
compile project(':applesingle-api')
implementation 'info.picocli:picocli:4.7.5'
implementation project(':applesingle-api')
}
application {
mainClass = "io.github.applecommander.applesingle.tools.asu.Main"
}
bootJar {
manifest {
attributes(
'Implementation-Title': 'applesingle',
'Implementation-Version': "${project.version} (${new Date().format('yyyy-MM-dd HH:mm')})"
)
}
}

View File

@ -2,18 +2,23 @@ package io.github.applecommander.applesingle.tools.asu;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.function.BiConsumer;
import io.github.applecommander.applesingle.AppleSingle;
import io.github.applecommander.applesingle.AppleSingleReader;
import io.github.applecommander.applesingle.Entry;
import io.github.applecommander.applesingle.EntryType;
import io.github.applecommander.applesingle.FileDatesInfo;
import io.github.applecommander.applesingle.ProdosFileInfo;
import io.github.applecommander.applesingle.Utilities;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
@ -43,29 +48,21 @@ public class AnalyzeCommand implements Callable<Void> {
@Override
public Void call() throws IOException {
byte[] fileData = stdinFlag ? AppleSingle.toByteArray(System.in) : Files.readAllBytes(path);
byte[] fileData = stdinFlag ? Utilities.toByteArray(System.in) : Files.readAllBytes(path);
if (verboseFlag) this.verbose = System.out;
State state = new State(fileData);
match(state, "Magic number", "Not an AppleSingle file - magic number does not match.",
AppleSingle.MAGIC_NUMBER);
int version = match(state, "Version", "Only recognize AppleSingle versions 1 and 2.",
AppleSingle.VERSION_NUMBER1, AppleSingle.VERSION_NUMBER2);
verbose.printf(" .. Version 0x%08x\n", version);
state.read(16, "Filler");
int numberOfEntries = state.read(Short.BYTES, "Number of entries").getShort();
verbose.printf(" .. Entries = %d\n", numberOfEntries);
List<Entry> entries = new ArrayList<>();
for (int i = 0; i < numberOfEntries; i++) {
ByteBuffer buffer = state.read(12, String.format("Entry #%d", i+1));
Entry entry = new Entry(i+1, buffer);
entry.print(verbose);
entries.add(entry);
}
entries.sort((a,b) -> Integer.compare(a.offset, b.offset));
for (Entry entry : entries) entryReport(state, entry);
List<IntRange> used = new ArrayList<>();
HexDumper dumper = HexDumper.standard();
AppleSingleReader reader = AppleSingleReader.builder(fileData)
.readAtReporter((start,chunk,desc) -> used.add(IntRange.of(start, start + chunk.length)))
.readAtReporter((start,chunk,desc) -> dumper.dump(start, chunk, desc))
.versionReporter(this::reportVersion)
.numberOfEntriesReporter(this::reportNumberOfEntries)
.entryReporter(this::reportEntry)
.build();
AppleSingle.asEntries(reader);
List<IntRange> ranges = IntRange.normalize(state.used);
List<IntRange> ranges = IntRange.normalize(used);
if (ranges.size() == 1 && ranges.get(0).getLow() == 0 && ranges.get(0).getHigh() == fileData.length) {
verbose.printf("The entirety of the file was used.\n");
} else {
@ -74,99 +71,55 @@ public class AnalyzeCommand implements Callable<Void> {
}
return null;
}
public int match(State state, String description, String message, int... expecteds) throws IOException {
ByteBuffer buffer = state.read(Integer.BYTES, description);
int actual = buffer.getInt();
for (int expected : expecteds) {
if (actual == expected) return actual;
}
throw new IOException(String.format("%s Aborting.", message));
public void reportVersion(int version) {
verbose.printf(" .. %s\n", VERSION_TEXT.getOrDefault(version, "Unrecognized version!"));
}
public void entryReport(State state, Entry entry) throws IOException {
String entryName = AppleSingle.ENTRY_TYPE_NAMES.getOrDefault(entry.entryId, "Unknown");
ByteBuffer buffer = state.readAt(entry.offset, entry.length,
String.format("Entry #%d data (%s)", entry.index, entryName));
switch (entry.entryId) {
case 3:
case 4:
case 13:
displayEntryString(buffer, entryName);
break;
case 8:
displayFileDatesInfo(buffer, entryName);
break;
case 11:
displayProdosFileInfo(buffer, entryName);
break;
default:
verbose.printf(" .. No further details for this entry type (%s).\n", entryName);
break;
}
public void reportNumberOfEntries(int numberOfEntries) {
verbose.printf(" .. Number of entries = %d\n", numberOfEntries);
}
public void displayEntryString(ByteBuffer buffer, String entryName) {
StringBuilder sb = new StringBuilder();
while (buffer.hasRemaining()) {
int ch = Byte.toUnsignedInt(buffer.get()) & 0x7f;
sb.append((char)ch);
}
verbose.printf(" .. %s: '%s'\n", entryName, sb.toString());
public void reportEntry(Entry entry) {
String entryName = EntryType.findNameOrUnknown(entry);
verbose.printf(" .. Entry: entryId=%d (%s), offset=%d, length=%d\n", entry.getEntryId(),
entryName, entry.getOffset(), entry.getLength());
REPORTERS.getOrDefault(entry.getEntryId(), this::reportDefaultEntry)
.accept(entry, entryName);
}
public void displayFileDatesInfo(ByteBuffer buffer, String entryName) {
FileDatesInfo info = new FileDatesInfo(buffer.getInt(), buffer.getInt(), buffer.getInt(), buffer.getInt());
private void reportDefaultEntry(Entry entry, String entryName) {
verbose.printf(" .. No further details for this entry type (%s).\n", entryName);
}
private void reportStringEntry(Entry entry, String entryName) {
verbose.printf(" .. %s: '%s'\n", entryName, Utilities.entryToAsciiString(entry));
}
private void reportFileDatesInfoEntry(Entry entry, String entryName) {
FileDatesInfo info = FileDatesInfo.fromEntry(entry);
verbose.printf(" .. %s -\n", entryName);
verbose.printf(" Creation: %s\n", info.getCreationInstant().toString());
verbose.printf(" Modification: %s\n", info.getModificationInstant().toString());
verbose.printf(" Backup: %s\n", info.getBackupInstant().toString());
verbose.printf(" Access: %s\n", info.getAccessInstant().toString());
}
public void displayProdosFileInfo(ByteBuffer buffer, String entryName) {
ProdosFileInfo info = new ProdosFileInfo(buffer.getShort(), buffer.getShort(), buffer.getInt());
private void reportProdosFileInfoEntry(Entry entry, String entryName) {
ProdosFileInfo info = ProdosFileInfo.fromEntry(entry);
verbose.printf(" .. %s -\n", entryName);
verbose.printf(" Access: %02X\n", info.getAccess());
verbose.printf(" File Type: %04X\n", info.getFileType());
verbose.printf(" Aux. Type: %04X\n", info.getAuxType());
}
public static class State {
private final byte[] data;
private int pos = 0;
private List<IntRange> used = new ArrayList<>();
private HexDumper dumper = HexDumper.standard();
public State(byte[] data) {
this.data = data;
}
public ByteBuffer read(int len, String description) throws IOException {
return readAt(pos, len, description);
}
public ByteBuffer readAt(int start, int len, String description) throws IOException {
byte[] chunk = new byte[len];
System.arraycopy(data, start, chunk, 0, len);
ByteBuffer buffer = ByteBuffer.wrap(chunk)
.order(ByteOrder.BIG_ENDIAN)
.asReadOnlyBuffer();
dumper.dump(start, chunk, description);
used.add(IntRange.of(start, start+len));
pos= start+len;
return buffer;
}
}
public static class Entry {
private int index;
private int entryId;
private int offset;
private int length;
public Entry(int index, ByteBuffer buffer) {
this.index = index;
this.entryId = buffer.getInt();
this.offset = buffer.getInt();
this.length = buffer.getInt();
}
public void print(PrintStream ps) {
ps.printf(" .. Entry #%d, entryId=%d (%s), offset=%d, length=%d\n", index, entryId,
AppleSingle.ENTRY_TYPE_NAMES.getOrDefault(entryId, "Unknown"), offset, length);
private static final Map<Integer,String> VERSION_TEXT = new HashMap<Integer,String>() {
private static final long serialVersionUID = 7142066556402030814L;
{
put(AppleSingle.VERSION_NUMBER1, "Version 1");
put(AppleSingle.VERSION_NUMBER2, "Version 2");
}
};
private final Map<Integer,BiConsumer<Entry,String>> REPORTERS = new HashMap<Integer,BiConsumer<Entry,String>>();
{
REPORTERS.put(EntryType.REAL_NAME.entryId, this::reportStringEntry);
REPORTERS.put(EntryType.COMMENT.entryId, this::reportStringEntry);
REPORTERS.put(EntryType.SHORT_NAME.entryId, this::reportStringEntry);
REPORTERS.put(EntryType.FILE_DATES_INFO.entryId, this::reportFileDatesInfoEntry);
REPORTERS.put(EntryType.PRODOS_FILE_INFO.entryId, this::reportProdosFileInfoEntry);
}
}

View File

@ -9,6 +9,7 @@ import java.util.Optional;
import java.util.concurrent.Callable;
import io.github.applecommander.applesingle.AppleSingle;
import io.github.applecommander.applesingle.Utilities;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
@ -99,7 +100,7 @@ public class CreateCommand implements Callable<Void> {
public byte[] prepDataFork() throws IOException {
byte[] dataFork = null;
if (stdinForkType == ForkType.data) {
dataFork = AppleSingle.toByteArray(System.in);
dataFork = Utilities.toByteArray(System.in);
} else if (dataForkFile != null) {
dataFork = Files.readAllBytes(dataForkFile);
}
@ -116,7 +117,7 @@ public class CreateCommand implements Callable<Void> {
public byte[] prepResourceFork() throws IOException {
byte[] resourceFork = null;
if (stdinForkType == ForkType.resource) {
resourceFork = AppleSingle.toByteArray(System.in);
resourceFork = Utilities.toByteArray(System.in);
} else if (resourceForkFile != null) {
resourceFork = Files.readAllBytes(resourceForkFile);
}

View File

@ -0,0 +1,167 @@
package io.github.applecommander.applesingle.tools.asu;
import io.github.applecommander.applesingle.AppleSingle;
import io.github.applecommander.applesingle.Utilities;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.Callable;
/**
* Supports editing of AppleSingle archives.
*/
@Command(name = "edit", description = { "Edit an AppleSingle file" },
parameterListHeading = "%nParameters:%n",
descriptionHeading = "%n",
footerHeading = "%nNotes:%n",
footer = { "* Dates should be supplied like '2007-12-03T10:15:30.00Z'.",
"* 'Known' ProDOS file types: TXT, BIN, INT, BAS, REL, SYS.",
"* Include the output file or specify stdout" },
optionListHeading = "%nOptions:%n")
public class EditCommand implements Callable<Void> {
@Option(names = { "-h", "--help" }, description = "Show help for subcommand", usageHelp = true)
private boolean helpFlag;
@Option(names = "--stdin", description = "Read AppleSingle file from stdin")
private boolean stdinFlag;
@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 = ProdosFileTypeConverter.class)
private Integer filetype;
@Option(names = "--auxtype", description = "Set the ProDOS auxtype", converter = IntegerTypeConverter.class)
private Integer auxtype;
@Option(names = "--creation-date", description = "Set the file creation date")
private Instant creationDate;
@Option(names = "--modification-date", description = "Set the file modification date")
private Instant modificationDate;
@Option(names = "--backup-date", description = "Set the file backup date")
private Instant backupDate;
@Option(names = "--access-date", description = "Set the file access date")
private Instant accessDate;
@Parameters(arity = "0..1", description = "AppleSingle file to modify")
private Path file;
@Override
public Void call() throws IOException {
validateArguments();
AppleSingle original = stdinFlag ? AppleSingle.read(System.in) : AppleSingle.read(file);
byte[] dataFork = prepDataFork();
byte[] resourceFork = prepResourceFork();
AppleSingle applesingle = buildAppleSingle(original, dataFork, resourceFork);
writeAppleSingle(applesingle);
return null;
}
public void validateArguments() throws IOException {
if ((stdinFlag && file != null) || (!stdinFlag && file == null)) {
throw new IOException("Please choose one of stdin or input file for original");
}
if ((dataForkFile != null && stdinForkType == ForkType.data)
|| (resourceForkFile != null && stdinForkType == ForkType.resource)) {
throw new IOException("Stdin only supports one type of fork for input");
}
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 = Utilities.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 = Utilities.toByteArray(System.in);
} else if (resourceForkFile != null) {
resourceFork = Files.readAllBytes(resourceForkFile);
}
return resourceFork;
}
public AppleSingle buildAppleSingle(AppleSingle original,
byte[] dataFork,
byte[] resourceFork) throws IOException {
AppleSingle.Builder builder = AppleSingle.builder(original);
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);
if (dataForkFile != null || resourceForkFile != null) {
Path path = Optional.ofNullable(dataForkFile).orElse(resourceForkFile);
BasicFileAttributes attribs = Files.readAttributes(path, BasicFileAttributes.class);
builder.creationDate(attribs.creationTime().toInstant());
builder.modificationDate(attribs.lastModifiedTime().toInstant());
builder.accessDate(attribs.lastAccessTime().toInstant());
}
if (creationDate != null) builder.creationDate(creationDate);
if (modificationDate != null) builder.modificationDate(modificationDate);
if (backupDate != null) builder.backupDate(backupDate);
if (accessDate != null) builder.accessDate(accessDate);
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);
}
}
}

View File

@ -0,0 +1,140 @@
package io.github.applecommander.applesingle.tools.asu;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.github.applecommander.applesingle.AppleSingle;
import io.github.applecommander.applesingle.Entry;
import io.github.applecommander.applesingle.EntryType;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
/**
* Allow filtering of an an AppleSingle archive.
* Both source and target can be a file or stream.
*/
@Command(name = "filter", description = { "Filter 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 FilterCommand 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;
@Option(names = "--stdout", description = "Write AppleSingle to stdout.")
private boolean stdoutFlag;
@Option(names = { "-o", "--output" }, description = "Write AppleSingle to file.")
private Path outputFile;
@Parameters(arity = "0..1", description = "File to process")
private Path inputFile;
@Option(names = "--prodos", description = "Apply ProDOS specific filter")
private boolean prodosFlag;
@Option(names = { "--mac", "--macintosh" }, description = "Apply Macintosh specific filter")
private boolean macintoshFlag;
@Option(names = "--msdos", description = "Apply MS-DOS specific filter")
private boolean msdosFlag;
@Option(names = "--afp", description = "Apply AFP specific filter")
private boolean afpFlag;
@Option(names = "--include", description = "Filter by including specific entryIds", split = ",")
private Integer[] includeEntryIds;
@Option(names = "--exclude", description = "Filter by excluding specific entryIds", split = ",")
private Integer[] excludeEntryIds;
@Override
public Void call() throws IOException {
try (PrintStream ps = this.stdoutFlag ? new PrintStream(NullOutputStream.INSTANCE) : System.out) {
OSFilter osFilter = validate();
SortedSet<Integer> included = toSet(includeEntryIds, osFilter);
SortedSet<Integer> excluded = toSet(excludeEntryIds, null);
List<Entry> entries = stdinFlag ? AppleSingle.asEntries(System.in) : AppleSingle.asEntries(inputFile);
List<Entry> newEntries = entries.stream()
.filter(e -> included.isEmpty() || included.contains(e.getEntryId()))
.filter(e -> excluded.isEmpty() || !excluded.contains(e.getEntryId()))
.collect(Collectors.toList());
// Check if we ended up with different things
SortedSet<EntryType> before = toEntryType(entries);
SortedSet<EntryType> after = toEntryType(newEntries);
before.removeAll(after); // Note: modifies before
if (!before.isEmpty()) {
ps.printf("Removed the following entries:\n");
before.forEach(e -> ps.printf("- %s\n", e.name));
} else {
ps.printf("No entries removed.\n");
}
OutputStream outputStream = stdoutFlag ? System.out : Files.newOutputStream(outputFile);
AppleSingle.write(outputStream, newEntries);
}
return null;
}
private OSFilter validate() throws IOException {
long count = Stream.of(prodosFlag, macintoshFlag, msdosFlag, afpFlag).filter(flag -> flag).count();
// Expected boundaries
if (count == 0) return null;
if (count > 1) throw new IOException("Please choose only one operating system flag!");
// Set the correct OS Flag
if (prodosFlag) return OSFilter.PRODOS;
if (macintoshFlag) return OSFilter.MACINTOSH;
if (msdosFlag) return OSFilter.MS_DOS;
if (afpFlag) return OSFilter.AFP;
// Not a clue how you can get here...
throw new IOException("Bug! Please put in a ticket or a pull request. Thanks! :-)");
}
private SortedSet<Integer> toSet(Integer[] entryIds, OSFilter filter) {
SortedSet<Integer> set = new TreeSet<>();
Optional.ofNullable(entryIds)
.map(a -> Arrays.asList(a))
.ifPresent(set::addAll);
Optional.ofNullable(filter)
.map(f -> f.types)
.ifPresent(t -> Stream.of(t)
.map(e -> e.entryId)
.collect(Collectors.toCollection(() -> set)));
return set;
}
private SortedSet<EntryType> toEntryType(Collection<Entry> entries) {
return entries.stream()
.map(e -> e.getEntryId())
.map(EntryType::find)
.collect(Collectors.toCollection(() -> new TreeSet<EntryType>()));
}
public enum OSFilter {
PRODOS(EntryType.DATA_FORK, EntryType.RESOURCE_FORK, EntryType.REAL_NAME, EntryType.FILE_DATES_INFO,
EntryType.PRODOS_FILE_INFO),
MACINTOSH(EntryType.DATA_FORK, EntryType.RESOURCE_FORK, EntryType.REAL_NAME, EntryType.COMMENT,
EntryType.ICON_BW, EntryType.ICON_COLOR, EntryType.FILE_DATES_INFO, EntryType.FINDER_INFO,
EntryType.MACINTOSH_FILE_INFO),
MS_DOS(EntryType.DATA_FORK, EntryType.REAL_NAME, EntryType.FILE_DATES_INFO, EntryType.MSDOS_FILE_INFO),
AFP(EntryType.DATA_FORK, EntryType.REAL_NAME, EntryType.FILE_DATES_INFO, EntryType.SHORT_NAME,
EntryType.AFP_FILE_INFO, EntryType.DIRECTORY_ID);
public final EntryType[] types;
private OSFilter(EntryType... types) {
this.types = types;
}
}
}

View File

@ -32,6 +32,9 @@ public class HexDumper {
description = ""; // Only on first line!
offset += line.length;
}
if (data.length == 0) {
printLine.print(address+offset, data, String.format("%s (empty)", description));
}
}
public void standardLine(int address, byte[] data, String description) {
@ -48,7 +51,7 @@ public class HexDumper {
char ch = ' ';
if (i < data.length) {
byte b = data[i];
ch = (b >= ' ') ? (char)b : '.';
ch = (b >= ' ' && Byte.toUnsignedInt(b) < 0x7f) ? (char)b : '.';
}
ps.printf("%c", ch);
}

View File

@ -6,6 +6,7 @@ import java.util.Optional;
import java.util.concurrent.Callable;
import io.github.applecommander.applesingle.AppleSingle;
import io.github.applecommander.applesingle.FileDatesInfo;
import io.github.applecommander.applesingle.ProdosFileInfo;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
@ -32,7 +33,9 @@ public class InfoCommand implements Callable<Void> {
@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.");
@ -42,7 +45,20 @@ public class InfoCommand implements Callable<Void> {
System.out.printf(" File Type: 0x%02X\n", prodosFileInfo.getFileType());
System.out.printf(" Auxtype: 0x%04X\n", prodosFileInfo.getAuxType());
}
System.out.printf("File dates info:\n");
if (applesingle.getFileDatesInfo() == null) {
System.out.println(" Not supplied.");
} else {
FileDatesInfo fileDatesInfo = applesingle.getFileDatesInfo();
System.out.printf(" Creation: %s\n", fileDatesInfo.getCreationInstant());
System.out.printf(" Modification: %s\n", fileDatesInfo.getModificationInstant());
System.out.printf(" Access: %s\n", fileDatesInfo.getAccessInstant());
System.out.printf(" Backup: %s\n", fileDatesInfo.getBackupInstant());
}
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))

View File

@ -17,7 +17,7 @@ public class IntRange {
/** Create an integer range. */
public static IntRange of(int low, int high) {
if (low == high) throw new UnsupportedOperationException("low and high cannot be the same");
if (low > high) throw new UnsupportedOperationException("low cannot be greater than high");
return new IntRange(Math.min(low,high), Math.max(low,high));
}
/** Normalize a list by combining all integer ranges that match. */
@ -61,6 +61,6 @@ public class IntRange {
}
@Override
public String toString() {
return String.format("[%d..%d)", low, high);
return String.format("%d..%d", low, high-1);
}
}

View File

@ -15,14 +15,22 @@ import picocli.CommandLine.Option;
commandListHeading = "%nCommands:%n",
optionListHeading = "%nOptions:%n",
description = "AppleSingle utility",
subcommands = { HelpCommand.class, InfoCommand.class, AnalyzeCommand.class, CreateCommand.class, ExtractCommand.class })
subcommands = {
AnalyzeCommand.class,
CreateCommand.class,
EditCommand.class,
ExtractCommand.class,
FilterCommand.class,
HelpCommand.class,
InfoCommand.class,
})
public class Main implements Runnable {
@Option(names = "--debug", description = "Dump full stack trackes if an error occurs")
@Option(names = "--debug", description = "Dump full stack traces if an error occurs")
private static boolean debugFlag;
public static void main(String[] args) {
try {
CommandLine.run(new Main(), args);
new CommandLine(new Main()).execute(args);
} catch (Throwable t) {
if (Main.debugFlag) {
t.printStackTrace(System.err);

View File

@ -1,10 +1,14 @@
package io.github.applecommander.applesingle.tools.asu;
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[] { Main.class.getPackage().getImplementationVersion() };
return new String[] {
String.format("CLI: %s", Main.class.getPackage().getImplementationVersion()),
String.format("API: %s", AppleSingle.VERSION)
};
}
}