commit cabcf33f6024890acb1abf536f95067f9a9c4198
parent a6e2f40fd3f7b4cf34230d373b05e8c3853d7dcf
Author: Michael Camilleri <[email protected]>
Date: Thu, 2 Jul 2026 22:31:54 +0900
Rebase Data slices in the Across Lite converter
PUZToXDConverter.convert(puzData:) reads the header, grid, and string
table through absolute offsets, but a Data slice keeps its parent's
indices, so handing the converter a slice with a non-zero startIndex
would trap on the first header byte. Every current call site passes a
freshly read file, which made the crash latent rather than reachable.
The converter now re-bases a sliced input onto zero-based storage at
entry, which covers every absolute read in one place; the extension
parsers already index relative to startIndex because they receive slices
of this same buffer.
This commit also re-stamps the bundled and debug puzzle sources from
`CmVer: 3` to the current 7, and brings Fillmake's writer literal (which
had drifted to 5 despite its keep-in-step comment) up to match. The
header is inert for bundled puzzles — games are stamped with
XD.currentCmVersion at creation and the refresh path compares that
stored stamp, never the file — so the stale values were only misleading.
Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat:
16 files changed, 48 insertions(+), 14 deletions(-)
diff --git a/Crossmake/Sources/Fillmake/main.swift b/Crossmake/Sources/Fillmake/main.swift
@@ -1195,7 +1195,7 @@ func exportXD(state: PuzzleState, black: [Bool], slots: [Slot], options: Options
"Title: \(options.title)",
// Keep in step with XD.currentCmVersion in the app; Crossmake is a
// standalone package and can't reference that symbol directly.
- "CmVer: 5",
+ "CmVer: 7",
"Revision: 1",
"Bundle: cm-basic",
"Author: \(options.author)",
diff --git a/Crossmate/Services/PUZToXDConverter.swift b/Crossmate/Services/PUZToXDConverter.swift
@@ -27,6 +27,12 @@ enum PUZToXDConverter {
}
static func convert(puzData data: Data) throws -> String {
+ // The offsets below are absolute, but a Data slice keeps its parent's
+ // indices, so re-base a slice onto fresh zero-based storage first.
+ // (The extension parsers already index relative to startIndex — they
+ // receive slices of this data — but the header/string-table walk here
+ // assumes zero-based subscripts throughout.)
+ let data = data.startIndex == 0 ? data : Data(data)
guard data.count >= 0x34 else {
throw ConversionError(message: "Across Lite file is too short.")
}
diff --git a/Puzzles/cm-starter/cm-starter-0001.xd b/Puzzles/cm-starter/cm-starter-0001.xd
@@ -1,5 +1,5 @@
Title: Crossmate Starter #1
-CmVer: 3
+CmVer: 7
Revision: 1
Bundle: cm-starter
Publisher: Michael Camilleri
diff --git a/Puzzles/cm-starter/cm-starter-0002.xd b/Puzzles/cm-starter/cm-starter-0002.xd
@@ -1,5 +1,5 @@
Title: Crossmate Starter #2
-CmVer: 3
+CmVer: 7
Revision: 1
Bundle: cm-starter
Publisher: Michael Camilleri
diff --git a/Puzzles/cm-starter/cm-starter-0003.xd b/Puzzles/cm-starter/cm-starter-0003.xd
@@ -1,5 +1,5 @@
Title: Crossmate Starter #3
-CmVer: 3
+CmVer: 7
Revision: 1
Bundle: cm-starter
Publisher: Michael Camilleri
diff --git a/Puzzles/cm-starter/cm-starter-0004.xd b/Puzzles/cm-starter/cm-starter-0004.xd
@@ -1,5 +1,5 @@
Title: Crossmate Starter #4
-CmVer: 3
+CmVer: 7
Revision: 1
Bundle: cm-starter
Publisher: Michael Camilleri
diff --git a/Puzzles/cm-starter/cm-starter-0005.xd b/Puzzles/cm-starter/cm-starter-0005.xd
@@ -1,5 +1,5 @@
Title: Crossmate Starter #5
-CmVer: 3
+CmVer: 7
Revision: 1
Bundle: cm-starter
Publisher: Michael Camilleri
diff --git a/Puzzles/cm-starter/cm-starter-0006.xd b/Puzzles/cm-starter/cm-starter-0006.xd
@@ -1,5 +1,5 @@
Title: Crossmate Starter #6
-CmVer: 3
+CmVer: 7
Revision: 1
Bundle: cm-starter
Publisher: Michael Camilleri
diff --git a/Puzzles/cm-starter/cm-starter-0007.xd b/Puzzles/cm-starter/cm-starter-0007.xd
@@ -1,5 +1,5 @@
Title: Crossmate Starter #7
-CmVer: 3
+CmVer: 7
Revision: 1
Bundle: cm-starter
Publisher: Michael Camilleri
diff --git a/Puzzles/cm-starter/cm-starter-0008.xd b/Puzzles/cm-starter/cm-starter-0008.xd
@@ -1,5 +1,5 @@
Title: Crossmate Starter #8
-CmVer: 3
+CmVer: 7
Revision: 1
Bundle: cm-starter
Publisher: Michael Camilleri
diff --git a/Puzzles/cm-starter/cm-starter-0009.xd b/Puzzles/cm-starter/cm-starter-0009.xd
@@ -1,5 +1,5 @@
Title: Crossmate Starter #9
-CmVer: 3
+CmVer: 7
Revision: 1
Bundle: cm-starter
Publisher: Michael Camilleri
diff --git a/Puzzles/cm-starter/cm-starter-0010.xd b/Puzzles/cm-starter/cm-starter-0010.xd
@@ -1,5 +1,5 @@
Title: Crossmate Starter #10
-CmVer: 3
+CmVer: 7
Revision: 1
Bundle: cm-starter
Publisher: Michael Camilleri
diff --git a/Puzzles/debug/garden.xd b/Puzzles/debug/garden.xd
@@ -1,5 +1,5 @@
Title: Garden Party
-CmVer: 3
+CmVer: 7
Bundle: debug
Author: Crossmate
Copyright: Public domain test puzzle
diff --git a/Puzzles/debug/morning.xd b/Puzzles/debug/morning.xd
@@ -1,5 +1,5 @@
Title: Morning Routine
-CmVer: 3
+CmVer: 7
Bundle: debug
Author: Crossmate
Copyright: Public domain test puzzle
diff --git a/Puzzles/debug/sample.xd b/Puzzles/debug/sample.xd
@@ -1,5 +1,5 @@
Title: Crossmate Demo
-CmVer: 3
+CmVer: 7
Bundle: debug
Author: Crossmate
Copyright: Public domain test puzzle
diff --git a/Tests/Unit/PUZToXDConverterTests.swift b/Tests/Unit/PUZToXDConverterTests.swift
@@ -282,6 +282,34 @@ struct PUZToXDConverterTests {
#expect(puzzle.cells[3][4].solution == "R19X")
}
+ @Test("A Data slice with a non-zero start index converts identically")
+ func sliceWithNonZeroStartIndexConverts() throws {
+ let data = try puzData(
+ width: 3,
+ height: 3,
+ solution: "ABCDEFGHI",
+ title: "Sliced",
+ author: "Tester",
+ copyright: "",
+ clues: [
+ "Across 1",
+ "Down 1",
+ "Down 2",
+ "Down 3",
+ "Across 4",
+ "Across 5"
+ ]
+ )
+
+ var padded = Data([0xFF, 0xFF, 0xFF, 0xFF])
+ padded.append(data)
+ let slice = padded[4...]
+ #expect(slice.startIndex != 0)
+
+ let source = try PUZToXDConverter.convert(puzData: slice)
+ #expect(source == (try PUZToXDConverter.convert(puzData: data)))
+ }
+
private func puzData(
width: UInt8,
height: UInt8,