From 097140df762e08ffc5f17488368f33832856cd5d Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Mon, 5 Oct 2026 03:32:17 +0200 Subject: [PATCH 1/3] fix(apple): contain measurement construction failures --- CHANGELOG.md | 3 +++ apple/include/OdrCoreObjC/ODRStyle.h | 9 +++++---- apple/src/ODRStyle.mm | 21 +++++++++++++++------ apple/tests/OdrCoreTests.swift | 13 +++++++++++++ 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a887c0e..0b20c5fae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ The release run heads these entries with the version and opens a fresh ## Unreleased +- **Breaking:** Apple `Measure` initializers are now failable. Invalid lengths + return `nil` instead of letting a C++ exception escape into Swift. + - Python, Java and Apple file/image/font reads report stream failures instead of returning partial data after a ZIP checksum or other read error. diff --git a/apple/include/OdrCoreObjC/ODRStyle.h b/apple/include/OdrCoreObjC/ODRStyle.h index 2223bd222..0284015c0 100644 --- a/apple/include/OdrCoreObjC/ODRStyle.h +++ b/apple/include/OdrCoreObjC/ODRStyle.h @@ -96,10 +96,11 @@ NS_SWIFT_NAME(Measure) /// Magnitude and unit as odrcore writes them, e.g. `12pt`. @property(nonatomic, readonly, copy) NSString *stringValue; -/// A css length as odrcore writes one, e.g. `3mm`. A magnitude with no unit -/// reads as unitless. -- (instancetype)initWithString:(NSString *)string; -- (instancetype)initWithMagnitude:(double)magnitude unit:(NSString *)unit; +/// Parses a length such as `3mm` or a unitless magnitude; returns nil on +/// failure. +- (nullable instancetype)initWithString:(NSString *)string; +- (nullable instancetype)initWithMagnitude:(double)magnitude + unit:(NSString *)unit; - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; diff --git a/apple/src/ODRStyle.mm b/apple/src/ODRStyle.mm index f39b489e2..607b77d87 100644 --- a/apple/src/ODRStyle.mm +++ b/apple/src/ODRStyle.mm @@ -121,20 +121,29 @@ - (NSString *)stringValue { return guarded_value([&] { return to_nsstring(_handle->to_string()); }, @""); } -- (instancetype)initWithString:(NSString *)string { +- (nullable instancetype)initWithString:(NSString *)string { if ((self = [super init]) == nil) { return nil; } - _handle = odr::Measure(to_string(string)); - return self; + return guarded_value( + [&]() -> ODRMeasure * { + _handle = odr::Measure(to_string(string)); + return self; + }, + nil); } -- (instancetype)initWithMagnitude:(double)magnitude unit:(NSString *)unit { +- (nullable instancetype)initWithMagnitude:(double)magnitude + unit:(NSString *)unit { if ((self = [super init]) == nil) { return nil; } - _handle = odr::Measure(magnitude, odr::DynamicUnit(to_string(unit))); - return self; + return guarded_value( + [&]() -> ODRMeasure * { + _handle = odr::Measure(magnitude, odr::DynamicUnit(to_string(unit))); + return self; + }, + nil); } - (const std::optional &)handle { diff --git a/apple/tests/OdrCoreTests.swift b/apple/tests/OdrCoreTests.swift index 0e97653cf..bec033b71 100644 --- a/apple/tests/OdrCoreTests.swift +++ b/apple/tests/OdrCoreTests.swift @@ -778,6 +778,19 @@ final class PdfAnnotationTests: XCTestCase { } } +final class MeasureTests: XCTestCase { + func testConstructionReturnsNilOnFailure() throws { + let parsed = try XCTUnwrap(Measure(string: "2.5cm")) + XCTAssertEqual(parsed.magnitude, 2.5) + XCTAssertEqual(parsed.unit, "cm") + XCTAssertEqual(Measure(magnitude: 2.5, unit: "cm")?.stringValue, "2.5cm") + XCTAssertEqual(Measure(string: "12")?.unit, "") + for invalid in ["", "invalid", "1e999px"] { + XCTAssertNil(Measure(string: invalid), invalid) + } + } +} + final class TableAddressTests: XCTestCase { func testRoundTrips() throws { XCTAssertEqual(TableAddress.columnNumber(from: "C"), 2) From 916a9140a18a15619774425556b28e6c7bd6c101 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Mon, 5 Oct 2026 23:01:58 +0200 Subject: [PATCH 2/3] fix(apple): keep the magnitude initializer of Measure non-failable It stores a magnitude and a unit and cannot fail, so it keeps its nonnull Swift signature. Only Measure(string:) parses and becomes failable. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Gb1fLafqqzehpqPuU6uBfn --- CHANGELOG.md | 4 ++-- apple/include/OdrCoreObjC/ODRStyle.h | 3 +-- apple/src/ODRStyle.mm | 11 +++-------- apple/tests/OdrCoreTests.swift | 2 +- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b20c5fae..bf26808bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,8 @@ The release run heads these entries with the version and opens a fresh ## Unreleased -- **Breaking:** Apple `Measure` initializers are now failable. Invalid lengths - return `nil` instead of letting a C++ exception escape into Swift. +- **Breaking:** Apple `Measure(string:)` is now failable. An invalid length + returns `nil` instead of letting a C++ exception escape into Swift. - Python, Java and Apple file/image/font reads report stream failures instead of returning partial data after a ZIP checksum or other read error. diff --git a/apple/include/OdrCoreObjC/ODRStyle.h b/apple/include/OdrCoreObjC/ODRStyle.h index 0284015c0..729357170 100644 --- a/apple/include/OdrCoreObjC/ODRStyle.h +++ b/apple/include/OdrCoreObjC/ODRStyle.h @@ -99,8 +99,7 @@ NS_SWIFT_NAME(Measure) /// Parses a length such as `3mm` or a unitless magnitude; returns nil on /// failure. - (nullable instancetype)initWithString:(NSString *)string; -- (nullable instancetype)initWithMagnitude:(double)magnitude - unit:(NSString *)unit; +- (instancetype)initWithMagnitude:(double)magnitude unit:(NSString *)unit; - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; diff --git a/apple/src/ODRStyle.mm b/apple/src/ODRStyle.mm index 607b77d87..c5f13c231 100644 --- a/apple/src/ODRStyle.mm +++ b/apple/src/ODRStyle.mm @@ -133,17 +133,12 @@ - (nullable instancetype)initWithString:(NSString *)string { nil); } -- (nullable instancetype)initWithMagnitude:(double)magnitude - unit:(NSString *)unit { +- (instancetype)initWithMagnitude:(double)magnitude unit:(NSString *)unit { if ((self = [super init]) == nil) { return nil; } - return guarded_value( - [&]() -> ODRMeasure * { - _handle = odr::Measure(magnitude, odr::DynamicUnit(to_string(unit))); - return self; - }, - nil); + _handle = odr::Measure(magnitude, odr::DynamicUnit(to_string(unit))); + return self; } - (const std::optional &)handle { diff --git a/apple/tests/OdrCoreTests.swift b/apple/tests/OdrCoreTests.swift index bec033b71..b844f78ac 100644 --- a/apple/tests/OdrCoreTests.swift +++ b/apple/tests/OdrCoreTests.swift @@ -783,7 +783,7 @@ final class MeasureTests: XCTestCase { let parsed = try XCTUnwrap(Measure(string: "2.5cm")) XCTAssertEqual(parsed.magnitude, 2.5) XCTAssertEqual(parsed.unit, "cm") - XCTAssertEqual(Measure(magnitude: 2.5, unit: "cm")?.stringValue, "2.5cm") + XCTAssertEqual(Measure(magnitude: 2.5, unit: "cm").stringValue, "2.5cm") XCTAssertEqual(Measure(string: "12")?.unit, "") for invalid in ["", "invalid", "1e999px"] { XCTAssertNil(Measure(string: invalid), invalid) From 330b09e4cd383921b5eaa9ae7407c4a579934fe5 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Mon, 5 Oct 2026 23:06:04 +0200 Subject: [PATCH 3/3] docs: list the failable Apple Measure(string:) as a fix A signature change in a binding wrapper is not breaking. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Gb1fLafqqzehpqPuU6uBfn --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf26808bb..ea122f9ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,8 @@ The release run heads these entries with the version and opens a fresh ## Unreleased -- **Breaking:** Apple `Measure(string:)` is now failable. An invalid length - returns `nil` instead of letting a C++ exception escape into Swift. +- Apple `Measure(string:)` is now failable. An invalid length returns `nil` + instead of letting a C++ exception escape into Swift. - Python, Java and Apple file/image/font reads report stream failures instead of returning partial data after a ZIP checksum or other read error.