Skip to content

Commit 51520e5

Browse files
authored
SWIFT-1016 Add a public withID() method (#48)
1 parent 5dda5bf commit 51520e5

File tree

3 files changed

+129
-45
lines changed

3 files changed

+129
-45
lines changed

.swiftlint.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ disabled_rules:
55
- todo
66
- type_body_length
77
- type_name
8+
- cyclomatic_complexity
89

910
opt_in_rules:
1011
- array_init

Sources/SwiftBSON/BSONDocument.swift

Lines changed: 94 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,6 @@ public struct BSONDocument {
1616

1717
private var storage: BSONDocumentStorage
1818

19-
internal var byteLength: Int {
20-
get {
21-
guard let byteLength = self.storage.buffer.getInteger(at: 0, endianness: .little, as: Int32.self) else {
22-
fatalError("Cannot read byteLength of BSON from buffer")
23-
}
24-
return Int(byteLength)
25-
}
26-
set {
27-
guard newValue <= Int32.max else {
28-
fatalError("Cannot cast \(newValue) down to Int32")
29-
}
30-
self.storage.buffer.setInteger(Int32(newValue), at: 0, endianness: .little, as: Int32.self)
31-
}
32-
}
33-
3419
/// An unordered set containing the keys in this document.
3520
internal private(set) var keySet: Set<String>
3621

@@ -66,8 +51,8 @@ public struct BSONDocument {
6651
fatalError("Data is \(self.storage.buffer.writerIndex - start) bytes, "
6752
+ "but maximum allowed BSON document size is \(Int32.max) bytes")
6853
}
69-
// Set byteLength in reserved space
70-
self.storage.buffer.setInteger(byteLength, at: 0, endianness: .little, as: Int32.self)
54+
// Set encodedLength in reserved space
55+
self.storage.encodedLength = Int(byteLength)
7156
}
7257

7358
/// Initializes a new, empty `BSONDocument`.
@@ -103,16 +88,13 @@ public struct BSONDocument {
10388
*/
10489
public init(fromBSON bson: ByteBuffer) throws {
10590
let storage = BSONDocumentStorage(bson)
106-
try storage.validate()
107-
self = BSONDocument(fromUnsafeBSON: storage)
91+
let keys = try storage.validateAndRetrieveKeys()
92+
self = BSONDocument(fromUnsafeBSON: storage, keys: keys)
10893
}
10994

110-
private init(fromUnsafeBSON storage: BSONDocumentStorage) {
111-
self.keySet = Set()
95+
private init(fromUnsafeBSON storage: BSONDocumentStorage, keys: Set<String>) {
11296
self.storage = storage
113-
for (key, _) in self {
114-
self.keySet.insert(key)
115-
}
97+
self.keySet = keys
11698
}
11799

118100
/**
@@ -240,6 +222,44 @@ public struct BSONDocument {
240222
set { self[member] = newValue }
241223
}
242224

225+
/**
226+
* Returns a copy of this document with an `_id` element prepended. If the document already contains an `_id`,
227+
* returns the document as-is.
228+
* - Throws: `BSONError.DocumentTooLargeError` if adding the `_id` would make the document exceed the maximum
229+
* allowed size for a document.
230+
* - SeeAlso: https://docs.mongodb.com/manual/core/document/#the-id-field
231+
*/
232+
public func withID() throws -> BSONDocument {
233+
guard !self.keySet.contains("_id") else {
234+
return self
235+
}
236+
237+
var newStorage = BSONDocumentStorage()
238+
// placeholder for length
239+
newStorage.buffer.writeInteger(0, endianness: .little, as: Int32.self)
240+
var newSize = self.storage.encodedLength
241+
242+
let _id = BSON.objectID()
243+
newSize += newStorage.append(key: "_id", value: _id)
244+
245+
guard newSize <= BSON_MAX_SIZE else {
246+
throw BSONError.DocumentTooLargeError(value: _id.bsonValue, forKey: "_id")
247+
}
248+
249+
guard let suffix = self.storage.buffer.getBytes(at: 4, length: self.storage.encodedLength - 4) else {
250+
throw BSONError.InternalError(
251+
message: "Failed to slice buffer from 4 to \(self.storage.encodedLength): \(self.storage.buffer)"
252+
)
253+
}
254+
newStorage.buffer.writeBytes(suffix)
255+
256+
var newKeys = self.keySet
257+
newKeys.insert("_id")
258+
var document = BSONDocument(fromUnsafeBSON: newStorage, keys: newKeys)
259+
document.storage.encodedLength = newSize
260+
return document
261+
}
262+
243263
/**
244264
* Sets a BSON element with the corresponding key
245265
* if element.value is nil the element is deleted from the BSON
@@ -252,10 +272,11 @@ public struct BSONDocument {
252272
}
253273
// appending new key
254274
self.keySet.insert(key)
255-
self.storage.buffer.moveWriterIndex(to: self.byteLength - 1) // setup to overwrite null terminator
275+
// setup to overwrite null terminator
276+
self.storage.buffer.moveWriterIndex(to: self.storage.encodedLength - 1)
256277
let size = self.storage.append(key: key, value: value)
257278
self.storage.buffer.writeInteger(0, endianness: .little, as: UInt8.self) // add back in our null terminator
258-
self.byteLength += size
279+
self.storage.encodedLength += size
259280
return
260281
}
261282

@@ -265,21 +286,24 @@ public struct BSONDocument {
265286
throw BSONError.InternalError(message: "Cannot find \(key) to delete")
266287
}
267288

289+
let prefixLength = range.startIndex
290+
let suffixLength = self.storage.encodedLength - range.endIndex
291+
268292
guard
269-
let prefix = self.storage.buffer.getBytes(at: 0, length: range.startIndex),
270-
let suffix = self.storage.buffer.getBytes(at: range.endIndex, length: self.byteLength - range.endIndex)
293+
let prefix = self.storage.buffer.getBytes(at: 0, length: prefixLength),
294+
let suffix = self.storage.buffer.getBytes(at: range.endIndex, length: suffixLength)
271295
else {
272296
throw BSONError.InternalError(
273297
message: "Cannot slice buffer from " +
274298
"0 to len \(range.startIndex) and from \(range.endIndex) " +
275-
"to len \(self.byteLength - range.endIndex) : \(self.storage.buffer)"
299+
"to len \(suffixLength) : \(self.storage.buffer)"
276300
)
277301
}
278302

279303
var newStorage = BSONDocumentStorage()
280304
newStorage.buffer.writeBytes(prefix)
281305

282-
var newSize = self.byteLength - (range.endIndex - range.startIndex)
306+
var newSize = self.storage.encodedLength - (range.endIndex - range.startIndex)
283307
if let value = value {
284308
// Overwriting
285309
let size = newStorage.append(key: key, value: value)
@@ -296,9 +320,9 @@ public struct BSONDocument {
296320
newStorage.buffer.writeBytes(suffix)
297321

298322
self.storage = newStorage
299-
self.byteLength = newSize
300-
guard self.byteLength == self.storage.buffer.readableBytes else {
301-
fatalError("BSONDocument's encoded byte length is \(self.byteLength), however the " +
323+
self.storage.encodedLength = newSize
324+
guard self.storage.encodedLength == self.storage.buffer.readableBytes else {
325+
fatalError("BSONDocument's encoded byte length is \(self.storage.encodedLength), however the " +
302326
"buffer has \(self.storage.buffer.readableBytes) readable bytes")
303327
}
304328
}
@@ -314,6 +338,21 @@ public struct BSONDocument {
314338
/// Create BSONDocumentStorage with a 0 capacity buffer.
315339
internal init() { self.buffer = BSON_ALLOCATOR.buffer(capacity: 0) }
316340

341+
internal var encodedLength: Int {
342+
get {
343+
guard let encodedLength = self.buffer.getInteger(at: 0, endianness: .little, as: Int32.self) else {
344+
fatalError("Cannot read encoded Length of BSON from buffer")
345+
}
346+
return Int(encodedLength)
347+
}
348+
set {
349+
guard newValue <= Int32.max else {
350+
fatalError("Cannot cast \(newValue) down to Int32")
351+
}
352+
self.buffer.setInteger(Int32(newValue), at: 0, endianness: .little, as: Int32.self)
353+
}
354+
}
355+
317356
/// Appends element to underlying BSON bytes, returns the size of the element appended: type + key + value
318357
@discardableResult internal mutating func append(key: String, value: BSON) -> Int {
319358
let writer = self.buffer.writerIndex
@@ -323,36 +362,44 @@ public struct BSONDocument {
323362
return self.buffer.writerIndex - writer
324363
}
325364

326-
internal func validate() throws {
365+
@discardableResult
366+
internal func validateAndRetrieveKeys() throws -> Set<String> {
327367
// Pull apart the underlying binary into [KeyValuePair], should reveal issues
328-
guard let byteLength = self.buffer.getInteger(at: 0, endianness: .little, as: Int32.self) else {
329-
throw BSONError.InvalidArgumentError(message: "Validation Failed: Cannot read byteLength")
368+
guard let encodedLength = self.buffer.getInteger(at: 0, endianness: .little, as: Int32.self) else {
369+
throw BSONError.InvalidArgumentError(message: "Validation Failed: Cannot read encoded length")
330370
}
331371

332-
guard byteLength >= BSON_MIN_SIZE && byteLength <= BSON_MAX_SIZE else {
372+
guard encodedLength >= BSON_MIN_SIZE && encodedLength <= BSON_MAX_SIZE else {
333373
throw BSONError.InvalidArgumentError(
334-
message: "Validation Failed: BSON cannot be \(byteLength) bytes long"
374+
message: "Validation Failed: BSON cannot be \(encodedLength) bytes long"
335375
)
336376
}
337377

338-
guard byteLength == self.buffer.readableBytes else {
378+
guard encodedLength == self.buffer.readableBytes else {
339379
throw BSONError.InvalidArgumentError(
340-
message: "BSONDocument's encoded byte length is \(byteLength), however the" +
380+
message: "BSONDocument's encoded byte length is \(encodedLength), however the" +
341381
"buffer has \(self.buffer.readableBytes) readable bytes"
342382
)
343383
}
344384

385+
var keySet = Set<String>()
345386
let iter = BSONDocumentIterator(over: self.buffer)
346387
// Implicitly validate with iterator
347388
do {
348-
while let (_, value) = try iter.nextThrowing() {
389+
while let (key, value) = try iter.nextThrowing() {
390+
let (inserted, _) = keySet.insert(key)
391+
guard inserted else {
392+
throw BSONError.InvalidArgumentError(
393+
message: "Validation Failed: BSON contains multiple values for key \(key)"
394+
)
395+
}
349396
switch value {
350397
case let .document(doc):
351-
try doc.storage.validate()
398+
try doc.storage.validateAndRetrieveKeys()
352399
case let .array(array):
353400
for item in array {
354401
if let doc = item.documentValue {
355-
try doc.storage.validate()
402+
try doc.storage.validateAndRetrieveKeys()
356403
}
357404
}
358405
default:
@@ -364,6 +411,8 @@ public struct BSONDocument {
364411
message: "Validation Failed: \(error.message)"
365412
)
366413
}
414+
415+
return keySet
367416
}
368417
}
369418
}
@@ -449,11 +498,11 @@ extension BSONDocument: BSONValue {
449498

450499
internal static func read(from buffer: inout ByteBuffer) throws -> BSON {
451500
let reader = buffer.readerIndex
452-
guard let byteLength = buffer.readInteger(endianness: .little, as: Int32.self) else {
501+
guard let encodedLength = buffer.readInteger(endianness: .little, as: Int32.self) else {
453502
throw BSONError.InternalError(message: "Cannot read document byte length")
454503
}
455504
buffer.moveReaderIndex(to: reader)
456-
guard let bytes = buffer.readBytes(length: Int(byteLength)) else {
505+
guard let bytes = buffer.readBytes(length: Int(encodedLength)) else {
457506
throw BSONError.InternalError(message: "Cannot read document contents")
458507
}
459508
return .document(try BSONDocument(fromBSON: Data(bytes)))

Tests/SwiftBSONTests/DocumentTests.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,4 +758,38 @@ final class DocumentTests: BSONTestCase {
758758
expect(try BSONDocument(fromBSON: data)).to(throwError(errorType: BSONError.InvalidArgumentError.self))
759759
}
760760
}
761+
762+
func testWithID() throws {
763+
let doc1: BSONDocument = ["a": .int32(1)]
764+
765+
let withID1 = try doc1.withID()
766+
expect(withID1.keys).to(equal(["_id", "a"]))
767+
expect(withID1["_id"]?.objectIDValue).toNot(beNil())
768+
769+
let data = withID1.toData()
770+
// 4 for length, 17 for "_id": oid, 7 for "a": 1, 1 for null terminator
771+
expect(data).to(haveCount(29))
772+
773+
// build what we expect the data to look like
774+
let length = "1d000000" // little-endian Int32 rep of 29
775+
// byte prefix for ObjectID + "_id" as cstring + oid bytes
776+
let oid = "07" + "5f696400" + withID1["_id"]!.objectIDValue!.hex
777+
// byte prefix for Int32 + "a" as cstring + little-endian Int32 rep of 1
778+
let a = "10" + "6100" + "01000000"
779+
780+
let expectedHex = length + oid + a + "00" // null terminator
781+
expect(data.hexDescription).to(equal(expectedHex))
782+
783+
// verify a document with an _id is unchanged by calling this method
784+
let doc2: BSONDocument = ["x": 1, "_id": .objectID()]
785+
let withID2 = try doc2.withID()
786+
expect(withID2).to(equal(doc2))
787+
}
788+
789+
func testDuplicateKeyInBSON() throws {
790+
// contains multiple values for key "a"
791+
let hex = "1b0000001261000100000000000000126100020000000000000000"
792+
let data = Data(hexString: hex)!
793+
expect(try BSONDocument(fromBSON: data)).to(throwError(errorType: BSONError.InvalidArgumentError.self))
794+
}
761795
}

0 commit comments

Comments
 (0)