Adding Nibble Checkit binary proofreader. Juggled interface names a bit.

This commit is contained in:
Rob Greene
2026-01-13 13:40:09 -06:00
parent f3ea9f659d
commit f8af76959e
11 changed files with 313 additions and 162 deletions
@@ -0,0 +1,36 @@
package org.applecommander.bastools.api.proofreaders;
import java.io.ByteArrayOutputStream;
/**
* Standard interface for proofreaders that evaluate a binary program.
*/
public interface BinaryDataProofReader {
/** Parses each line into a byte stream and then uses addBytes to roll into checksum. */
default void addProgram(String text) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
int address = 0;
for (var line : text.lines().toList()) {
var info = parseLine(line);
if (address == 0) address = info.address;
outputStream.writeBytes(info.code);
};
addBytes(address, outputStream.toByteArray());
}
/** Parses the line and adds to the current checksums. */
void addBytes(int address, byte... binary);
/** Handy method to parse a line from text into address + bytes. */
static LineInfo parseLine(final String line) {
String[] parts = line.split(":");
assert parts.length != 0;
int address = Integer.parseInt(parts[0], 16);
String[] bytes = parts[1].split(" ");
assert bytes.length > 0;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
for (String value : bytes) {
outputStream.write(Integer.parseInt(value, 16));
}
return new LineInfo(address, outputStream.toByteArray(), line);
}
record LineInfo(int address, byte[] code, String line) {}
}
@@ -0,0 +1,31 @@
package org.applecommander.bastools.api.proofreaders;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
/**
* Standard interface for proofreaders that evaluate a binary program.
*/
public interface BinaryInputBufferProofReader {
default void addProgram(String code) {
code.lines().forEach(this::addLine);
}
/** Parses the line and adds to the current checksums. */
void addLine(String line);
/** Converts to standard lines adds to the current checksums. */
default void addBytes(int address, byte... binary) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PrintWriter pw = new PrintWriter(outputStream);
for (int i=0; i<binary.length; i++) {
if (i % 16 == 0) {
if (i > 0) pw.println();
pw.printf("%04X:", address+i);
}
else {
pw.print(' ');
}
pw.printf("%02X", binary[i]);
}
addProgram(outputStream.toString());
}
}
@@ -1,12 +0,0 @@
package org.applecommander.bastools.api.proofreaders;
/**
* Standard interface for proofreaders that evaluate a binary program.
*/
public interface BinaryProofReader {
default void addProgram(String text) {
// TODO
}
/** Parses the line and adds to the current checksums. */
void addBytes(int address, byte... binary);
}
@@ -1,6 +1,6 @@
package org.applecommander.bastools.api.proofreaders;
public class NibbleAppleCheckerBinary implements BinaryProofReader {
public class NibbleAppleCheckerBinary implements BinaryDataProofReader {
private final String filename;
private int length;
private final NibbleAppleCheckerChecksum checksum = new NibbleAppleCheckerChecksum();
@@ -1,142 +0,0 @@
/*
* bastools
* Copyright (C) 2026 Robert Greene
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.applecommander.bastools.api.proofreaders;
import org.applecommander.bastools.api.Configuration;
public class NibbleCheckit implements ApplesoftInputBufferProofReader {
private final Configuration config;
private final Checksum totalChecksum = new Checksum();
private final Checksum lineChecksum = new Checksum();
public NibbleCheckit(Configuration config) {
this.config = config;
}
@Override
public Configuration getConfiguration() {
return config;
}
/** {@inheritDoc} */
@Override
public void addProgramText(String code) {
System.out.println("Nibble Checkit, Copyright 1988, Microsparc Inc.");
ApplesoftInputBufferProofReader.super.addProgramText(code);
System.out.printf("TOTAL: %02X%02X\n", totalChecksum.checksum & 0xff, totalChecksum.checksum >> 8);
}
/** {@inheritDoc} */
@Override
public void addLine(final String originalLine) {
// Calculate and output line value
lineChecksum.reset();
// The ? => PRINT replacement always occurs, including in strings!
String line = originalLine.replace("?", "PRINT");
boolean inQuote = false;
StringBuilder remLettersSeen = new StringBuilder();
for (char ch : line.toCharArray()) {
if (ch == '"') inQuote = !inQuote;
if (!inQuote && ch == ' ') continue;
lineChecksum.add(ch|0x80);
// we only allow R E M from a comment; skip rest of the comment
remLettersSeen.append(ch);
if ("REM".contentEquals(remLettersSeen)) {
break;
}
else if ("R".contentEquals(remLettersSeen) || "RE".contentEquals(remLettersSeen)) {
// Keep them, this may be a comment
}
else {
remLettersSeen = new StringBuilder();
}
}
int cs = ( (lineChecksum.checksum & 0xff) - (lineChecksum.checksum >> 8) ) & 0xff;
System.out.printf("%02X | %s\n", cs, originalLine);
// Update program values
int lineNumber = Integer.parseInt(line.split(" ")[0].trim());
totalChecksum.add(lineNumber & 0xff);
totalChecksum.add(lineNumber >> 8);
}
public int getLineChecksumValue() {
return lineChecksum.value();
}
public int getTotalChecksumValue() {
return totalChecksum.value();
}
/**
* Perform Nibble Checkit algorithm. Note that a large part of the assembly is shifting the
* new value byte into the checksum; if the checksum itself causes a carry in the high byte,
* an exclusive-or is done with (maybe?) the CCITT CRC polynomial.
* <pre>
* 864F- A2 08 L864F LDX #$08 ; 8 bits
* 8651- 0A L8651 ASL ; Acc. is value
* 8652- 26 08 ROL $08 ; $08/$09 is low/high byte of checksum
* 8654- 26 09 ROL $09
* 8656- 90 0E BCC L8666 ; no carry, continue loop
* 8658- 48 PHA ; save current value
* 8659- A5 08 LDA $08
* 865B- 49 21 EOR #$21 ; low byte of $1021
* 865D- 85 08 STA $08
* 865F- A5 09 LDA $09
* 8661- 49 10 EOR #$10 ; high byte of $1021
* 8663- 85 09 STA $09
* 8665- 68 PLA ; restore current value
* 8666- CA L8666 DEX
* 8667- D0 E8 BNE L8651 ; loop through all 8 bits
* 8669- 60 RTS
* </pre>
* In this implementation, we just mash the value and the checksum together, so the
* resulting number is <code>0x00CCCCVV</code>. Then the intermediate carry bits aren't
* a concern, and we simply need to detect when we overflow three bytes and then do the
* XOR. The result is the middle two bytes where the checksum resides. (Note that the
* <code>0x1021</code> was shifted by a byte as well.)
*/
public static class Checksum implements ProofReaderChecksum {
private int checksum = 0;
@Override
public void reset() {
this.checksum = 0;
}
@Override
public void add(int value) {
assert value >= 0 && value <= 0xff;
int work = (checksum << 8) | value;
for (int i=0; i<8; i++) {
work <<= 1;
// Note: If we run into negative issues somehow, this could also be "((work & 0xff000000) != 0)".
if (work > 0xffffff) {
work &= 0xffffff;
work ^= 0x102100;
}
}
checksum = work >> 8;
}
@Override
public int value() {
return checksum;
}
}
}
@@ -0,0 +1,89 @@
/*
* bastools
* Copyright (C) 2026 Robert Greene
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.applecommander.bastools.api.proofreaders;
import org.applecommander.bastools.api.Configuration;
public class NibbleCheckitApplesoft implements ApplesoftInputBufferProofReader {
private final Configuration config;
private final NibbleCheckitChecksum totalChecksum = new NibbleCheckitChecksum();
private final NibbleCheckitChecksum lineChecksum = new NibbleCheckitChecksum();
public NibbleCheckitApplesoft(Configuration config) {
this.config = config;
}
@Override
public Configuration getConfiguration() {
return config;
}
/** {@inheritDoc} */
@Override
public void addProgramText(String code) {
System.out.println("Nibble Checkit, Copyright 1988, Microsparc Inc.");
ApplesoftInputBufferProofReader.super.addProgramText(code);
System.out.printf("TOTAL: %04X\n", getTotalChecksumValue());
}
/** {@inheritDoc} */
@Override
public void addLine(final String originalLine) {
// Calculate and output line value
lineChecksum.reset();
// The ? => PRINT replacement always occurs, including in strings!
String line = originalLine.replace("?", "PRINT");
boolean inQuote = false;
StringBuilder remLettersSeen = new StringBuilder();
for (char ch : line.toCharArray()) {
if (ch == '"') inQuote = !inQuote;
if (!inQuote && ch == ' ') continue;
lineChecksum.add(ch|0x80);
// we only allow R E M from a comment; skip rest of the comment
remLettersSeen.append(ch);
if ("REM".contentEquals(remLettersSeen)) {
break;
}
else if ("R".contentEquals(remLettersSeen) || "RE".contentEquals(remLettersSeen)) {
// Keep them, this may be a comment
}
else {
remLettersSeen = new StringBuilder();
}
}
System.out.printf("%02X | %s\n", getLineChecksumValue(), originalLine);
// Update program values
int lineNumber = Integer.parseInt(line.split(" ")[0].trim());
totalChecksum.add(lineNumber & 0xff);
totalChecksum.add(lineNumber >> 8);
}
public int getLineChecksumValue() {
return ( (lineChecksum.value() & 0xff) - (lineChecksum.value() >> 8) ) & 0xff;
}
public int getTotalChecksumValue() {
int high = totalChecksum.value() & 0xff;
int low = (totalChecksum.value() >> 8) & 0xff;
return high << 8 | low;
}
}
@@ -0,0 +1,36 @@
package org.applecommander.bastools.api.proofreaders;
public class NibbleCheckitBinary implements BinaryInputBufferProofReader {
private final NibbleCheckitChecksum lineChecksum = new NibbleCheckitChecksum();
private final NibbleCheckitChecksum totalChecksum = new NibbleCheckitChecksum();
@Override
public void addProgram(final String code) {
System.out.println("Nibble Checkit, Copyright 1988, Microsparc Inc.");
BinaryInputBufferProofReader.super.addProgram(code);
System.out.printf("TOTAL: %04X\n", getTotalChecksumValue());
}
@Override
public void addLine(final String line) {
lineChecksum.reset();
for (char ch : line.toCharArray()) {
lineChecksum.add(ch|0x80);
}
System.out.printf("%02X | %s\n", getLineChecksumValue(), line);
var info = BinaryDataProofReader.parseLine(line);
for (byte b : info.code()) {
totalChecksum.add(Byte.toUnsignedInt(b));
}
}
public int getLineChecksumValue() {
return ( (lineChecksum.value() & 0xff) - (lineChecksum.value() >> 8) ) & 0xff;
}
public int getTotalChecksumValue() {
int high = totalChecksum.value() & 0xff;
int low = (totalChecksum.value() >> 8) & 0xff;
return high << 8 | low;
}
}
@@ -0,0 +1,58 @@
package org.applecommander.bastools.api.proofreaders;
/**
* Perform Nibble Checkit algorithm. Note that a large part of the assembly is shifting the
* new value byte into the checksum; if the checksum itself causes a carry in the high byte,
* an exclusive-or is done with (maybe?) the CCITT CRC polynomial.
* <pre>
* 864F- A2 08 L864F LDX #$08 ; 8 bits
* 8651- 0A L8651 ASL ; Acc. is value
* 8652- 26 08 ROL $08 ; $08/$09 is low/high byte of checksum
* 8654- 26 09 ROL $09
* 8656- 90 0E BCC L8666 ; no carry, continue loop
* 8658- 48 PHA ; save current value
* 8659- A5 08 LDA $08
* 865B- 49 21 EOR #$21 ; low byte of $1021
* 865D- 85 08 STA $08
* 865F- A5 09 LDA $09
* 8661- 49 10 EOR #$10 ; high byte of $1021
* 8663- 85 09 STA $09
* 8665- 68 PLA ; restore current value
* 8666- CA L8666 DEX
* 8667- D0 E8 BNE L8651 ; loop through all 8 bits
* 8669- 60 RTS
* </pre>
* In this implementation, we just mash the value and the checksum together, so the
* resulting number is <code>0x00CCCCVV</code>. Then the intermediate carry bits aren't
* a concern, and we simply need to detect when we overflow three bytes and then do the
* XOR. The result is the middle two bytes where the checksum resides. (Note that the
* <code>0x1021</code> was shifted by a byte as well.)
*/
public class NibbleCheckitChecksum implements ProofReaderChecksum {
private int checksum = 0;
@Override
public void reset() {
this.checksum = 0;
}
@Override
public void add(int value) {
assert value >= 0 && value <= 0xff;
int work = (checksum << 8) | value;
for (int i = 0; i < 8; i++) {
work <<= 1;
// Note: If we run into negative issues somehow, this could also be "((work & 0xff000000) != 0)".
if (work > 0xffffff) {
work &= 0xffffff;
work ^= 0x102100;
}
}
checksum = work >> 8;
}
@Override
public int value() {
return checksum;
}
}
@@ -28,7 +28,7 @@ import static org.junit.Assert.assertEquals;
* Perform some rudimentary testing of the Nibble Checkit algorithm.
* Note that some of the algorithm is replicated in the "perform" methods.
*/
public class NibbleCheckitTest {
public class NibbleCheckitApplesoftTest {
@Test
public void testLineChecksums() {
assertEquals(0x37, performLineCalc("10 REM"));
@@ -44,8 +44,7 @@ public class NibbleCheckitTest {
@Test
public void testProgramChecksum() {
// Note: The value displayed is 1CB9, but the code prints low byte first...
assertEquals(0xb91c, performProgramCalc(10, 20, 30));
assertEquals(0x1cb9, performProgramCalc(10, 20, 30));
}
protected int performLineCalc(String text) {
@@ -53,9 +52,9 @@ public class NibbleCheckitTest {
.preserveNumbers(true)
.sourceFile(new File("test.bas"))
.build();
NibbleCheckit proofreader = new NibbleCheckit(config);
NibbleCheckitApplesoft proofreader = new NibbleCheckitApplesoft(config);
proofreader.addLine(text);
return ( (proofreader.getLineChecksumValue() & 0xff) - (proofreader.getLineChecksumValue() >> 8) ) & 0xff;
return proofreader.getLineChecksumValue();
}
protected int performProgramCalc(int... lineNumbers) {
@@ -63,7 +62,7 @@ public class NibbleCheckitTest {
.preserveNumbers(true)
.sourceFile(new File("test.bas"))
.build();
NibbleCheckit proofreader = new NibbleCheckit(config);
NibbleCheckitApplesoft proofreader = new NibbleCheckitApplesoft(config);
for (int lineNumber : lineNumbers) {
// We ignore the line, but line also computes the program checksum
proofreader.addLine(Integer.toString(lineNumber));
@@ -0,0 +1,56 @@
package org.applecommander.bastools.api.proofreaders;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
public class NibbleCheckitBinaryTest {
@Test
public void testExtermShapes() {
// Nibble, June 1989, page 53
final String code = """
6100:04 00 0a 00 29 00 4d 00
6108:73 00 49 29 15 3e 3c 37
6110:35 3e 3f 3e 2e 4d 2c 24
6118:15 2d 2e 3e 96 3a 3c fe
6120:3b 67 25 2c 25 15 3e 3f
6128:00 92 32 2e 24 24 25 2d
6130:c1 2d 15 35 35 15 36 27
6138:3c 3c c1 3f 5f f7 2e 2d
6140:ac f5 3f 2e 35 3f 17 2d
6148:2d 3e 3f 3f 00 49 09 ad
6150:3f bf 6d 29 ad ff 3b 3f
6158:3f 17 6d 29 4d 6d 3a df
6160:3f df bf 2d 2d 6d 29 f5
6168:1b ff 1b bf 6d 29 f5 3b
6170:3f 3f 00 35 35 35 35 35
6178:35 35 35 35 35 25 c1 c1
6180:c1 c1 c1 c1 c1 c1 c1 37
6188:37 37 37 f7 3a 3e 3e 3e
6190:3e 06 00
""".toUpperCase(); // yes, being lazy
final int[] expectedLine = {
0x59, 0x5c, 0xa5, 0x24, 0xa6, 0x8a, 0xf9, 0x52,
0x32, 0x6c, 0xa5, 0x2c, 0x35, 0xf4, 0xb0, 0x03,
0xb0, 0xdf, 0x11
};
final int expectedTotal = 0x1dd8;
check(code, expectedLine, expectedTotal);
}
public void check(final String code, final int[] expectedLine, final int expectedTotal) {
NibbleCheckitBinary proofreader = new NibbleCheckitBinary();
List<String> lines = code.lines().toList();
assertEquals("lines are expected length", expectedLine.length, lines.size());
for (int i=0; i< lines.size(); i++) {
String line = lines.get(i);
proofreader.addLine(line);
int expectedChecksum = expectedLine[i];
assertEquals("line checksum", expectedChecksum, proofreader.getLineChecksumValue());
}
assertEquals("total checksum", expectedTotal, proofreader.getTotalChecksumValue());
}
}
@@ -352,7 +352,7 @@ public class Main implements Callable<Integer> {
@Option(names = "--checkit", description = "Apply Nibble Checkit (ca 1988) to code")
public void selectNibbleCheckit(boolean flag) {
this.proofReaderFn = (c,p) -> {
NibbleCheckit proofreader = new NibbleCheckit(c);
NibbleCheckitApplesoft proofreader = new NibbleCheckitApplesoft(c);
proofreader.addProgram(p);
};
}