Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/ack/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* Validate discriminated codec output against its selected map branch or a
matching typed runtime branch, including required fields and refinements,
without decoding runtime values again or adding encode callback passes.
* Reject duration millisecond inputs that overflow the platform's `Duration`
representation instead of silently decoding to a different duration.
* Expose the shared `deepUnmodifiableCopy` helper for generated immutable
constructors. It snapshots nested collections without validating data or
changing opaque noncollection objects.
Expand Down
11 changes: 10 additions & 1 deletion packages/ack/lib/src/ack.dart
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ final class Ack {
/// Creates a bidirectional duration codec between integer milliseconds and
/// `Duration` values.
///
/// Inputs that cannot be represented by `Duration` on the current platform
/// without changing their millisecond value are rejected.
///
/// Runtime invariant: the `Duration` must be a whole number of
/// milliseconds (sub-millisecond precision is rejected to avoid silent
/// truncation on encode).
Expand Down Expand Up @@ -271,7 +274,13 @@ bool _isWholeMillisecondDuration(Duration value) {
}

Duration _decodeDuration(int milliseconds) {
return Duration(milliseconds: milliseconds);
final duration = Duration(milliseconds: milliseconds);
if (duration.inMilliseconds != milliseconds) {
throw const FormatException(
'Milliseconds cannot be represented by Duration without loss.',
);
}
return duration;
}

int _encodeDuration(Duration value) => value.inMilliseconds;
Expand Down
78 changes: 78 additions & 0 deletions packages/ack/test/schemas/duration_range_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import 'package:ack/ack.dart';
import 'package:test/test.dart';

final _usesJavaScriptNumberRepresentation = <num>[1e20].single is int;

void main() {
test('preserves milliseconds within the exact range on VM and web', () {
final schema = Ack.duration();
for (final milliseconds in <int>[0, 1, -1, 9007199254740, -9007199254740]) {
final parsed = schema.parse(milliseconds)!;
expect(parsed.inMilliseconds, milliseconds);
expect(schema.encode(parsed), milliseconds);
}
});

test('retains lossless JSON integer normalization', () {
final schema = Ack.duration();
expect(schema.encode(schema.parse(1500.0)), 1500);
expect(schema.encode(schema.parse(-1500.0)), -1500);
expect(schema.safeParse(1.5).isFail, isTrue);
});

test('any accepted large input must round-trip without corruption', () {
final schema = Ack.duration();
for (final text in [
'9007199254740991',
'-9007199254740991',
'9223372036854775',
'-9223372036854775',
'9223372036854776',
'-9223372036854776',
'9223372036854775807',
'-9223372036854775808',
]) {
final milliseconds = int.parse(text);
final result = schema.safeParse(milliseconds);
if (result.isOk) {
final parsed = result.getOrNull()!;
expect(parsed.inMilliseconds, milliseconds, reason: text);
expect(schema.encode(parsed), milliseconds, reason: text);
}
}
});

group(
'native signed 64-bit Duration bounds',
() {
test('accepts both whole-millisecond supported edges', () {
final schema = Ack.duration();
final edge = (BigInt.parse('9223372036854775807') ~/ BigInt.from(1000))
.toInt();
for (final milliseconds in [edge, -edge]) {
final parsed = schema.parse(milliseconds)!;
expect(parsed.inMilliseconds, milliseconds);
expect(schema.encode(parsed), milliseconds);
}
});

test('rejects values outside the representable microsecond range', () {
final schema = Ack.duration();
for (final text in [
'9223372036854776',
'-9223372036854776',
'9223372036854775807',
'-9223372036854775808',
]) {
final result = schema.safeParse(int.parse(text));
expect(result.isFail, isTrue, reason: text);
expect(result.getError(), isA<SchemaTransformError>(), reason: text);
expect(result.getError().cause, isA<FormatException>(), reason: text);
}
});
},
skip: _usesJavaScriptNumberRepresentation
? 'JavaScript does not use the native signed 64-bit representation.'
: false,
);
}
Loading