Skip to content

Commit a5ba117

Browse files
committed
Harden Swift binding validation
1 parent 5203422 commit a5ba117

4 files changed

Lines changed: 104 additions & 26 deletions

File tree

swift_package/Sources/BrainFlow/BoardShim.swift

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public final class BoardShim {
2828
}
2929

3030
public func start_stream(buffer_size: Int = 450_000, streamer_params: String = "") throws {
31+
guard buffer_size > 0 else { throw invalidArguments("buffer_size must be positive") }
3132
try serialized_params.withCString { params in
3233
try streamer_params.withCString { streamer in
3334
try BoardShimNative.withBoard { native in
@@ -86,34 +87,40 @@ public final class BoardShim {
8687
try serialized_params.withCString { params in
8788
try config.withCString { configPtr in
8889
var response = [CChar](repeating: 0, count: 16_000)
90+
let responseCapacity = response.count
8991
var responseLen: CInt = 0
90-
try BoardShimNative.withBoard { native in
91-
try checkBrainFlowExitCode(
92-
native.config_board(configPtr, &response, &responseLen, CInt(response.count), CInt(board_id), params),
93-
"Error in config_board"
94-
)
92+
try response.withUnsafeMutableBufferPointer { responsePtr in
93+
try BoardShimNative.withBoard { native in
94+
try checkBrainFlowExitCode(
95+
native.config_board(configPtr, responsePtr.baseAddress, &responseLen, CInt(responseCapacity), CInt(board_id), params),
96+
"Error in config_board"
97+
)
98+
}
9599
}
96-
return String(cString: response)
100+
let returnedCount = min(max(Int(responseLen), 0), responseCapacity)
101+
return String(bytes: response.prefix(returnedCount).map { UInt8(bitPattern: $0) }, encoding: .utf8) ?? ""
97102
}
98103
}
99104
}
100105

101106
public func config_board_with_bytes(_ bytes: [UInt8]) throws {
102-
guard !bytes.isEmpty else { return }
107+
guard !bytes.isEmpty else { throw invalidArguments("bytes must be non-empty") }
103108
try serialized_params.withCString { params in
104109
try bytes.withUnsafeBufferPointer { buffer in
105-
let raw = UnsafeRawPointer(buffer.baseAddress!).assumingMemoryBound(to: CChar.self)
106-
try BoardShimNative.withBoard { native in
107-
try checkBrainFlowExitCode(
108-
native.config_board_with_bytes(raw, CInt(bytes.count), CInt(board_id), params),
109-
"Error in config_board_with_bytes"
110-
)
110+
try buffer.baseAddress!.withMemoryRebound(to: CChar.self, capacity: buffer.count) { bytesPtr in
111+
try BoardShimNative.withBoard { native in
112+
try checkBrainFlowExitCode(
113+
native.config_board_with_bytes(bytesPtr, CInt(buffer.count), CInt(board_id), params),
114+
"Error in config_board_with_bytes"
115+
)
116+
}
111117
}
112118
}
113119
}
114120
}
115121

116122
public func get_current_board_data(num_samples: Int, preset: BrainFlowPresets = .DEFAULT_PRESET) throws -> [[Double]] {
123+
guard num_samples > 0 else { throw invalidArguments("num_samples must be positive") }
117124
let rows = try Self.get_num_rows(board_id: board_id, preset: preset)
118125
var data = [Double](repeating: 0.0, count: rows * num_samples)
119126
var returnedSamples: CInt = 0

swift_package/Sources/BrainFlow/BrainFlowNative.swift

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,7 @@ final class NativeLibrary {
3939
}
4040

4141
private static var openFlags: Int32 {
42-
#if os(Linux)
4342
return RTLD_NOW | RTLD_GLOBAL
44-
#else
45-
return RTLD_NOW | RTLD_GLOBAL
46-
#endif
4743
}
4844

4945
private static func candidatePaths(for names: [String]) -> [String] {
@@ -128,7 +124,7 @@ enum NativeLibraries {
128124
final class LazyNativeLibrary {
129125
private let names: [String]
130126
private let lock = NSLock()
131-
private var storage: Result<NativeLibrary, Error>?
127+
private var storage: NativeLibrary?
132128

133129
init(names: [String]) {
134130
self.names = names
@@ -138,10 +134,10 @@ final class LazyNativeLibrary {
138134
lock.lock()
139135
defer { lock.unlock() }
140136
if let storage {
141-
return try storage.get()
137+
return storage
142138
}
143-
let result = Result { try NativeLibrary(names: names) }
144-
storage = result
145-
return try result.get()
139+
let library = try NativeLibrary(names: names)
140+
storage = library
141+
return library
146142
}
147143
}

swift_package/Sources/BrainFlow/DataFilter.swift

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -339,10 +339,16 @@ public enum DataFilter {
339339
}
340340

341341
public static func get_csp(data: [[[Double]]], labels: [Double]) throws -> CSPResult {
342-
guard let firstEpoch = data.first, let firstChannel = firstEpoch.first else { throw invalidArguments("Invalid CSP data") }
342+
guard let firstEpoch = data.first, let firstChannel = firstEpoch.first, !firstChannel.isEmpty else { throw invalidArguments("Invalid CSP data") }
343343
let nEpochs = data.count
344344
let nChannels = firstEpoch.count
345345
let nTimes = firstChannel.count
346+
guard labels.count == nEpochs else { throw invalidArguments("labels count must match epoch count") }
347+
guard data.allSatisfy({ epoch in
348+
epoch.count == nChannels && epoch.allSatisfy { $0.count == nTimes }
349+
}) else {
350+
throw invalidArguments("CSP data must be rectangular")
351+
}
346352
var flattened = [Double]()
347353
flattened.reserveCapacity(nEpochs * nChannels * nTimes)
348354
for epoch in data {
@@ -368,6 +374,7 @@ public enum DataFilter {
368374
}
369375

370376
public static func get_window(window_function: Int, window_len: Int) throws -> [Double] {
377+
guard window_len > 0 else { throw invalidArguments("window_len must be positive") }
371378
var output = [Double](repeating: 0.0, count: window_len)
372379
try output.withUnsafeMutableBufferPointer { pointer in
373380
try DataFilterNative.withData { native in
@@ -413,6 +420,7 @@ public enum DataFilter {
413420
}
414421

415422
public static func perform_ifft(data: [Complex]) throws -> [Double] {
423+
guard data.count >= 2 else { throw invalidArguments("FFT data must contain at least two bins") }
416424
var real = data.map(\.real)
417425
var imag = data.map(\.imag)
418426
let restoredLength = (data.count - 1) * 2
@@ -461,7 +469,10 @@ public enum DataFilter {
461469
}
462470

463471
public static func get_psd_welch(data: [Double], nfft: Int, overlap: Int, sampling_rate: Int, window: Int) throws -> PSD {
464-
guard nfft % 2 == 0 else { throw invalidArguments("nfft must be even") }
472+
guard nfft > 0, nfft & (nfft - 1) == 0 else { throw invalidArguments("nfft must be a positive power of two") }
473+
guard data.count >= nfft else { throw invalidArguments("nfft must be less than or equal to data count") }
474+
guard overlap >= 0, overlap < nfft else { throw invalidArguments("overlap must be non-negative and less than nfft") }
475+
guard sampling_rate > 0 else { throw invalidArguments("sampling_rate must be positive") }
465476
var input = data
466477
var ampl = [Double](repeating: 0.0, count: nfft / 2 + 1)
467478
var freq = [Double](repeating: 0.0, count: nfft / 2 + 1)
@@ -482,6 +493,8 @@ public enum DataFilter {
482493
}
483494

484495
public static func get_band_power(psd: PSD, freq_start: Double, freq_end: Double) throws -> Double {
496+
guard !psd.ampl.isEmpty, psd.ampl.count == psd.freq.count else { throw invalidArguments("PSD arrays must be non-empty and have equal lengths") }
497+
guard freq_start < freq_end else { throw invalidArguments("freq_start must be less than freq_end") }
485498
var ampl = psd.ampl
486499
var freq = psd.freq
487500
var output = 0.0
@@ -513,8 +526,11 @@ public enum DataFilter {
513526
sampling_rate: Int,
514527
apply_filter: Bool
515528
) throws -> BandPowerResult {
529+
let (rows, cols) = try BrainFlowArray.validateRectangular(data)
516530
guard !channels.isEmpty, !bands.isEmpty else { throw invalidArguments("Channels and bands must be non-empty") }
517-
let cols = data[channels[0]].count
531+
guard channels.allSatisfy({ $0 >= 0 && $0 < rows }) else { throw invalidArguments("Channel index is out of range") }
532+
guard bands.allSatisfy({ $0.start < $0.stop }) else { throw invalidArguments("Band start frequency must be less than stop frequency") }
533+
guard sampling_rate > 0 else { throw invalidArguments("sampling_rate must be positive") }
518534
var selected = [Double]()
519535
selected.reserveCapacity(channels.count * cols)
520536
for channel in channels {
@@ -543,7 +559,11 @@ public enum DataFilter {
543559
public static func perform_ica(data: [[Double]], num_components: Int, channels: [Int]? = nil) throws -> ICAResult {
544560
let (rows, cols) = try BrainFlowArray.validateRectangular(data)
545561
let selectedChannels = channels ?? Array(0..<rows)
546-
guard num_components >= 1 else { throw invalidArguments("num_components must be positive") }
562+
guard !selectedChannels.isEmpty else { throw invalidArguments("channels must be non-empty") }
563+
guard selectedChannels.allSatisfy({ $0 >= 0 && $0 < rows }) else { throw invalidArguments("Channel index is out of range") }
564+
guard cols >= 2 else { throw invalidArguments("ICA data must contain at least two samples") }
565+
guard selectedChannels.count >= 2 else { throw invalidArguments("ICA requires at least two channels") }
566+
guard num_components >= 2, num_components <= selectedChannels.count else { throw invalidArguments("num_components must be between 2 and the selected channel count") }
547567
var selected = [Double]()
548568
selected.reserveCapacity(selectedChannels.count * cols)
549569
for channel in selectedChannels {
@@ -578,6 +598,7 @@ public enum DataFilter {
578598
var input = data
579599
let start = start_pos ?? 0
580600
let end = end_pos ?? data.count
601+
guard start >= 0, end <= data.count, start < end else { throw invalidArguments("Invalid position arguments") }
581602
var output = 0.0
582603
try input.withUnsafeMutableBufferPointer { pointer in
583604
try DataFilterNative.withData { native in
@@ -588,6 +609,7 @@ public enum DataFilter {
588609
}
589610

590611
public static func get_railed_percentage(data: [Double], gain: Int) throws -> Double {
612+
guard !data.isEmpty else { throw invalidArguments("data must be non-empty") }
591613
var input = data
592614
var output = 0.0
593615
try input.withUnsafeMutableBufferPointer { pointer in
@@ -599,6 +621,8 @@ public enum DataFilter {
599621
}
600622

601623
public static func get_oxygen_level(ppg_ir: [Double], ppg_red: [Double], sampling_rate: Int, coef1: Double = 1.5958422, coef2: Double = -34.6596622, coef3: Double = 112.6898759) throws -> Double {
624+
guard !ppg_ir.isEmpty, ppg_ir.count == ppg_red.count else { throw invalidArguments("PPG arrays must be non-empty and have equal lengths") }
625+
guard sampling_rate > 0 else { throw invalidArguments("sampling_rate must be positive") }
602626
var ir = ppg_ir
603627
var red = ppg_red
604628
var output = 0.0
@@ -613,6 +637,9 @@ public enum DataFilter {
613637
}
614638

615639
public static func get_heart_rate(ppg_ir: [Double], ppg_red: [Double], sampling_rate: Int, fft_size: Int) throws -> Double {
640+
guard !ppg_ir.isEmpty, ppg_ir.count == ppg_red.count else { throw invalidArguments("PPG arrays must be non-empty and have equal lengths") }
641+
guard sampling_rate > 0 else { throw invalidArguments("sampling_rate must be positive") }
642+
guard fft_size >= 1024, fft_size % 2 == 0 else { throw invalidArguments("fft_size must be even and at least 1024") }
616643
var ir = ppg_ir
617644
var red = ppg_red
618645
var output = 0.0
@@ -655,6 +682,7 @@ public enum DataFilter {
655682
try checkBrainFlowExitCode(native.get_num_elements_in_file(fileNamePtr, &elements), "Failed to determine number of file elements")
656683
}
657684
}
685+
guard elements >= 0 else { throw invalidArguments("File element count must be non-negative") }
658686
var data = [Double](repeating: 0.0, count: Int(elements))
659687
var rows: CInt = 0
660688
var cols: CInt = 0

swift_package/Tests/BrainFlowTests/BrainFlowTests.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ final class BrainFlowTests: XCTestCase {
1010
}
1111
}
1212

13+
private func assertInvalidArguments<T>(
14+
_ expression: @autoclosure () throws -> T,
15+
file: StaticString = #filePath,
16+
line: UInt = #line
17+
) {
18+
XCTAssertThrowsError(try expression(), file: file, line: line) { error in
19+
guard let brainFlowError = error as? BrainFlowError else {
20+
return XCTFail("Expected BrainFlowError, got \(error)", file: file, line: line)
21+
}
22+
XCTAssertEqual(
23+
brainFlowError.exit_code,
24+
BrainFlowExitCodes.INVALID_ARGUMENTS_ERROR.rawValue,
25+
file: file,
26+
line: line
27+
)
28+
}
29+
}
30+
1331
func testInputParamsJSON() throws {
1432
var params = BrainFlowInputParams()
1533
params.serial_port = "/dev/ttyUSB0"
@@ -21,6 +39,35 @@ final class BrainFlowTests: XCTestCase {
2139
XCTAssertTrue(json.contains("master_board"))
2240
}
2341

42+
func testBoardShimRejectsInvalidArgumentsBeforeNativeCalls() throws {
43+
let board = try BoardShim(board_id: .SYNTHETIC_BOARD)
44+
45+
assertInvalidArguments(try board.start_stream(buffer_size: 0))
46+
assertInvalidArguments(try board.config_board_with_bytes([]))
47+
assertInvalidArguments(try board.get_current_board_data(num_samples: 0))
48+
}
49+
50+
func testDataFilterRejectsInvalidArgumentsBeforeNativeCalls() throws {
51+
assertInvalidArguments(try DataFilter.get_csp(data: [[[1.0, 2.0]], [[3.0]]], labels: [0.0, 1.0]))
52+
assertInvalidArguments(try DataFilter.get_csp(data: [[[1.0, 2.0]]], labels: []))
53+
assertInvalidArguments(try DataFilter.get_window(window_function: WindowOperations.HANNING.rawValue, window_len: 0))
54+
assertInvalidArguments(try DataFilter.perform_ifft(data: [Complex(real: 1.0, imag: 0.0)]))
55+
assertInvalidArguments(try DataFilter.get_psd_welch(data: [1.0, 2.0, 3.0], nfft: 4, overlap: 0, sampling_rate: 250, window: WindowOperations.NO_WINDOW.rawValue))
56+
assertInvalidArguments(try DataFilter.get_band_power(psd: PSD(ampl: [1.0], freq: []), freq_start: 1.0, freq_end: 2.0))
57+
assertInvalidArguments(try DataFilter.get_custom_band_powers(
58+
data: [[1.0, 2.0]],
59+
bands: [FrequencyBand(start: 1.0, stop: 2.0)],
60+
channels: [1],
61+
sampling_rate: 250,
62+
apply_filter: false
63+
))
64+
assertInvalidArguments(try DataFilter.perform_ica(data: [[1.0, 2.0], [3.0, 4.0]], num_components: 3))
65+
assertInvalidArguments(try DataFilter.calc_stddev(data: [1.0, 2.0], start_pos: 1, end_pos: 3))
66+
assertInvalidArguments(try DataFilter.get_railed_percentage(data: [], gain: 24))
67+
assertInvalidArguments(try DataFilter.get_oxygen_level(ppg_ir: [1.0], ppg_red: [1.0, 2.0], sampling_rate: 25))
68+
assertInvalidArguments(try DataFilter.get_heart_rate(ppg_ir: [1.0, 2.0], ppg_red: [1.0, 2.0], sampling_rate: 25, fft_size: 1023))
69+
}
70+
2471
func testBrainFlowGetDataSyntheticBoard() throws {
2572
try requireNativeLibraries()
2673
let board = try BoardShim(board_id: .SYNTHETIC_BOARD)

0 commit comments

Comments
 (0)