/* * Copyright 2024 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import Foundation /// Verifier that check if the buffer passed into it is a valid, /// safe, aligned Flatbuffers object since swift read from `unsafeMemory` public struct Verifier { /// Flag to check for alignment if true fileprivate let _checkAlignment: Bool /// Storage for all changing values within the verifier private let storage: Storage /// Current verifiable ByteBuffer internal var _buffer: ByteBuffer /// Options for verification internal let _options: VerifierOptions /// Current stored capacity within the verifier var capacity: Int { storage.capacity } /// Current depth of verifier var depth: Int { storage.depth } /// Current table count var tableCount: Int { storage.tableCount } /// Initializer for the verifier /// - Parameters: /// - buffer: Bytebuffer that is required to be verified /// - options: `VerifierOptions` that set the rule for some of the verification done /// - checkAlignment: If alignment check is required to be preformed /// - Throws: `exceedsMaxSizeAllowed` if capacity of the buffer is more than 2GiB public init( buffer: inout ByteBuffer, options: VerifierOptions = .init(), checkAlignment: Bool = true) throws { guard buffer.capacity < FlatBufferMaxSize else { throw FlatbuffersErrors.exceedsMaxSizeAllowed } _buffer = buffer _checkAlignment = checkAlignment _options = options storage = Storage(capacity: buffer.capacity) } /// Resets the verifier to initial state public func reset() { storage.depth = 0 storage.tableCount = 0 } /// Checks if the value of type `T` is aligned properly in the buffer /// - Parameters: /// - position: Current position /// - type: Type of value to check /// - Throws: `missAlignedPointer` if the pointer is not aligned properly public func isAligned(position: Int, type: T.Type) throws { /// If check alignment is false this mutating function doesnt continue if !_checkAlignment { return } /// advance pointer to position X let ptr = _buffer._storage.memory.advanced(by: position) /// Check if the pointer is aligned if Int(bitPattern: ptr) & (MemoryLayout.alignment &- 1) == 0 { return } throw FlatbuffersErrors.missAlignedPointer( position: position, type: String(describing: T.self)) } /// Checks if the value of Size "X" is within the range of the buffer /// - Parameters: /// - position: Current position to be read /// - size: `Byte` Size of readable object within the buffer /// - Throws: `outOfBounds` if the value is out of the bounds of the buffer /// and `apparentSizeTooLarge` if the apparent size is bigger than the one specified /// in `VerifierOptions` public func rangeInBuffer(position: Int, size: Int) throws { let end = UInt(clamping: (position &+ size).magnitude) if end > _buffer.capacity { throw FlatbuffersErrors.outOfBounds(position: end, end: storage.capacity) } storage.apparentSize = storage.apparentSize &+ UInt32(size) if storage.apparentSize > _options._maxApparentSize { throw FlatbuffersErrors.apparentSizeTooLarge } } /// Validates if a value of type `T` is aligned and within the bounds of /// the buffer /// - Parameters: /// - position: Current readable position /// - type: Type of value to check /// - Throws: FlatbuffersErrors public func inBuffer(position: Int, of type: T.Type) throws { try isAligned(position: position, type: type) try rangeInBuffer(position: position, size: MemoryLayout.size) } /// Visits a table at the current position and validates if the table meets /// the rules specified in the `VerifierOptions` /// - Parameter position: Current position to be read /// - Throws: FlatbuffersErrors /// - Returns: A `TableVerifier` at the current readable table public mutating func visitTable(at position: Int) throws -> TableVerifier { let vtablePosition = try derefOffset(position: position) let vtableLength: VOffset = try getValue(at: vtablePosition) let length = Int(vtableLength) try isAligned( position: Int(clamping: (vtablePosition + length).magnitude), type: VOffset.self) try rangeInBuffer(position: vtablePosition, size: length) storage.tableCount += 1 if storage.tableCount > _options._maxTableCount { throw FlatbuffersErrors.maximumTables } storage.depth += 1 if storage.depth > _options._maxDepth { throw FlatbuffersErrors.maximumDepth } return TableVerifier( position: position, vtable: vtablePosition, vtableLength: length, verifier: &self) } /// Validates if a value of type `T` is within the buffer and returns it /// - Parameter position: Current position to be read /// - Throws: `inBuffer` errors /// - Returns: a value of type `T` usually a `VTable` or a table offset internal func getValue(at position: Int) throws -> T { try inBuffer(position: position, of: T.self) return _buffer.read(def: T.self, position: position) } /// derefrences an offset within a vtable to get the position of the field /// in the bytebuffer /// - Parameter position: Current readable position /// - Throws: `inBuffer` errors & `signedOffsetOutOfBounds` /// - Returns: Current readable position for a field @inline(__always) internal func derefOffset(position: Int) throws -> Int { try inBuffer(position: position, of: Int32.self) let offset = _buffer.read(def: Int32.self, position: position) // switching to int32 since swift's default Int is int64 // this should be safe since we already checked if its within // the buffer let _int32Position = UInt32(position) let reportedOverflow: (partialValue: UInt32, overflow: Bool) if offset > 0 { reportedOverflow = _int32Position .subtractingReportingOverflow(offset.magnitude) } else { reportedOverflow = _int32Position .addingReportingOverflow(offset.magnitude) } /// since `subtractingReportingOverflow` & `addingReportingOverflow` returns true, /// if there is overflow we return failure if reportedOverflow.overflow || reportedOverflow.partialValue > _buffer .capacity { throw FlatbuffersErrors.signedOffsetOutOfBounds( offset: Int(offset), position: position) } return Int(reportedOverflow.partialValue) } /// finishes the current iteration of verification on an object internal func finish() { storage.depth -= 1 } @inline(__always) func verify(id: String) throws { let size = MemoryLayout.size guard storage.capacity >= (size * 2) else { throw FlatbuffersErrors.bufferDoesntContainID } let str = _buffer.readString(at: size, count: size) if id == str { return } throw FlatbuffersErrors.bufferIdDidntMatchPassedId } final private class Storage { /// Current ApparentSize fileprivate var apparentSize: UOffset = 0 /// Amount of tables present within a buffer fileprivate var tableCount = 0 /// Capacity of the current buffer fileprivate let capacity: Int /// Current reached depth within the buffer fileprivate var depth = 0 init(capacity: Int) { self.capacity = capacity } } }