Skip to content

Commit 903e2ea

Browse files
committed
Fix writing to OutputStream
1 parent bddb497 commit 903e2ea

File tree

5 files changed

+152
-140
lines changed

5 files changed

+152
-140
lines changed

Sources/BSONSerialization/BSONClasses.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ private func areBSONEntitiesEqual(_ entity1: Any?, _ entity2: Any?) throws -> Bo
267267
case let dbPointer as MongoDBPointer: guard entity2 as? MongoDBPointer == dbPointer else {return false}
268268

269269
default:
270-
throw BSONSerialization.BSONSerializationError.invalidBSONObject(invalidElement: entity1! /* nil case already processed above */)
270+
throw Err.invalidBSONObject(invalidElement: entity1! /* nil case already processed above */)
271271
}
272272

273273
return true

Sources/BSONSerialization/BSONSerialization.swift

Lines changed: 26 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -46,79 +46,6 @@ final public class BSONSerialization {
4646

4747
}
4848

49-
/** The BSON Serialization errors enum. */
50-
public enum BSONSerializationError : Error {
51-
/** The given data/stream contains too few bytes to be a valid bson doc. */
52-
case dataTooSmall
53-
/** The given data size is not the one declared by the bson doc. */
54-
case dataLengthDoNotMatch
55-
56-
/** The length of the bson doc is invalid. */
57-
case invalidLength
58-
/**
59-
An invalid element was found.
60-
The element is given in argument to this enum case. */
61-
case invalidElementType(UInt8)
62-
63-
/** Found an invalid bool value (given in arg). */
64-
case invalidBooleanValue(UInt8)
65-
66-
/**
67-
Asked to read an invalid string for the required encoding.
68-
The original data that has been tried to be parsed is given in arg of this error. */
69-
case invalidString(Data)
70-
/**
71-
Invalid end of BSON string found.
72-
Expected `NULL` (`0`), but found the bytes given in argument to this enum case (if `nil`, no data can be read after the string). */
73-
case invalidEndOfString(UInt8?)
74-
75-
/**
76-
An invalid key was found in an array:
77-
Keys must be integers, sorted in ascending order from `0` to `n-1` (where `n = number of elements in the array`).
78-
79-
- Note: Not so sure the increments from one element to the next should necessarily be of one…
80-
The doc is pretty vague on the subject.
81-
It says:
82-
```text
83-
[…] with integer values for the keys, starting with 0 and continuing sequentially.
84-
[…] The keys must be in ascending numerical order.
85-
``` */
86-
case invalidArrayKey(currentKey: String, previousKey: String?)
87-
88-
/** Found an invalid regular expression options value (the complete options and the faulty character are given in arg). */
89-
case invalidRegularExpressionOptions(options: String, invalidCharacter: Character)
90-
/** Found an invalid regular expression value (the regular expression and the parsing error are given in arg). */
91-
case invalidRegularExpression(pattern: String, error: Error)
92-
93-
/**
94-
The JS with scope element gives the raw data length in its definition.
95-
If the given length does not match the decoded length, this error is thrown.
96-
The expected and actual length are given in the error. */
97-
case invalidJSWithScopeLength(expected: Int, actual: Int)
98-
99-
/** An error occurred writing the stream. */
100-
case cannotWriteToStream(streamError: Error?)
101-
102-
/**
103-
An invalid BSON object was given to be serialized.
104-
The invalid element is passed in argument to this error. */
105-
case invalidBSONObject(invalidElement: Any)
106-
/**
107-
Tried to serialize an unserializable string (using the C representation).
108-
This is due to the fact that the `\0` char can be used in a valid UTF8 string.
109-
(Note: the characters `\254` and `\255` can NEVER be used in a valid UTF8 string.
110-
Why were they not the separator?)
111-
112-
Usually, the BSON strings represented using the C representation are dictionary keys.
113-
But they can also be the components of a regexp. */
114-
case unserializableCString(String)
115-
116-
/** Cannot allocate memory (either with `malloc` or `UnsafePointer.alloc()`). */
117-
case cannotAllocateMemory(Int)
118-
/** An internal error occurred rendering the serialization impossible. */
119-
case internalError
120-
}
121-
12249
/**
12350
Serialize the given data into a dictionary with String keys, object values.
12451

@@ -128,9 +55,9 @@ final public class BSONSerialization {
12855
- Returns: The serialized BSON data. */
12956
public class func bsonObject(with data: Data, options opt: ReadingOptions = []) throws -> BSONDoc {
13057
/* Let’s check whether the length of the data correspond to the length declared in the data. */
131-
guard data.count >= 5 else {throw BSONSerializationError.dataTooSmall}
58+
guard data.count >= 5 else {throw Err.dataTooSmall}
13259
let length32 = data.withUnsafeBytes{ $0.load(as: Int32.self) }
133-
guard Int(length32) == data.count else {throw BSONSerializationError.dataLengthDoNotMatch}
60+
guard Int(length32) == data.count else {throw Err.dataLengthDoNotMatch}
13461

13562
let bufferedData = DataReader(data: data)
13663
return try bsonObject(with: bufferedData, options: opt)
@@ -167,7 +94,7 @@ final public class BSONSerialization {
16794

16895
streamReader.readSizeLimit = initialReadPosition + MemoryLayout<Int32>.size
16996
let length32: Int32 = try streamReader.readType()
170-
guard length32 >= 5 else {throw BSONSerializationError.dataTooSmall}
97+
guard length32 >= 5 else {throw Err.dataTooSmall}
17198

17299
let length = Int(length32)
173100
streamReader.readSizeLimit = initialReadPosition + length
@@ -176,7 +103,7 @@ final public class BSONSerialization {
176103

177104
var isAtEnd = false
178105
while !isAtEnd {
179-
guard streamReader.currentReadPosition - initialReadPosition <= length else {throw BSONSerializationError.invalidLength}
106+
guard streamReader.currentReadPosition - initialReadPosition <= length else {throw Err.invalidLength}
180107

181108
let currentElementType: UInt8 = try streamReader.readType()
182109
guard currentElementType != BSONElementType.endOfDocument.rawValue else {
@@ -195,7 +122,7 @@ final public class BSONSerialization {
195122
switch valAsInt8 {
196123
case 1: try decodeCallback(key, true); ret[key] = true
197124
case 0: try decodeCallback(key, false); ret[key] = false
198-
default: throw BSONSerializationError.invalidBooleanValue(valAsInt8)
125+
default: throw Err.invalidBooleanValue(valAsInt8)
199126
}
200127

201128
case .int32Bits? where MemoryLayout<Int>.size == MemoryLayout<Int32>.size:
@@ -247,12 +174,12 @@ final public class BSONSerialization {
247174
case "l": (/* Make \w, \W, etc. locale dependent. (Unsupported, or most likely default unremovable behaviour…) */)
248175
case "s": foundationOptions.insert(.dotMatchesLineSeparators) /* Dotall mode (`.` matches everything). Not sure if this option is enough. */
249176
case "u": foundationOptions.insert(.useUnicodeWordBoundaries) /* Make \w, \W, etc. match unicode. */
250-
default: throw BSONSerializationError.invalidRegularExpressionOptions(options: options, invalidCharacter: c)
177+
default: throw Err.invalidRegularExpressionOptions(options: options, invalidCharacter: c)
251178
}
252179
}
253180
let val: NSRegularExpression
254181
do {val = try NSRegularExpression(pattern: pattern, options: foundationOptions)}
255-
catch {throw BSONSerializationError.invalidRegularExpression(pattern: pattern, error: error)}
182+
catch {throw Err.invalidRegularExpression(pattern: pattern, error: error)}
256183

257184
try decodeCallback(key, val)
258185
ret[key] = val
@@ -271,7 +198,7 @@ final public class BSONSerialization {
271198
var val = [Any?]()
272199
var prevKey: String? = nil
273200
_ = try bsonObject(with: streamReader, options: opt, initialReadPosition: streamReader.currentReadPosition, decodeCallback: { subkey, subval in
274-
guard String(val.count) == subkey else {throw BSONSerializationError.invalidArrayKey(currentKey: subkey, previousKey: prevKey)}
201+
guard String(val.count) == subkey else {throw Err.invalidArrayKey(currentKey: subkey, previousKey: prevKey)}
275202
val.append(subval)
276203
prevKey = subkey
277204
})
@@ -310,7 +237,7 @@ final public class BSONSerialization {
310237
let valSize: Int32 = try streamReader.readType()
311238
let jsCode = try streamReader.readBSONString(encoding: .utf8)
312239
let scope = try bsonObject(with: streamReader, options: opt, initialReadPosition: streamReader.currentReadPosition)
313-
guard streamReader.currentReadPosition - valStartPosition == Int(valSize) else {throw BSONSerializationError.invalidJSWithScopeLength(expected: Int(valSize), actual: streamReader.currentReadPosition - valStartPosition)}
240+
guard streamReader.currentReadPosition - valStartPosition == Int(valSize) else {throw Err.invalidJSWithScopeLength(expected: Int(valSize), actual: streamReader.currentReadPosition - valStartPosition)}
314241
let val = JavascriptWithScope(javascript: jsCode, scope: scope)
315242
try decodeCallback(key, val)
316243
ret[key] = val
@@ -341,11 +268,11 @@ final public class BSONSerialization {
341268
try decodeCallback(key, val)
342269
ret[key] = val
343270

344-
case nil: throw BSONSerializationError.invalidElementType(currentElementType)
271+
case nil: throw Err.invalidElementType(currentElementType)
345272
case .endOfDocument?: fatalError() /* Guarded before the switch */
346273
}
347274
}
348-
guard streamReader.currentReadPosition - initialReadPosition == length else {throw BSONSerializationError.invalidLength}
275+
guard streamReader.currentReadPosition - initialReadPosition == length else {throw Err.invalidLength}
349276
return ret
350277
}
351278

@@ -362,7 +289,7 @@ final public class BSONSerialization {
362289
})
363290

364291
guard let nsdata = stream.property(forKey: Stream.PropertyKey.dataWrittenToMemoryStreamKey) as? NSData else {
365-
throw BSONSerializationError.internalError
292+
throw Err.internalError
366293
}
367294

368295
var data = Data(referencing: nsdata)
@@ -706,7 +633,7 @@ final public class BSONSerialization {
706633
size += 12
707634

708635
case let unknown?:
709-
throw BSONSerializationError.invalidBSONObject(invalidElement: unknown)
636+
throw Err.invalidBSONObject(invalidElement: unknown)
710637
}
711638

712639
return (size, subSizes)
@@ -829,20 +756,7 @@ final public class BSONSerialization {
829756

830757
var type = bin.binaryTypeAsInt
831758
size += try write(value: &type, toStream: stream)
832-
833-
/* Writing a 0-length data seems to make next writes to the stream fail. */
834-
if bin.data.count > 0 {
835-
/* Note: Joe says we can bind the memory (or even assume it’s already bound) to UInt8
836-
* because the memory will be immutable in the closure, and thus cannot be aliased.
837-
* https://twitter.com/jckarter/status/1142446184700624896 */
838-
let written = bin.data.withUnsafeBytes{ (bytes: UnsafeRawBufferPointer) -> Int in
839-
let boundBytes = bytes.bindMemory(to: UInt8.self)
840-
assert(bin.data.count == boundBytes.count, "INTERNAL ERROR")
841-
return stream.write(boundBytes.baseAddress!, maxLength: boundBytes.count)
842-
}
843-
guard written == bin.data.count else {throw BSONSerializationError.cannotWriteToStream(streamError: stream.streamError)}
844-
size += written
845-
}
759+
size += try bin.data.withUnsafeBytes{ try stream.write(dataPtr: $0) }
846760

847761
case var val as MongoObjectId:
848762
size += try write(elementType: .objectId, toStream: stream)
@@ -886,7 +800,7 @@ final public class BSONSerialization {
886800
size += try write(value: &bytes, toStream: stream)
887801

888802
case let unknown?:
889-
throw BSONSerializationError.invalidBSONObject(invalidElement: unknown)
803+
throw Err.invalidBSONObject(invalidElement: unknown)
890804
}
891805

892806
return size
@@ -955,21 +869,10 @@ final public class BSONSerialization {
955869
private class func write(CEncodedString str: String, toStream stream: OutputStream) throws -> Int {
956870
var written = 0
957871

958-
/* Apparently writing 0 bytes to the stream will f**ck it up… */
959-
if str.count > 0 {
960-
/* Let’s get the UTF8 bytes of the string. */
961-
let bytes = [UInt8](str.utf8)
962-
assert(bytes.count > 0, "How on earth a non-empty string has 0 UTF-8 bytes?")
963-
guard !bytes.contains(0) else {throw BSONSerializationError.unserializableCString(str)}
964-
965-
let curWrite = bytes.withUnsafeBufferPointer{ p -> Int in
966-
assert(p.count == bytes.count)
967-
return stream.write(p.baseAddress!, maxLength: p.count)
968-
}
969-
970-
guard curWrite == bytes.count else {throw BSONSerializationError.cannotWriteToStream(streamError: stream.streamError)}
971-
written += curWrite
972-
}
872+
/* Let’s get the UTF8 bytes of the string. */
873+
let bytes = [UInt8](str.utf8)
874+
guard !bytes.contains(0) else {throw Err.unserializableCString(str)}
875+
written += try bytes.withUnsafeBufferPointer{ try stream.write(dataPtr: UnsafeRawBufferPointer($0)) }
973876

974877
var zero: Int8 = 0
975878
written += try write(value: &zero, toStream: stream)
@@ -984,19 +887,9 @@ final public class BSONSerialization {
984887
/* Let’s write the size of the string to the stream. */
985888
written += try write(value: &strLength, toStream: stream)
986889

987-
/* Apparently writing 0 bytes to the stream will f**ck it up… */
988-
if str.count > 0 {
989-
/* Let’s get the UTF8 bytes of the string. */
990-
let bytes = [UInt8](str.utf8)
991-
assert(bytes.count > 0, "How on earth a non-empty string has 0 UTF-8 bytes?")
992-
let curWrite = bytes.withUnsafeBufferPointer{ p -> Int in
993-
assert(p.count == bytes.count)
994-
return stream.write(p.baseAddress!, maxLength: p.count)
995-
}
996-
997-
guard curWrite == bytes.count else {throw BSONSerializationError.cannotWriteToStream(streamError: stream.streamError)}
998-
written += curWrite
999-
}
890+
/* Let’s get the UTF8 bytes of the string. */
891+
let bytes = [UInt8](str.utf8)
892+
written += try bytes.withUnsafeBufferPointer{ try stream.write(dataPtr: UnsafeRawBufferPointer($0)) }
1000893

1001894
var zero: Int8 = 0
1002895
written += try write(value: &zero, toStream: stream)
@@ -1012,11 +905,7 @@ final public class BSONSerialization {
1012905
/* We cannot use withMemoryRebound because the doc says this method can only be used if the new type have the same size and stride as the original pointer’s type.
1013906
* So instead we have to convert the pointer to a raw pointer and bind the raw pointer’s memory to UInt8. */
1014907
let rawPointer = UnsafeRawPointer(pointer)
1015-
let uint8Pointer = rawPointer.bindMemory(to: UInt8.self, capacity: size)
1016-
guard stream.write(uint8Pointer, maxLength: size) == size else {
1017-
throw BSONSerializationError.cannotWriteToStream(streamError: stream.streamError)
1018-
}
1019-
return size
908+
return try stream.write(dataPtr: UnsafeRawBufferPointer(start: rawPointer, count: size))
1020909
})
1021910
}
1022911

@@ -1054,7 +943,7 @@ private extension StreamReader {
1054943

1055944
/* This String init fails if the data is invalid for the given encoding. */
1056945
guard let str = String(data: data, encoding: encoding) else {
1057-
throw BSONSerialization.BSONSerializationError.invalidString(data)
946+
throw Err.invalidString(data)
1058947
}
1059948

1060949
return str
@@ -1072,13 +961,13 @@ private extension StreamReader {
1072961
let strData = try readData(size: Int(stringSize-1))
1073962
assert(strData.count == Int(stringSize-1))
1074963
guard let str = String(data: strData, encoding: encoding) else {
1075-
throw BSONSerialization.BSONSerializationError.invalidString(strData)
964+
throw Err.invalidString(strData)
1076965
}
1077966

1078967
/* Reading the last byte and checking it is indeed 0. */
1079968
try readData(size: 1, { null in
1080969
assert(null.count == 1)
1081-
guard null.first == 0 else {throw BSONSerialization.BSONSerializationError.invalidEndOfString(null.first)}
970+
guard null.first == 0 else {throw Err.invalidEndOfString(null.first)}
1082971
})
1083972

1084973
return str
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Errors.swift
3+
* BSONSerialization
4+
*
5+
* Created by François Lamboley on 2022/02/09.
6+
* Copyright © 2022 frizlab. All rights reserved.
7+
*/
8+
9+
import Foundation
10+
11+
12+
13+
/** The BSON Serialization errors enum. */
14+
public enum BSONSerializationError : Error {
15+
16+
/** The given data/stream contains too few bytes to be a valid bson doc. */
17+
case dataTooSmall
18+
/** The given data size is not the one declared by the bson doc. */
19+
case dataLengthDoNotMatch
20+
21+
/** The length of the bson doc is invalid. */
22+
case invalidLength
23+
/**
24+
An invalid element was found.
25+
The element is given in argument to this enum case. */
26+
case invalidElementType(UInt8)
27+
28+
/** Found an invalid bool value (given in arg). */
29+
case invalidBooleanValue(UInt8)
30+
31+
/**
32+
Asked to read an invalid string for the required encoding.
33+
The original data that has been tried to be parsed is given in arg of this error. */
34+
case invalidString(Data)
35+
/**
36+
Invalid end of BSON string found.
37+
Expected `NULL` (`0`), but found the bytes given in argument to this enum case (if `nil`, no data can be read after the string). */
38+
case invalidEndOfString(UInt8?)
39+
40+
/**
41+
An invalid key was found in an array:
42+
Keys must be integers, sorted in ascending order from `0` to `n-1` (where `n = number of elements in the array`).
43+
44+
- Note: Not so sure the increments from one element to the next should necessarily be of one…
45+
The doc is pretty vague on the subject.
46+
It says:
47+
```text
48+
[…] with integer values for the keys, starting with 0 and continuing sequentially.
49+
[…] The keys must be in ascending numerical order.
50+
``` */
51+
case invalidArrayKey(currentKey: String, previousKey: String?)
52+
53+
/** Found an invalid regular expression options value (the complete options and the faulty character are given in arg). */
54+
case invalidRegularExpressionOptions(options: String, invalidCharacter: Character)
55+
/** Found an invalid regular expression value (the regular expression and the parsing error are given in arg). */
56+
case invalidRegularExpression(pattern: String, error: Error)
57+
58+
/**
59+
The JS with scope element gives the raw data length in its definition.
60+
If the given length does not match the decoded length, this error is thrown.
61+
The expected and actual length are given in the error. */
62+
case invalidJSWithScopeLength(expected: Int, actual: Int)
63+
64+
/** An error occurred writing the stream. */
65+
case cannotWriteToStream(streamError: Error?)
66+
67+
/**
68+
An invalid BSON object was given to be serialized.
69+
The invalid element is passed in argument to this error. */
70+
case invalidBSONObject(invalidElement: Any)
71+
/**
72+
Tried to serialize an unserializable string (using the C representation).
73+
This is due to the fact that the `\0` char can be used in a valid UTF8 string.
74+
(Note: the characters `\254` and `\255` can NEVER be used in a valid UTF8 string.
75+
Why were they not the separator?)
76+
77+
Usually, the BSON strings represented using the C representation are dictionary keys.
78+
But they can also be the components of a regexp. */
79+
case unserializableCString(String)
80+
81+
/** Cannot allocate memory (either with `malloc` or `UnsafePointer.alloc()`). */
82+
case cannotAllocateMemory(Int)
83+
/** An internal error occurred rendering the serialization impossible. */
84+
case internalError
85+
86+
}
87+
88+
typealias Err = BSONSerializationError

0 commit comments

Comments
 (0)