crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

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:
MCrossmake/Sources/Fillmake/main.swift | 2+-
MCrossmate/Services/PUZToXDConverter.swift | 6++++++
MPuzzles/cm-starter/cm-starter-0001.xd | 2+-
MPuzzles/cm-starter/cm-starter-0002.xd | 2+-
MPuzzles/cm-starter/cm-starter-0003.xd | 2+-
MPuzzles/cm-starter/cm-starter-0004.xd | 2+-
MPuzzles/cm-starter/cm-starter-0005.xd | 2+-
MPuzzles/cm-starter/cm-starter-0006.xd | 2+-
MPuzzles/cm-starter/cm-starter-0007.xd | 2+-
MPuzzles/cm-starter/cm-starter-0008.xd | 2+-
MPuzzles/cm-starter/cm-starter-0009.xd | 2+-
MPuzzles/cm-starter/cm-starter-0010.xd | 2+-
MPuzzles/debug/garden.xd | 2+-
MPuzzles/debug/morning.xd | 2+-
MPuzzles/debug/sample.xd | 2+-
MTests/Unit/PUZToXDConverterTests.swift | 28++++++++++++++++++++++++++++
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,