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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added (`relocate-binary-readers-into-dak`)
- `DataReader`: sequential reader with offset tracking; throws `BinaryReadError.outOfBounds` and never advances on a short read.
- `BinaryDataReader`: big- and little-endian integer reads and writes at an offset. The named readers return 0, and the writers return `false`, when out of range; they never trap and do no misaligned loads. The generic `read(_:from:at:endian:)` and `write(_:to:at:endian:)` throw instead.
- `StringExtractor.extractStrings(from:minLength:maxLength:)`: printable runs, like `strings`.
- `BinaryReadError` and `ByteOrder`.
- All of them read `Data` slices relative to their start index. They replace FileSystemCore's copies of the same names.

### Changed (`platform-and-test-hygiene`)
- Building without CryptoKit is a compile-time error. The non-cryptographic placeholder that `SHA256.hash` returned on such platforms, and the `fatalError` paths in `MD5Strategy`/`SHA1Strategy`, are gone. `SHA256.hash(data:)` delegates to `SHA256Strategy`.
- Platform claims match the build: macOS 12 and iOS 15 (both built in CI). WASM, tvOS and watchOS are no longer claimed.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ One library product and target: **`DesignAlgorithmsKit`** (`Sources/DesignAlgori
| Probabilistic filters | `BloomFilter`, `CountingBloomFilter`, `BloomFilterHashable` |
| Merkle tree | `MerkleTree`, `MerkleProof`, `MerkleHashable` |
| Graphs | `Graph<T>` (directed, BFS reachability, unreachable-node detection) |
| Binary data | `DataReader` (sequential, throwing), `BinaryDataReader` (big/little-endian at an offset; lenient named readers and checked `read`/`write`), `BinaryReadError`, `ByteOrder` |
| Compression | `Zlib`/`ZlibError` (zlib, gzip and raw DEFLATE framing by window bits, via system zlib), `Gzip` (in-memory GZIP) |
| Encryption | `HybridEncryption` (Curve25519 + HKDF-SHA256 + ChaCha20-Poly1305) |
| Text / paths | `FilenameTokenizer`, `GitObjectLayout`, `FallbackTextDecoder`/`TextDecodingStrategy`/`StringEncodingStrategy`/`TextDecoderRegistry`, `Data.decodedText(using:)` |
| Text / paths | `FilenameTokenizer`, `StringExtractor` (printable runs, like `strings`), `GitObjectLayout`, `FallbackTextDecoder`/`TextDecodingStrategy`/`StringEncodingStrategy`/`TextDecoderRegistry`, `Data.decodedText(using:)` |

## Platforms

Expand Down
140 changes: 140 additions & 0 deletions Sources/DesignAlgorithmsKit/Algorithms/Binary/BinaryDataReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import Foundation

/// Reads and writes integers at an offset in `Data`, in big- or little-endian byte order.
///
/// Offsets are relative to the data's start index, and fields are read byte by byte, so
/// slices and unaligned offsets are fine.
///
/// - The named readers (`readUInt32LE(from:at:)` and so on) return 0, and the named writers
/// return `false`, when the offset is negative or the field does not fit. They never trap.
/// - ``read(_:from:at:endian:)`` and ``write(_:to:at:endian:)`` throw
/// ``BinaryReadError/outOfBounds(offset:count:available:)`` instead, for callers that must
/// tell a missing field from a real 0.
public enum BinaryDataReader {

// MARK: - Checked

/// Reads a `T` at `offset`.
public static func read<T: FixedWidthInteger>(
_ type: T.Type,
from data: Data,
at offset: Int,
endian: ByteOrder
) throws -> T {
let size = MemoryLayout<T>.size
try checkBounds(offset: offset, count: size, dataCount: data.count)
let start = data.startIndex + offset
var value: T = 0
for index in 0..<size {
let byte = T(truncatingIfNeeded: data[start + index])
let shift = endian == .little ? index * 8 : (size - 1 - index) * 8
value |= byte << shift
}
return value
}

/// Writes `value` at `offset`.
public static func write<T: FixedWidthInteger>(
_ value: T,
to data: inout Data,
at offset: Int,
endian: ByteOrder
) throws {
let size = MemoryLayout<T>.size
try checkBounds(offset: offset, count: size, dataCount: data.count)
let start = data.startIndex + offset
for index in 0..<size {
let shift = endian == .little ? index * 8 : (size - 1 - index) * 8
data[start + index] = UInt8(truncatingIfNeeded: value >> shift)
}
}

// MARK: - Big-endian reads (0 when out of range)

public static func readInt32BE(from data: Data, at offset: Int) -> Int32 {
lenient(Int32.self, data, offset, .big)
}

public static func readUInt32BE(from data: Data, at offset: Int) -> UInt32 {
lenient(UInt32.self, data, offset, .big)
}

public static func readInt16BE(from data: Data, at offset: Int) -> Int16 {
lenient(Int16.self, data, offset, .big)
}

public static func readUInt16BE(from data: Data, at offset: Int) -> UInt16 {
lenient(UInt16.self, data, offset, .big)
}

// MARK: - Little-endian reads (0 when out of range)

public static func readInt32LE(from data: Data, at offset: Int) -> Int32 {
lenient(Int32.self, data, offset, .little)
}

public static func readUInt32LE(from data: Data, at offset: Int) -> UInt32 {
lenient(UInt32.self, data, offset, .little)
}

public static func readInt16LE(from data: Data, at offset: Int) -> Int16 {
lenient(Int16.self, data, offset, .little)
}

public static func readUInt16LE(from data: Data, at offset: Int) -> UInt16 {
lenient(UInt16.self, data, offset, .little)
}

public static func readUInt8(from data: Data, at offset: Int) -> UInt8 {
lenient(UInt8.self, data, offset, .little)
}

// MARK: - Writes (false when out of range)

@discardableResult
public static func writeInt32BE(_ value: Int32, to data: inout Data, at offset: Int) -> Bool {
lenientWrite(value, &data, offset, .big)
}

@discardableResult
public static func writeInt32LE(_ value: Int32, to data: inout Data, at offset: Int) -> Bool {
lenientWrite(value, &data, offset, .little)
}

@discardableResult
public static func writeUInt32BE(_ value: UInt32, to data: inout Data, at offset: Int) -> Bool {
lenientWrite(value, &data, offset, .big)
}

@discardableResult
public static func writeUInt32LE(_ value: UInt32, to data: inout Data, at offset: Int) -> Bool {
lenientWrite(value, &data, offset, .little)
}

@discardableResult
public static func writeUInt16BE(_ value: UInt16, to data: inout Data, at offset: Int) -> Bool {
lenientWrite(value, &data, offset, .big)
}

@discardableResult
public static func writeUInt16LE(_ value: UInt16, to data: inout Data, at offset: Int) -> Bool {
lenientWrite(value, &data, offset, .little)
}

// MARK: - Internal

private static func checkBounds(offset: Int, count: Int, dataCount: Int) throws {
guard offset >= 0, offset <= dataCount, count <= dataCount - offset else {
let available = (offset >= 0 && offset <= dataCount) ? dataCount - offset : 0
throw BinaryReadError.outOfBounds(offset: offset, count: count, available: available)
}
}

private static func lenient<T: FixedWidthInteger>(_ type: T.Type, _ data: Data, _ offset: Int, _ endian: ByteOrder) -> T {
(try? read(type, from: data, at: offset, endian: endian)) ?? 0
}

private static func lenientWrite<T: FixedWidthInteger>(_ value: T, _ data: inout Data, _ offset: Int, _ endian: ByteOrder) -> Bool {
(try? write(value, to: &data, at: offset, endian: endian)) != nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

/// Errors from ``DataReader`` and the checked ``BinaryDataReader`` methods.
public enum BinaryReadError: Error, Equatable, Sendable {
/// `count` bytes were needed at `offset`, but only `available` bytes exist past it
/// (0 when the offset itself is outside the data).
case outOfBounds(offset: Int, count: Int, available: Int)
}

/// Byte order of a multi-byte field.
public enum ByteOrder: Sendable {
case big
case little
}
87 changes: 87 additions & 0 deletions Sources/DesignAlgorithmsKit/Algorithms/Binary/DataReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import Foundation

/// Reads binary data sequentially, tracking the offset.
///
/// Offsets are relative to the data's start index, so slices read correctly. A read that
/// runs past the end throws ``BinaryReadError/outOfBounds(offset:count:available:)`` and
/// leaves the offset unchanged.
///
/// ```swift
/// let reader = DataReader(data: header)
/// let magic = try reader.readUInt32BE()
/// let count = try reader.readUInt16LE()
/// ```
public final class DataReader {
private let data: Data

/// Current position, from 0 to the data's count.
public private(set) var offset: Int

/// - Parameter data: Data to read from
public init(data: Data) {
self.data = data
self.offset = 0
}

/// Moves to `newOffset` (0 through the data's count).
public func seek(to newOffset: Int) throws {
guard newOffset >= 0 && newOffset <= data.count else {
throw BinaryReadError.outOfBounds(offset: newOffset, count: 0, available: 0)
}
offset = newOffset
}

/// Reads `length` bytes.
public func readData(length: Int) throws -> Data {
let start = try reserve(length)
return data.subdata(in: start..<(start + length))
}

public func readUInt8() throws -> UInt8 {
try read(UInt8.self, .little)
}

public func readUInt16LE() throws -> UInt16 {
try read(UInt16.self, .little)
}

public func readUInt16BE() throws -> UInt16 {
try read(UInt16.self, .big)
}

public func readUInt32LE() throws -> UInt32 {
try read(UInt32.self, .little)
}

public func readUInt32BE() throws -> UInt32 {
try read(UInt32.self, .big)
}

/// True when at least `count` bytes remain.
public func hasBytesAvailable(_ count: Int) -> Bool {
count >= 0 && count <= bytesRemaining
}

/// Bytes left after the current offset.
public var bytesRemaining: Int {
max(0, data.count - offset)
}

// MARK: - Internal

private func read<T: FixedWidthInteger>(_ type: T.Type, _ endian: ByteOrder) throws -> T {
let value = try BinaryDataReader.read(type, from: data, at: offset, endian: endian)
offset += MemoryLayout<T>.size
return value
}

/// Checks that `count` bytes remain, advances past them and returns their absolute start index.
private func reserve(_ count: Int) throws -> Data.Index {
guard count >= 0, count <= bytesRemaining else {
throw BinaryReadError.outOfBounds(offset: offset, count: count, available: bytesRemaining)
}
let start = data.startIndex + offset
offset += count
return start
}
}
44 changes: 44 additions & 0 deletions Sources/DesignAlgorithmsKit/Algorithms/Text/StringExtractor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Foundation

/// Finds printable text runs in binary data, like the Unix `strings` command.
public enum StringExtractor {
/// Default minimum run length kept by ``extractStrings(from:minLength:maxLength:)``.
public static let minimumStringLength = 4

/// Default maximum length of each returned run.
public static let maximumStringLength = 256

/// Returns each run of printable bytes, in order.
///
/// A run is a sequence of bytes in 32...126 or 160...255 (read as Latin-1), tab, line feed
/// or carriage return. Each run is truncated to `maxLength` characters, trimmed of
/// surrounding whitespace and newlines, and kept when at least `minLength` characters remain.
public static func extractStrings(
from data: Data,
minLength: Int = minimumStringLength,
maxLength: Int = maximumStringLength
) -> [String] {
var strings: [String] = []
var current = ""

func flush() {
let trimmed = current.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.count >= minLength {
strings.append(trimmed)
}
current = ""
}

for byte in data {
if (32...126).contains(byte) || byte >= 160 || byte == 9 || byte == 10 || byte == 13 {
if current.count < maxLength {
current.append(Character(UnicodeScalar(byte)))
}
} else {
flush()
}
}
flush()
return strings
}
}
Loading
Loading