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
19 changes: 10 additions & 9 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Swift on Ubuntu (latest)
name: Tests

on:
push:
Expand All @@ -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
run: swift test
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:4.0
// swift-tools-version:5.6
import PackageDescription

let package = Package(
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SwiftRoaring
<a href="https://developer.apple.com/swift"><img src="https://img.shields.io/badge/Swift4-compatible-green.svg?style=flat" alt="Swift 4 compatible" /></a>
<a href="https://developer.apple.com/swift"><img src="https://img.shields.io/badge/Swift_5.6%2B-compatible-green.svg?style=flat" alt="Swift 5.6 compatible" /></a>
<a href="https://github.com/apple/swift-package-manager"><img src="https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg"/></a>
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) ![Ubuntu CI](https://github.com/RoaringBitmap/SwiftRoaring/workflows/Swift%20on%20Ubuntu%20(latest)/badge.svg)
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) ![Tests](https://github.com/RoaringBitmap/SwiftRoaring/actions/workflows/tests.yml/badge.svg)



Expand Down Expand Up @@ -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

Expand Down
47 changes: 29 additions & 18 deletions Sources/SwiftRoaring/Roaring64Bitmap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ 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
/// holds the aligned buffer the view reads from. It is deallocated, after the
/// bitmap itself, by `deinit`. It is `nil` for every ordinary bitmap.
///
@usableFromInline
var frozenBacking: UnsafeMutableRawPointer?
let frozenBacking: UnsafeMutableRawPointer?

public typealias Element = UInt64

Expand All @@ -44,24 +44,30 @@ 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()!)
}

///
/// Creates a new bitmap using a given ptr
///
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<UInt64>, step: UInt64) {
Expand All @@ -71,17 +77,17 @@ 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)!)
}

///
/// Creates a new 64-bit bitmap holding the values of a 32-bit one, by moving
/// 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)!)
}

///
Expand All @@ -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 {
Expand Down Expand Up @@ -1338,23 +1344,21 @@ 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())
}

///
/// 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 {
Expand All @@ -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)"
)
}
}
}

Expand Down
78 changes: 50 additions & 28 deletions Sources/SwiftRoaring/RoaringBitmap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ public final class RoaringBitmap: Sequence, Equatable, CustomStringConvertible,
Hashable, ExpressibleByArrayLiteral, SetAlgebra, Codable {

@usableFromInline
var ptr: UnsafeMutablePointer<roaring_bitmap_t>
let ptr: UnsafeMutablePointer<roaring_bitmap_t>

///
/// When this bitmap is a *frozen view* created by `frozenView(bytes:)`, this
/// holds the aligned buffer the view reads from. It is deallocated, after the
/// bitmap itself, by `deinit`. It is `nil` for every ordinary bitmap.
///
@usableFromInline
var frozenBacking: UnsafeMutableRawPointer?
let frozenBacking: UnsafeMutableRawPointer?

public typealias Element = UInt32

Expand All @@ -41,50 +41,56 @@ 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()!)
}

///
/// Creates a new bitmap using a given ptr
///
required public init(ptr: UnsafeMutablePointer<roaring_bitmap_t>) {
self.ptr = ptr
self.frozenBacking = nil
}

private init(ptr: UnsafeMutablePointer<roaring_bitmap_t>, 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<UInt64>, step: UInt32) {
self.ptr = croaring.roaring_bitmap_from_range(
public convenience init(range: Range<UInt64>, 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) }

}
Expand Down Expand Up @@ -782,28 +788,46 @@ 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())
}

///
/// 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)"
)
}
}

Expand Down Expand Up @@ -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)
}
}

Expand Down
11 changes: 11 additions & 0 deletions Tests/swiftRoaringTests/roaring64Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
11 changes: 11 additions & 0 deletions Tests/swiftRoaringTests/swiftRoaringTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<UInt32>, Set<UInt32>) {
Expand Down
Loading