diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index a07e30c..c06e30e 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,4 +1,4 @@
-name: Swift on Ubuntu (latest)
+name: Tests
on:
push:
@@ -9,18 +9,19 @@ on:
- master
jobs:
- tests:
+ linux:
+ name: Swift ${{ matrix.image }}
runs-on: ubuntu-latest
- # The official image, rather than fwal/setup-swift, which does not
- # support Ubuntu 26.04 (ubuntu-latest).
- container: swift:6.2
+ container: swift:${{ matrix.image }}
+ strategy:
+ fail-fast: false
+ matrix:
+ image: ["5.6-focal", "5.10", "6.2", "latest"]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v7
- name: Version
run: swift --version
- - name: linuxmain
- run: swift test
- name: Build
run: swift build
- name: Test
- run: swift test
\ No newline at end of file
+ run: swift test
diff --git a/Package.swift b/Package.swift
index 6c1603a..e60dc54 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:4.0
+// swift-tools-version:5.6
import PackageDescription
let package = Package(
diff --git a/README.md b/README.md
index 7a893c0..49963d0 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# SwiftRoaring
-
+
-[](https://github.com/Carthage/Carthage) /badge.svg)
+[](https://github.com/Carthage/Carthage) 
@@ -29,7 +29,7 @@ Roaring bitmaps are found to work well in many important applications:
### Dependencies
-Swift 4.0 or higher
+Swift 5.6 or higher
### Usage using Swift Package Manager
diff --git a/Sources/SwiftRoaring/Roaring64Bitmap.swift b/Sources/SwiftRoaring/Roaring64Bitmap.swift
index f486343..0b2433e 100644
--- a/Sources/SwiftRoaring/Roaring64Bitmap.swift
+++ b/Sources/SwiftRoaring/Roaring64Bitmap.swift
@@ -25,7 +25,7 @@ public final class Roaring64Bitmap: Sequence, Equatable, CustomStringConvertible
Hashable, ExpressibleByArrayLiteral, SetAlgebra, Codable {
@usableFromInline
- var ptr: OpaquePointer
+ let ptr: OpaquePointer
///
/// When this bitmap is a *frozen view* created by `frozenView(bytes:)`, this
@@ -33,7 +33,7 @@ public final class Roaring64Bitmap: Sequence, Equatable, CustomStringConvertible
/// bitmap itself, by `deinit`. It is `nil` for every ordinary bitmap.
///
@usableFromInline
- var frozenBacking: UnsafeMutableRawPointer?
+ let frozenBacking: UnsafeMutableRawPointer?
public typealias Element = UInt64
@@ -44,8 +44,8 @@ public final class Roaring64Bitmap: Sequence, Equatable, CustomStringConvertible
///
/// Creates a new bitmap (initially empty)
///
- required public init() {
- self.ptr = croaring.roaring64_bitmap_create()!
+ required public convenience init() {
+ self.init(ptr: croaring.roaring64_bitmap_create()!)
}
///
@@ -53,15 +53,21 @@ public final class Roaring64Bitmap: Sequence, Equatable, CustomStringConvertible
///
required public init(ptr: OpaquePointer) {
self.ptr = ptr
+ self.frozenBacking = nil
+ }
+
+ private init(ptr: OpaquePointer, frozenBacking: UnsafeMutableRawPointer) {
+ self.ptr = ptr
+ self.frozenBacking = frozenBacking
}
///
/// Add all the values between min (included) and max (excluded) that are at a
/// distance k*step from min.
///
- public init(min: UInt64, max: UInt64, step: UInt64) {
- self.ptr = croaring.roaring64_bitmap_from_range(min, max, step)
- ?? croaring.roaring64_bitmap_create()!
+ public convenience init(min: UInt64, max: UInt64, step: UInt64) {
+ self.init(ptr: croaring.roaring64_bitmap_from_range(min, max, step)
+ ?? croaring.roaring64_bitmap_create()!)
}
public convenience init(range: Range, step: UInt64) {
@@ -71,8 +77,8 @@ public final class Roaring64Bitmap: Sequence, Equatable, CustomStringConvertible
///
/// Creates a new bitmap from an array of `uint64_t` integers
///
- public init(values: [UInt64]) {
- self.ptr = croaring.roaring64_bitmap_of_ptr(values.count, values)!
+ public convenience init(values: [UInt64]) {
+ self.init(ptr: croaring.roaring64_bitmap_of_ptr(values.count, values)!)
}
///
@@ -80,8 +86,8 @@ public final class Roaring64Bitmap: Sequence, Equatable, CustomStringConvertible
/// its containers over. This is cheaper than copying them, but it leaves
/// `bitmap` empty.
///
- public init(moving bitmap: RoaringBitmap) {
- self.ptr = croaring.roaring64_bitmap_move_from_roaring32(bitmap.ptr)!
+ public convenience init(moving bitmap: RoaringBitmap) {
+ self.init(ptr: croaring.roaring64_bitmap_move_from_roaring32(bitmap.ptr)!)
}
///
@@ -92,8 +98,8 @@ public final class Roaring64Bitmap: Sequence, Equatable, CustomStringConvertible
self.init(moving: bitmap.copy())
}
- public required init(arrayLiteral: Element...) {
- self.ptr = croaring.roaring64_bitmap_of_ptr(arrayLiteral.count, arrayLiteral)!
+ public required convenience init(arrayLiteral: Element...) {
+ self.init(ptr: croaring.roaring64_bitmap_of_ptr(arrayLiteral.count, arrayLiteral)!)
}
deinit {
@@ -1338,15 +1344,13 @@ extension Roaring64Bitmap {
buffer.deallocate()
return nil
}
- let bitmap = Self(ptr: ptr)
- bitmap.frozenBacking = buffer
- return bitmap
+ return Self(ptr: ptr, frozenBacking: buffer)
}
///
/// Encodable conformance, using the portable format.
///
- public func encode(to encoder: Encoder) throws {
+ public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(Data(self.portableSerialize()).base64EncodedString())
}
@@ -1354,7 +1358,7 @@ extension Roaring64Bitmap {
///
/// Decodable conformance, using the portable format.
///
- public convenience init(from decoder: Decoder) throws {
+ public convenience init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
let encoded = try container.decode(String.self)
guard let data = Data(base64Encoded: encoded) else {
@@ -1376,6 +1380,13 @@ extension Roaring64Bitmap {
)
}
self.init(ptr: ptr)
+ // A bitmap read from untrusted bytes is only safe to use once validated.
+ if let reason = internalValidate() {
+ throw DecodingError.dataCorruptedError(
+ in: container,
+ debugDescription: "not a valid serialized Roaring64Bitmap: \(reason)"
+ )
+ }
}
}
diff --git a/Sources/SwiftRoaring/RoaringBitmap.swift b/Sources/SwiftRoaring/RoaringBitmap.swift
index 3590104..0305d96 100644
--- a/Sources/SwiftRoaring/RoaringBitmap.swift
+++ b/Sources/SwiftRoaring/RoaringBitmap.swift
@@ -23,7 +23,7 @@ public final class RoaringBitmap: Sequence, Equatable, CustomStringConvertible,
Hashable, ExpressibleByArrayLiteral, SetAlgebra, Codable {
@usableFromInline
- var ptr: UnsafeMutablePointer
+ let ptr: UnsafeMutablePointer
///
/// When this bitmap is a *frozen view* created by `frozenView(bytes:)`, this
@@ -31,7 +31,7 @@ public final class RoaringBitmap: Sequence, Equatable, CustomStringConvertible,
/// bitmap itself, by `deinit`. It is `nil` for every ordinary bitmap.
///
@usableFromInline
- var frozenBacking: UnsafeMutableRawPointer?
+ let frozenBacking: UnsafeMutableRawPointer?
public typealias Element = UInt32
@@ -41,8 +41,8 @@ public final class RoaringBitmap: Sequence, Equatable, CustomStringConvertible,
///
/// Creates a new bitmap (initially empty)
///
- required public init() {
- self.ptr = croaring.roaring_bitmap_create()!
+ required public convenience init() {
+ self.init(ptr: croaring.roaring_bitmap_create()!)
}
///
@@ -50,41 +50,47 @@ public final class RoaringBitmap: Sequence, Equatable, CustomStringConvertible,
///
required public init(ptr: UnsafeMutablePointer) {
self.ptr = ptr
+ self.frozenBacking = nil
+ }
+
+ private init(ptr: UnsafeMutablePointer, frozenBacking: UnsafeMutableRawPointer) {
+ self.ptr = ptr
+ self.frozenBacking = frozenBacking
}
///
/// Add all the values between min (included) and max (excluded) that are at a
/// distance k*step from min.
///
- public init(min: UInt64, max: UInt64, step: UInt32) {
- self.ptr = croaring.roaring_bitmap_from_range(min, max, step)!
+ public convenience init(min: UInt64, max: UInt64, step: UInt32) {
+ self.init(ptr: croaring.roaring_bitmap_from_range(min, max, step)!)
}
- public init(range: Range, step: UInt32) {
- self.ptr = croaring.roaring_bitmap_from_range(
+ public convenience init(range: Range, step: UInt32) {
+ self.init(ptr: croaring.roaring_bitmap_from_range(
range.lowerBound,
range.upperBound,
step
- )!
+ )!)
}
///
/// Creates a new bitmap (initially empty) with a provided
/// container-storage capacity (it is a performance hint).
///
- public init(capacity: UInt32) {
- self.ptr = croaring.roaring_bitmap_create_with_capacity(capacity)!
+ public convenience init(capacity: UInt32) {
+ self.init(ptr: croaring.roaring_bitmap_create_with_capacity(capacity)!)
}
///
/// Creates a new bitmap from a pointer of `uint32_t` integers
///
- public init(values: [UInt32]) {
- self.ptr = croaring.roaring_bitmap_of_ptr(values.count, values)!
+ public convenience init(values: [UInt32]) {
+ self.init(ptr: croaring.roaring_bitmap_of_ptr(values.count, values)!)
}
- public required init(arrayLiteral: Element...) {
- self.ptr = croaring.roaring_bitmap_create()!
+ public required convenience init(arrayLiteral: Element...) {
+ self.init(ptr: croaring.roaring_bitmap_create()!)
for i in arrayLiteral { add(i) }
}
@@ -782,14 +788,14 @@ public final class RoaringBitmap: Sequence, Equatable, CustomStringConvertible,
///
/// Encodable conformance
///
- public func encode(to encoder: Encoder) throws {
+ public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
let count = self.sizeInBytes()
var data = Data(count: count)
- data.withUnsafeMutableBytes {
- let ptr = $0.baseAddress!.assumingMemoryBound(to: Int8.self)
- croaring.roaring_bitmap_serialize(self.ptr, ptr)
+ data.withUnsafeMutableBytes { (buffer: UnsafeMutableRawBufferPointer) in
+ let ptr = buffer.baseAddress!.assumingMemoryBound(to: Int8.self)
+ _ = croaring.roaring_bitmap_serialize(self.ptr, ptr)
}
try container.encode(data.base64EncodedString())
}
@@ -797,13 +803,31 @@ public final class RoaringBitmap: Sequence, Equatable, CustomStringConvertible,
///
/// Decodable conformance
///
- public init(from decoder: Decoder) throws {
+ public convenience init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
-
- let buffer = try container.decode(String.self)
- let data = Data(base64Encoded: buffer)
- self.ptr = data!.withUnsafeBytes {
- croaring.roaring_bitmap_deserialize($0)!
+ let encoded = try container.decode(String.self)
+ guard let data = Data(base64Encoded: encoded) else {
+ throw DecodingError.dataCorruptedError(
+ in: container,
+ debugDescription: "not valid base64"
+ )
+ }
+ let ptr = data.withUnsafeBytes {
+ croaring.roaring_bitmap_deserialize_safe($0.baseAddress, data.count)
+ }
+ guard let ptr = ptr else {
+ throw DecodingError.dataCorruptedError(
+ in: container,
+ debugDescription: "not a valid serialized RoaringBitmap"
+ )
+ }
+ self.init(ptr: ptr)
+ // A bitmap read from untrusted bytes is only safe to use once validated.
+ if let reason = internalValidate() {
+ throw DecodingError.dataCorruptedError(
+ in: container,
+ debugDescription: "not a valid serialized RoaringBitmap: \(reason)"
+ )
}
}
@@ -1653,9 +1677,7 @@ extension RoaringBitmap {
buffer.deallocate()
return nil
}
- let bitmap = Self(ptr: ptr)
- bitmap.frozenBacking = buffer
- return bitmap
+ return Self(ptr: ptr, frozenBacking: buffer)
}
}
diff --git a/Tests/swiftRoaringTests/roaring64Tests.swift b/Tests/swiftRoaringTests/roaring64Tests.swift
index 2169ad3..a39812b 100644
--- a/Tests/swiftRoaringTests/roaring64Tests.swift
+++ b/Tests/swiftRoaringTests/roaring64Tests.swift
@@ -453,6 +453,17 @@ class roaring64Tests: XCTestCase {
let garbage = try JSONEncoder().encode("not a bitmap")
XCTAssertThrowsError(try JSONDecoder().decode(Roaring64Bitmap.self, from: garbage))
+
+ // A well-formed header over an array container whose last two values
+ // are out of order: it deserializes, but it is not a valid bitmap.
+ let small = Roaring64Bitmap()
+ small.addMany(values: Array(stride(from: 0, to: 100, by: 2)))
+ let json = try JSONDecoder().decode(String.self, from: try JSONEncoder().encode(small))
+ var bytes = [UInt8](try XCTUnwrap(Data(base64Encoded: json)))
+ bytes.swapAt(bytes.count - 4, bytes.count - 2)
+ bytes.swapAt(bytes.count - 3, bytes.count - 1)
+ let unsorted = try JSONEncoder().encode(Data(bytes).base64EncodedString())
+ XCTAssertThrowsError(try JSONDecoder().decode(Roaring64Bitmap.self, from: unsorted))
}
func testFromRoaringBitmap() {
diff --git a/Tests/swiftRoaringTests/swiftRoaringTests.swift b/Tests/swiftRoaringTests/swiftRoaringTests.swift
index b38cb14..71513a7 100644
--- a/Tests/swiftRoaringTests/swiftRoaringTests.swift
+++ b/Tests/swiftRoaringTests/swiftRoaringTests.swift
@@ -338,6 +338,17 @@ class swiftRoaringTests: XCTestCase {
XCTAssert(s == expected)
XCTAssert(r == b)
+
+ XCTAssertThrowsError(try dec.decode(RoaringBitmap.self, from: Data("\"AAAA\"".utf8)))
+ XCTAssertThrowsError(try dec.decode(RoaringBitmap.self, from: Data("\"not base64\"".utf8)))
+
+ // A well-formed header over an array container whose last two values
+ // are out of order: it deserializes, but it is not a valid bitmap.
+ var bytes = [UInt8](Data(base64Encoded: try! dec.decode(String.self, from: a))!)
+ bytes.swapAt(bytes.count - 4, bytes.count - 2)
+ bytes.swapAt(bytes.count - 3, bytes.count - 1)
+ let unsorted = try! enc.encode(Data(bytes).base64EncodedString())
+ XCTAssertThrowsError(try dec.decode(RoaringBitmap.self, from: unsorted))
}
func makeSets() -> (RoaringBitmap, RoaringBitmap, Set, Set) {