|
| 1 | +//===----------------------------------------------------------------------===// |
| 2 | +// |
| 3 | +// This source file is part of the Hummingbird server framework project |
| 4 | +// |
| 5 | +// Copyright (c) 2024 the Hummingbird authors |
| 6 | +// Licensed under Apache License v2.0 |
| 7 | +// |
| 8 | +// See LICENSE.txt for license information |
| 9 | +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors |
| 10 | +// |
| 11 | +// SPDX-License-Identifier: Apache-2.0 |
| 12 | +// |
| 13 | +//===----------------------------------------------------------------------===// |
| 14 | + |
| 15 | +import NIOCore |
| 16 | + |
| 17 | +#if compiler(>=6) |
| 18 | +extension ByteBuffer { |
| 19 | + /// Get the string at `index` from this `ByteBuffer` decoding using the UTF-8 encoding. Does not move the reader index. |
| 20 | + /// The selected bytes must be readable or else `nil` will be returned. |
| 21 | + /// |
| 22 | + /// This is an alternative to `ByteBuffer.getString(at:length:)` which ensures the returned string is valid UTF8. If the |
| 23 | + /// string is not valid UTF8 then a `ReadUTF8ValidationError` error is thrown. |
| 24 | + /// |
| 25 | + /// - Parameters: |
| 26 | + /// - index: The starting index into `ByteBuffer` containing the string of interest. |
| 27 | + /// - length: The number of bytes making up the string. |
| 28 | + /// - Returns: A `String` value containing the UTF-8 decoded selected bytes from this `ByteBuffer` or `nil` if |
| 29 | + /// the requested bytes are not readable. |
| 30 | + @inlinable |
| 31 | + @available(macOS 15, iOS 18, tvOS 18, watchOS 11, *) |
| 32 | + public func getUTF8ValidatedString(at index: Int, length: Int) throws -> String? { |
| 33 | + guard let range = self.rangeWithinReadableBytes(index: index, length: length) else { |
| 34 | + return nil |
| 35 | + } |
| 36 | + return try self.withUnsafeReadableBytes { pointer in |
| 37 | + assert(range.lowerBound >= 0 && (range.upperBound - range.lowerBound) <= pointer.count) |
| 38 | + guard |
| 39 | + let string = String( |
| 40 | + validating: UnsafeRawBufferPointer(fastRebase: pointer[range]), |
| 41 | + as: Unicode.UTF8.self |
| 42 | + ) |
| 43 | + else { |
| 44 | + throw ReadUTF8ValidationError.invalidUTF8 |
| 45 | + } |
| 46 | + return string |
| 47 | + } |
| 48 | + } |
| 49 | + |
| 50 | + /// Read `length` bytes off this `ByteBuffer`, decoding it as `String` using the UTF-8 encoding. Move the reader index |
| 51 | + /// forward by `length`. |
| 52 | + /// |
| 53 | + /// This is an alternative to `ByteBuffer.readString(length:)` which ensures the returned string is valid UTF8. If the |
| 54 | + /// string is not valid UTF8 then a `ReadUTF8ValidationError` error is thrown and the reader index is not advanced. |
| 55 | + /// |
| 56 | + /// - Parameters: |
| 57 | + /// - length: The number of bytes making up the string. |
| 58 | + /// - Returns: A `String` value deserialized from this `ByteBuffer` or `nil` if there aren't at least `length` bytes readable. |
| 59 | + @inlinable |
| 60 | + @available(macOS 15, iOS 18, tvOS 18, watchOS 11, *) |
| 61 | + public mutating func readUTF8ValidatedString(length: Int) throws -> String? { |
| 62 | + guard let result = try self.getUTF8ValidatedString(at: self.readerIndex, length: length) else { |
| 63 | + return nil |
| 64 | + } |
| 65 | + self.moveReaderIndex(forwardBy: length) |
| 66 | + return result |
| 67 | + } |
| 68 | + |
| 69 | + /// Errors thrown when calling `readUTF8ValidatedString` or `getUTF8ValidatedString`. |
| 70 | + public struct ReadUTF8ValidationError: Error, Equatable { |
| 71 | + private enum BaseError: Hashable { |
| 72 | + case invalidUTF8 |
| 73 | + } |
| 74 | + |
| 75 | + private var baseError: BaseError |
| 76 | + |
| 77 | + /// The length of the bytes to copy was negative. |
| 78 | + public static let invalidUTF8: ReadUTF8ValidationError = .init(baseError: .invalidUTF8) |
| 79 | + } |
| 80 | + |
| 81 | + @inlinable |
| 82 | + func rangeWithinReadableBytes(index: Int, length: Int) -> Range<Int>? { |
| 83 | + guard index >= self.readerIndex, length >= 0 else { |
| 84 | + return nil |
| 85 | + } |
| 86 | + |
| 87 | + // both these &-s are safe, they can't underflow because both left & right side are >= 0 (and index >= readerIndex) |
| 88 | + let indexFromReaderIndex = index &- self.readerIndex |
| 89 | + assert(indexFromReaderIndex >= 0) |
| 90 | + guard indexFromReaderIndex <= self.readableBytes &- length else { |
| 91 | + return nil |
| 92 | + } |
| 93 | + |
| 94 | + let upperBound = indexFromReaderIndex &+ length // safe, can't overflow, we checked it above. |
| 95 | + |
| 96 | + // uncheckedBounds is safe because `length` is >= 0, so the lower bound will always be lower/equal to upper |
| 97 | + return Range<Int>(uncheckedBounds: (lower: indexFromReaderIndex, upper: upperBound)) |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +extension UnsafeRawBufferPointer { |
| 102 | + @inlinable |
| 103 | + init(fastRebase slice: Slice<UnsafeRawBufferPointer>) { |
| 104 | + let base = slice.base.baseAddress?.advanced(by: slice.startIndex) |
| 105 | + self.init(start: base, count: slice.endIndex &- slice.startIndex) |
| 106 | + } |
| 107 | +} |
| 108 | + |
| 109 | +#endif // compiler(>=6) |
| 110 | + |
| 111 | +extension String { |
| 112 | + init?(buffer: ByteBuffer, validateUTF8: Bool) { |
| 113 | + #if compiler(>=6) |
| 114 | + if #available(macOS 15, iOS 18, tvOS 18, watchOS 11, *), validateUTF8 { |
| 115 | + do { |
| 116 | + var buffer = buffer |
| 117 | + self = try buffer.readUTF8ValidatedString(length: buffer.readableBytes)! |
| 118 | + } catch { |
| 119 | + return nil |
| 120 | + } |
| 121 | + } else { |
| 122 | + self = .init(buffer: buffer) |
| 123 | + } |
| 124 | + #else |
| 125 | + self = .init(buffer: buffer) |
| 126 | + #endif // compiler(>=6) |
| 127 | + } |
| 128 | +} |
0 commit comments