Coverage Report

Created: 2026-10-13 06:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/swift-nio/Sources/NIOCore/ByteBuffer-binaryEncodedLengthPrefix.swift
Line
Count
Source
1
//===----------------------------------------------------------------------===//
2
//
3
// This source file is part of the SwiftNIO open source project
4
//
5
// Copyright (c) 2024 Apple Inc. and the SwiftNIO project authors
6
// Licensed under Apache License v2.0
7
//
8
// See LICENSE.txt for license information
9
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10
//
11
// SPDX-License-Identifier: Apache-2.0
12
//
13
//===----------------------------------------------------------------------===//
14
15
/// Describes a way to encode and decode an integer as bytes.
16
///
17
/// For more information, see <doc:ByteBuffer-lengthPrefix>
18
public protocol NIOBinaryIntegerEncodingStrategy {
19
    /// Read an integer from a buffer.
20
    /// If there are not enough bytes to read an integer of this encoding, return nil, and do not move the reader index.
21
    /// If the the full integer can be read, move the reader index to after the integer, and return the integer.
22
    /// - Parameters:
23
    ///   - as: The type of integer to be read.
24
    ///   - buffer: The buffer to read from.
25
    /// - Returns: The integer that was read, or nil if it was not possible to read it.
26
    func readInteger<IntegerType: FixedWidthInteger>(
27
        as: IntegerType.Type,
28
        from buffer: inout ByteBuffer
29
    ) -> IntegerType?
30
31
    /// Write an integer to a buffer. Move the writer index to after the written integer.
32
    /// - Parameters:
33
    ///    - integer: The integer to write.
34
    ///    - buffer: The buffer to write to.
35
    /// - Returns: The number of bytes used to write the integer.
36
    func writeInteger<IntegerType: FixedWidthInteger>(
37
        _ integer: IntegerType,
38
        to buffer: inout ByteBuffer
39
    ) -> Int
40
41
    /// An estimate of the number of bytes required to write integers using this strategy.
42
    /// Callers may use this to reserve bytes before writing the integer.
43
    /// If the actual bytes used by the write function is more or less than this, it may be necessary to shuffle bytes.
44
    /// Therefore, an accurate prediction here will improve performance.
45
    /// This function will be called from ``ByteBuffer/writeLengthPrefixed(strategy:writeData:)``
46
    var requiredBytesHint: Int { get }
47
48
    /// Write an integer to a buffer. Move the writer index to after the written integer.
49
    /// This function will be called when an integer needs to be written, and some capacity has already been reserved for it.
50
    /// Implementers should consider using a less efficient encoding, if possible, to fit exactly within the reserved capacity.
51
    /// Otherwise, the caller will need to shift bytes to reconcile the difference.
52
    /// It is up to the implementer to find the balance between performance and size.
53
    /// - Parameters:
54
    ///   - integer: The integer to write
55
    ///   - reservedCapacity: The capacity already reserved for writing this integer
56
    ///   - buffer: The buffer to write into.
57
    /// - Returns: The number of bytes used to write the integer.
58
    func writeInteger(
59
        _ integer: Int,
60
        reservedCapacity: Int,
61
        to buffer: inout ByteBuffer
62
    ) -> Int
63
}
64
65
extension NIOBinaryIntegerEncodingStrategy {
66
    @inlinable
67
0
    public var requiredBytesHint: Int { 1 }
68
69
    @inlinable
70
    public func writeInteger<IntegerType: FixedWidthInteger>(
71
        _ integer: IntegerType,
72
        reservedCapacity: Int,
73
        to buffer: inout ByteBuffer
74
0
    ) -> Int {
75
0
        self.writeInteger(integer, to: &buffer)
76
0
    }
77
}
78
79
extension ByteBuffer {
80
    /// Read a binary encoded integer, moving the `readerIndex` appropriately.
81
    /// If there are not enough bytes, nil is returned.
82
    @inlinable
83
    public mutating func readEncodedInteger<Strategy: NIOBinaryIntegerEncodingStrategy, Integer: FixedWidthInteger>(
84
        as: Integer.Type = Integer.self,
85
        strategy: Strategy
86
0
    ) -> Integer? {
87
0
        strategy.readInteger(as: Integer.self, from: &self)
88
0
    }
89
90
    /// Write a binary encoded integer.
91
    ///
92
    /// - Returns: The number of bytes written.
93
    @discardableResult
94
    @inlinable
95
    public mutating func writeEncodedInteger<
96
        Integer: FixedWidthInteger,
97
        Strategy: NIOBinaryIntegerEncodingStrategy
98
    >(
99
        _ value: Integer,
100
        strategy: Strategy
101
0
    ) -> Int {
102
0
        strategy.writeInteger(value, to: &self)
103
0
    }
104
105
    /// Prefixes bytes written by `writeData` with the number of bytes written.
106
    /// The number of bytes written is encoded using `strategy`.
107
    ///
108
    /// - Note: This function works by reserving the number of bytes suggested by `strategy` before the data.
109
    /// It then writes the data, and then goes back to write the length.
110
    /// If the reserved capacity turns out to be too little or too much, then the data will be shifted.
111
    /// Therefore, this function is most performant if the strategy is able to use the same number of bytes that it reserved.
112
    ///
113
    /// - Parameters:
114
    ///   - strategy: The strategy to use for encoding the length.
115
    ///   - writeData: A closure that takes a buffer, writes some data to it, and returns the number of bytes written.
116
    /// - Returns: Number of total bytes written. This is the length of the written data + the number of bytes used to write the length before it.
117
    @discardableResult
118
    @inlinable
119
    public mutating func writeLengthPrefixed<Strategy: NIOBinaryIntegerEncodingStrategy, ErrorType: Error>(
120
        strategy: Strategy,
121
        writeData: (_ buffer: inout ByteBuffer) throws(ErrorType) -> Int
122
0
    ) throws(ErrorType) -> Int {
123
0
        /// The index at which we write the length
124
0
        let lengthPrefixIndex = self.writerIndex
125
0
        /// The space which we reserve for writing the length
126
0
        let reservedCapacity = strategy.requiredBytesHint
127
0
        self.writeRepeatingByte(0, count: reservedCapacity)
128
0
129
0
        /// The index at which we start writing the data originally. We may later move the data if the reserved space for the length wasn't right
130
0
        let originalDataStartIndex = self.writerIndex
131
0
        /// The length of the data written
132
0
        let dataLength: Int
133
0
        do {
134
0
            dataLength = try writeData(&self)
135
0
        } catch {
136
0
            // Clean up our write so that it as if we never did it.
137
0
            self.moveWriterIndex(to: lengthPrefixIndex)
138
0
            throw error
139
0
        }
140
0
        /// The index at the end of the written data originally. We may later move the data if the reserved space for the length wasn't right
141
0
        let originalDataEndIndex = self.writerIndex
142
0
143
0
        // Quick check to make sure the user didn't do something silly
144
0
        precondition(
145
0
            originalDataEndIndex - originalDataStartIndex == dataLength,
146
0
            "writeData returned \(dataLength) bytes, but actually \(originalDataEndIndex - originalDataStartIndex) bytes were written. They must be the same."
147
0
        )
148
0
149
0
        // We write the length after the data to begin with. We will move it later
150
0
151
0
        /// The actual number of bytes used to write the length written. The user may write more or fewer bytes than what we reserved
152
0
        let actualIntegerLength = strategy.writeInteger(
153
0
            dataLength,
154
0
            reservedCapacity: reservedCapacity,
155
0
            to: &self
156
0
        )
157
0
158
0
        switch actualIntegerLength {
159
0
        case reservedCapacity:
160
0
            // Good, exact match, swap the values and then "delete" the trailing bytes by moving the index back
161
0
            self._moveBytes(from: originalDataEndIndex, to: lengthPrefixIndex, size: actualIntegerLength)
162
0
            self.moveWriterIndex(to: originalDataEndIndex)
163
0
        case ..<reservedCapacity:
164
0
            // We wrote fewer bytes. We now have to move the length bytes from the end, and
165
0
            // _then_ shrink the rest of the buffer onto it.
166
0
            self._moveBytes(from: originalDataEndIndex, to: lengthPrefixIndex, size: actualIntegerLength)
167
0
            let newDataStartIndex = lengthPrefixIndex + actualIntegerLength
168
0
            self._moveBytes(
169
0
                from: originalDataStartIndex,
170
0
                to: newDataStartIndex,
171
0
                size: dataLength
172
0
            )
173
0
            self.moveWriterIndex(to: newDataStartIndex + dataLength)
174
0
        case reservedCapacity...:
175
0
            // We wrote more bytes. We now have to create enough space. Once we do, we have the same
176
0
            // implementation as the matching case.
177
0
            let extraSpaceNeeded = actualIntegerLength - reservedCapacity
178
0
            self._createSpace(before: lengthPrefixIndex, requiredSpace: extraSpaceNeeded)
179
0
180
0
            // Clean up the indices.
181
0
            let newDataEndIndex = originalDataEndIndex + extraSpaceNeeded
182
0
            // We wrote the length after the data, so we have to move those bytes to the space at the front
183
0
            self._moveBytes(from: newDataEndIndex, to: lengthPrefixIndex, size: actualIntegerLength)
184
0
            self.moveWriterIndex(to: newDataEndIndex)
185
0
        default:
186
0
            fatalError("Unreachable")
187
0
        }
188
0
189
0
        let totalBytesWritten = self.writerIndex - lengthPrefixIndex
190
0
        return totalBytesWritten
191
0
    }
192
193
    /// Reads a slice which is prefixed with a length. The length will be read using `strategy`, and then that many bytes will be read to create a slice.
194
    /// - Parameter strategy: The encoding strategy to use.
195
    /// - Returns: The slice, if there are enough bytes to read it fully. In this case, the readerIndex will move to after the slice.
196
    /// If there are not enough bytes to read the full slice, the readerIndex will stay unchanged.
197
    @inlinable
198
    public mutating func readLengthPrefixedSlice<Strategy: NIOBinaryIntegerEncodingStrategy>(
199
        strategy: Strategy
200
0
    ) -> ByteBuffer? {
201
0
        let originalReaderIndex = self.readerIndex
202
0
        guard let length = strategy.readInteger(as: Int.self, from: &self), let slice = self.readSlice(length: length)
203
0
        else {
204
0
            self.moveReaderIndex(to: originalReaderIndex)
205
0
            return nil
206
0
        }
207
0
        return slice
208
0
    }
209
}
210
211
// MARK: - Helpers for writing length-prefixed things
212
213
extension ByteBuffer {
214
    /// Write the length of `buffer` using `strategy`. Then write the buffer.
215
    /// - Parameters:
216
    ///   - buffer: The buffer to be written.
217
    ///   - strategy: The encoding strategy to use.
218
    /// - Returns: The total bytes written. This is the bytes needed to write the length, plus the length of the buffer itself.
219
    @discardableResult
220
    @inlinable
221
    public mutating func writeLengthPrefixedBuffer<
222
        Strategy: NIOBinaryIntegerEncodingStrategy
223
    >(
224
        _ buffer: ByteBuffer,
225
        strategy: Strategy
226
0
    ) -> Int {
227
0
        self.reserveCapacity(minimumWritableBytes: buffer.readableBytes + strategy.requiredBytesHint)
228
0
        var written = 0
229
0
        written += self.writeEncodedInteger(buffer.readableBytes, strategy: strategy)
230
0
        written += self.writeImmutableBuffer(buffer)
231
0
        return written
232
0
    }
233
234
    /// Write the length of `string` using `strategy`. Then write the string.
235
    /// - Parameters:
236
    ///  - string: The string to be written.
237
    ///  - strategy: The encoding strategy to use.
238
    /// - Returns: The total bytes written. This is the bytes needed to write the length, plus the length of the string itself.
239
    @discardableResult
240
    @inlinable
241
    public mutating func writeLengthPrefixedString<
242
        Strategy: NIOBinaryIntegerEncodingStrategy
243
    >(
244
        _ string: String,
245
        strategy: Strategy
246
0
    ) -> Int {
247
0
        // writeString always writes the String as UTF8 bytes, without a null-terminator
248
0
        // So the length will be the utf8 count
249
0
        self.reserveCapacity(minimumWritableBytes: string.utf8.count + strategy.requiredBytesHint)
250
0
        var written = 0
251
0
        written += self.writeEncodedInteger(string.utf8.count, strategy: strategy)
252
0
        written += self.writeString(string)
253
0
        return written
254
0
    }
255
256
    /// Write the length of `bytes` using `strategy`. Then write the bytes.
257
    /// - Parameters:
258
    ///  - bytes: The bytes to be written.
259
    ///  - strategy: The encoding strategy to use.
260
    /// - Returns: The total bytes written. This is the bytes needed to write the length, plus the length of the bytes themselves.
261
    @discardableResult
262
    @inlinable
263
    public mutating func writeLengthPrefixedBytes<
264
        Bytes: Sequence,
265
        Strategy: NIOBinaryIntegerEncodingStrategy
266
    >(
267
        _ bytes: Bytes,
268
        strategy: Strategy
269
    ) -> Int
270
0
    where Bytes.Element == UInt8 {
271
0
        let numberOfBytes = bytes.withContiguousStorageIfAvailable { b in
272
0
            UnsafeRawBufferPointer(b).count
273
0
        }
274
0
        if let numberOfBytes {
275
0
            self.reserveCapacity(minimumWritableBytes: numberOfBytes + strategy.requiredBytesHint)
276
0
            var written = 0
277
0
            written += self.writeEncodedInteger(numberOfBytes, strategy: strategy)
278
0
            written += self.writeBytes(bytes)
279
0
            return written
280
0
        } else {
281
0
            return self.writeLengthPrefixed(strategy: strategy) { buffer in
282
0
                buffer.writeBytes(bytes)
283
0
            }
284
0
        }
285
0
    }
286
}
287
288
// MARK: - Helpers for reading length-prefixed things
289
290
extension ByteBuffer {
291
    /// Reads a string which is prefixed with a length. The length will be read using `strategy`, and then that many bytes will be read to create a string.
292
    /// - Parameter strategy: The encoding strategy to use.
293
    /// - Returns: The string, if there are enough bytes to read it fully. In this case, the readerIndex will move to after the string.
294
    /// If there are not enough bytes to read the full string, the readerIndex will stay unchanged.
295
    @inlinable
296
    public mutating func readLengthPrefixedString<Strategy: NIOBinaryIntegerEncodingStrategy>(
297
        strategy: Strategy
298
0
    ) -> String? {
299
0
        self.readLengthPrefixedSlice(strategy: strategy).map { String(buffer: $0) }
300
0
    }
301
302
    /// Reads bytes which are prefixed with a length. The length will be read using `strategy`, and then that many bytes will be read to create an array of bytes.
303
    /// - Parameter strategy: The encoding strategy to use.
304
    /// - Returns: The array of bytes, if there are enough bytes to read it fully. In this case, the readerIndex will move to after the bytes.
305
    /// If there are not enough bytes to read the full array of bytes, the readerIndex will stay unchanged.
306
    @inlinable
307
    public mutating func readLengthPrefixedBytes<Strategy: NIOBinaryIntegerEncodingStrategy>(
308
        strategy: Strategy
309
0
    ) -> [UInt8]? {
310
0
        self.readLengthPrefixedSlice(strategy: strategy).map { Array(buffer: $0) }
311
0
    }
312
}
313
314
extension ByteBuffer {
315
    /// Creates `requiredSpace` bytes of free space immediately before `index`.
316
    /// e.g. given [a, b, c, d, e, f, g, h, i, j] and calling this function with (before: 4, requiredSpace: 2) would result in
317
    /// [a, b, c, d, 0, 0, e, f, g, h, i, j]
318
    /// 2 extra bytes of space were created before index 4 (the letter e).
319
    /// The total bytes written will be equal to `requiredSpace`, and the writer index will be moved accordingly.
320
    @usableFromInline
321
0
    mutating func _createSpace(before index: Int, requiredSpace: Int) {
322
0
        precondition(index >= self.readerIndex)
323
0
        let bytesToMove = self.writerIndex - index
324
0
325
0
        // Add the required number of bytes to the end first
326
0
        self.writeRepeatingByte(0, count: requiredSpace)
327
0
        // Move the data forward by that many bytes, to make space at the front
328
0
        // The precondition above makes this safe: our indices are in the valid range, so we can safely use them here
329
0
        try! self.copyBytes(at: index, to: index + requiredSpace, length: bytesToMove)
330
0
    }
331
332
    /// Move the `size` bytes starting from `source` to `destination`.
333
    /// `source` and `destination` must both be within the writable range.
334
    @usableFromInline
335
0
    mutating func _moveBytes(from source: Int, to destination: Int, size: Int) {
336
0
        precondition(source >= self.readerIndex && destination < self.writerIndex && source >= destination)
337
0
        precondition(source + size <= self.writerIndex)
338
0
339
0
        // The precondition above makes this safe: our indices are in the valid range, so we can safely use them here
340
0
        try! self.copyBytes(at: source, to: destination, length: size)
341
0
    }
342
}