Skip to content
Open
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
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
macos:
name: macOS
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Show toolchain
run: swift --version
- name: Build
run: swift build -v
- name: Test
run: swift test -v

linux:
name: Linux (Swift ${{ matrix.swift }})
runs-on: ubuntu-latest
strategy:
matrix:
swift: ["5.10"]
steps:
- uses: actions/checkout@v4
- uses: swift-actions/setup-swift@v2
with:
swift-version: ${{ matrix.swift }}
- name: Show toolchain
run: swift --version
- name: Build
run: swift build -v
- name: Test
run: swift test -v
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,13 @@ playground.xcworkspace
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.swiftpm/

.build/

# DocC build output
*.doccarchive/

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
Expand Down
4 changes: 4 additions & 0 deletions .spi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
version: 1
builder:
configs:
- documentation_targets: [DataKit]
7 changes: 0 additions & 7 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

This file was deleted.

99 changes: 99 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Changelog

All notable changes to DataKit are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
While the major version is `0`, breaking changes are released as minor version bumps.

## [Unreleased]

_Nothing yet._

## [0.2.0] - 2026-05-29

First release of the modernized DataKit. This is a maintenance-focused release: no new
format features, but a significant pass over packaging, documentation, and Swift
concurrency support. It contains source-breaking changes — see
[Migrating to 0.2.0](Sources/DataKit/DataKit.docc/Migrating-to-0.2.0.md) for a guided
upgrade.

### Added

- **Full Swift Concurrency support.** The public API now conforms to `Sendable`, so
formats, conversions, and values can be shared across actor and task boundaries.
- **Documentation.** Every public symbol now carries a `///` doc comment, and a DocC
catalog (`DataKit.docc`) introduces the two-phase read model, the environment system,
and the conversion pipeline. Hosted documentation is available on the
[Swift Package Index](https://swiftpackageindex.com/QuickBirdEng/DataKit/documentation).
- **Declared platforms.** `Package.swift` now declares minimum platforms explicitly:
iOS 13, macOS 10.15, tvOS 13, watchOS 6.
- **Continuous integration.** A GitHub Actions workflow builds and tests on macOS and
Linux.
- **`.spi.yml`** so the Swift Package Index builds and hosts the DocC documentation.
- **`NOTES.md`** tracking candidate future features (error context enrichment, `Bounded`,
`BitField`, a `DataKitTesting` target, additional conversions).

### Changed

- **Swift tools version raised from 5.4 to 5.10.** 5.4 could not actually compile the
result-builder code (`buildPartialBlock` requires 5.7); 5.10 is the supported floor and
enables checked `Sendable` conformances on key-path-backed types.
- The library target enables the `StrictConcurrency` and `ExistentialAny` upcoming
features.
- The README has been restructured for approachability: a quick-start example up front, a
concepts reference, and an explicit requirements section.

### Fixed

- **`skipChecksumVerification` is now honored.** Reading the environment value but never
consulting it meant checksums were always verified even when verification had been
disabled. The flag now correctly suppresses verification (while still consuming the
checksum bytes) for both the explicit `ChecksumProperty` and the bare-checksum builder
form (`CRC32.default` inside a format block).
- Corrected the broken `crc-swift` repository link in the README (`crc-swift.org` →
`crc-swift`).

### Removed

- **`CannotWriteNilError`.** This error type was declared publicly but never thrown
anywhere in the library. (Breaking only for code that referenced the type by name.)

### Breaking Changes

These require source changes in dependent code. See
[Migrating to 0.2.0](Sources/DataKit/DataKit.docc/Migrating-to-0.2.0.md) for details and
fixes.

- Conforming types of `Readable`, `Writable`, and `ReadWritable` must now be `Sendable`.
All standard-library conformers already satisfy this.
- `FormatProperty` — and therefore `ReadableProperty` and `WritableProperty` — now refine
`Sendable`; their `Root` must be `Sendable`.
- Closures passed to `Custom` and `Convert` (and the fluent `Property.read`/`.write`/
`.converted` helpers), to `Using` and `Environment`, and to the `transformEnvironment`
modifiers must now be `@Sendable`. The value passed to `.environment(_:_:)` must be
`Sendable`.
- `Checksum` types used inside a format must be `Sendable`.
- `UnexpectedValueError.expectedValue` and `.actualValue` are now typed `any Sendable`
instead of `Any`.
- `KeyPath` no longer conforms to `FormatProperty` / `ReadableProperty` /
`WritableProperty` directly. Bare key-path syntax (`\.field`) inside a format builder
still works; passing a key path where a `FormatProperty` is expected **outside** a
builder now requires wrapping it explicitly as `Property(\.field)`.
- Minimum Swift toolchain is now 5.10 (previously declared 5.4).

## [0.1.1] - 2024-03-14

### Fixed

- Bumped `crc-swift` to 0.1.1.
- Fixed a compilation issue on x86_64 hardware.

## [0.1.0]

- Initial public release.

[Unreleased]: https://github.com/QuickBirdEng/DataKit/compare/0.2.0...HEAD
[0.2.0]: https://github.com/QuickBirdEng/DataKit/compare/0.1.1...0.2.0
[0.1.1]: https://github.com/QuickBirdEng/DataKit/compare/0.1.0...0.1.1
[0.1.0]: https://github.com/QuickBirdEng/DataKit/releases/tag/0.1.0
139 changes: 139 additions & 0 deletions NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Feature ideas

Drafts of feature ideas surfaced during the 2026 audit. Each entry is sized for a single
GitHub issue when there is appetite to act on it. None of these are committed work — the
audit explicitly scoped this maintenance pass to docs, packaging, README, and Sendable.

## 1. Error context enrichment

**What.** Wrap thrown errors at every `FormatProperty` boundary with the current byte
offset, the `Scope`/`Using`/`KeyPath` breadcrumb path, and a reference to the underlying
error. Surface as a public `DataKitError` struct with `.byteOffset`, `.path`, and
`.underlyingError`. Today errors like `LengthExceededError`, `ConversionError`, and
`ReadContext.ValueDoesNotExistError` are positionless — almost useless for debugging a
real-world wire-protocol failure on byte 47 of a 200-byte packet.

**Sketch.**
```swift
do {
let v = try Packet(data)
} catch let error as DataKitError {
print(error.byteOffset) // 47
print(error.path) // "Packet.payload -> Scope -> Convert(\.temperature)"
throw error.underlyingError // the original LengthExceededError
}
```

**Feasibility.** Medium. Every existing `FormatProperty` site that throws must be
touched. Risk of double-wrapping when errors propagate through several layers; add an
"already enriched" check. No public-API breakage.

**Prior art.** `construct.py` `ConstructError.stream_position`,
`swift-binary-parsing.ParsingError.location`.

## 2. `Bounded` / `FixedLength` sub-format

**What.** Run a child format inside an explicit byte budget. On read, assert the child
consumed *exactly* N bytes (or pad with a fill byte if `mode: .padding`). On write,
assert the child produced N (or pad). Builds directly on `Scope` — most of the read-side
code already exists there.

**Sketch.**
```swift
Bounded(length: 16, padding: 0x00) {
Convert(\.name) { $0.encoded(.utf8).dynamicCount }
.suffix(0 as UInt8, isRequired: false)
}

// Or length read from another keyPath:
\.payloadLength
Bounded(length: \.payloadLength) {
\.payload
}
```

**Feasibility.** Easy. ~80% of the implementation is already in
`Sources/DataKit/Property/Scope.swift` — just add an `expectedLength: Int?` and
post-check `container.index` movement.

**Prior art.** Kaitai Struct `size:`; construct.py `FixedSized` / `Padded`.

## 3. `BitField` / `Bits` primitive

**What.** Open a "bit cursor" inside the byte stream and read/write N-bit fields. Today
the only sub-byte expression is an `OptionSet`, which works only for one-bit-per-field
boolean flags. Real protocols (BLE, IPv4 header, H.264 NALU, MIDI) regularly pack
multi-bit fields into one byte.

**Sketch.**
```swift
BitField { // consumes whole bytes; must end byte-aligned
Bits(\.version, width: 4) // top nibble
Bits(\.headerLength, width: 4)
Bits(\.dscp, width: 6)
Bits(\.ecn, width: 2)
}
.bitOrder(.msbFirst)
```

**Feasibility.** Medium. Needs `BitReadContainer` / `BitWriteContainer` wrappers and a
decision on MSB-first vs LSB-first bit order. Hardest design choice: error vs pad when a
block doesn't end on a byte boundary. No existing-code rewrites.

**Prior art.** Kaitai `b3`, `b13`; construct.py `BitStruct` / `BitsInteger`.

## 4. `DataKitTesting` round-trip helpers (new target)

**What.** A `DataKitTesting` library target shipping `assertRoundTrip(_:bytes:)` and a
diff-friendly hex-dump on failure. The round-trip invariant is the central correctness
contract of `ReadWritable`; every consumer reinvents this same test today.

**Sketch.**
```swift
import DataKitTesting

func testWeatherPacketRoundTrip() throws {
try assertRoundTrip(
WeatherStationUpdate(features: [.hasTemperature], ...),
bytes: [0x02, 0x01, 0x42, ...]
)
}
```

**Feasibility.** Easy. Mostly: write, read back, compare; re-write, compare bytes. New
target in `Package.swift` so it doesn't bloat the runtime library.

**Prior art.** Pattern is widely used (`swift-snapshot-testing` philosophy,
QuickCheck-style property tests).

## 5. More conversions: LEB128, ZigZag varint, Q-format, half-float

**What.** Add the obvious missing numeric encodings to `Sources/DataKit/Conversions/`:

- LEB128 / ULEB128 (DWARF, Wasm, Protocol Buffers varints).
- ZigZag int↔uint (Protobuf `sint32` / `sint64`).
- Q-format fixed-point (Q15.16, Q7.8, etc. — DSP/audio/BLE).
- ASN.1 BER length octets (LDAP, SNMP, TLS certs).

`Float16` is already a `ReadWritable` conformance on arm64; consider a software fallback
for x86_64 if there is demand.

**Feasibility.** Easy each, but LEB128's variable byte count on write needs care. Same
pattern as `Conversion+PrefixCount.swift`. Can be shipped one PR at a time.

**Prior art.** Every binary-protocol library ships varints; Foundation does not.

---

## Out-of-scope ideas mentioned in the audit but not pursued

- **Async streaming reads** from `AsyncSequence<UInt8>` / `InputStream`. Hard — collides
with random-access-data assumptions in `Scope` and `Checksum`. Defer until concrete
demand.
- **`@ReadWritable` macro** that synthesizes `format` + `init(from:)` from declared
properties. Substantial implementation effort; worth reconsidering once Apple's
`swift-binary-parsing` story shakes out.
- **`OneOf` discriminated-union primitive.** Useful for TLV-style protocols but requires
a non-trivial type-erasure design.
- **Hex-dump / debug pretty-printer.** Strong UX win, but depends on the error-context
feature (#1) for the field-to-byte mapping.
12 changes: 11 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
// swift-tools-version: 5.4
// swift-tools-version: 5.10

import PackageDescription

let package = Package(
name: "DataKit",
platforms: [
.iOS(.v13),
.macOS(.v10_15),
.tvOS(.v13),
.watchOS(.v6),
],
products: [
.library(
name: "DataKit",
Expand All @@ -21,6 +27,10 @@ let package = Package(
name: "DataKit",
dependencies: [
.product(name: "CRC", package: "crc-swift"),
],
swiftSettings: [
.enableUpcomingFeature("StrictConcurrency"),
.enableUpcomingFeature("ExistentialAny"),
]
),
.testTarget(
Expand Down
Loading
Loading