summaryrefslogtreecommitdiff
path: root/src/mscorlib/shared/System/Runtime/InteropServices/StringBuffer.cs
blob: fdd0b95590194574951a1e41a70d265ed7490f3f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Buffers;
using System.Runtime.CompilerServices;

namespace System.Runtime.InteropServices
{
    /// <summary>
    /// Buffer that deals in char size increments. Dispose to free memory. Always makes ordinal
    /// comparisons. Not thread safe.
    ///
    /// A more performant replacement for StringBuilder when performing native interop.
    /// 
    /// "No copy" valuetype. Has to be passed as "ref".
    /// 
    /// </summary>
    /// <remarks>
    /// Suggested use through P/Invoke: define DllImport arguments that take a character buffer as SafeHandle and pass StringBuffer.GetHandle().
    /// </remarks>
    internal struct StringBuffer
    {
        private char[] _buffer;
        private int _length;

        /// <summary>
        /// Instantiate the buffer with capacity for at least the specified number of characters. Capacity
        /// includes the trailing null character.
        /// </summary>
        public StringBuffer(int initialCapacity)
        {
            _buffer = ArrayPool<char>.Shared.Rent(initialCapacity);
            _length = 0;
        }

        /// <summary>
        /// Get/set the character at the given index.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">Thrown if attempting to index outside of the buffer length.</exception>
        public char this[int index]
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get
            {
                if (index >= _length) throw new ArgumentOutOfRangeException(nameof(index));
                return _buffer[index];
            }
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            set
            {
                if (index >= _length) throw new ArgumentOutOfRangeException(nameof(index));
                _buffer[index] = value;
            }
        }

        /// <summary>
        /// Underlying storage of the buffer. Used for interop.
        /// </summary>
        public char[] UnderlyingArray => _buffer;

        /// <summary>
        /// Character capacity of the buffer. Includes the count for the trailing null character.
        /// </summary>
        public int Capacity => _buffer.Length;

        /// <summary>
        /// Ensure capacity in characters is at least the given minimum.
        /// </summary>
        /// <exception cref="OutOfMemoryException">Thrown if unable to allocate memory when setting.</exception>
        public void EnsureCapacity(int minCapacity)
        {
            if (minCapacity > Capacity)
            {
                char[] oldBuffer = _buffer;
                _buffer = ArrayPool<char>.Shared.Rent(minCapacity);
                Array.Copy(oldBuffer, 0, _buffer, 0, oldBuffer.Length);
                ArrayPool<char>.Shared.Return(oldBuffer);
            }
        }

        /// <summary>
        /// The logical length of the buffer in characters. (Does not include the final null.) Will automatically attempt to increase capacity.
        /// This is where the usable data ends.
        /// </summary>
        /// <exception cref="OutOfMemoryException">Thrown if unable to allocate memory when setting.</exception>
        /// <exception cref="ArgumentOutOfRangeException">Thrown if the set size in bytes is int.MaxValue (as space is implicitly reserved for the trailing null).</exception>
        public int Length
        {
            get { return _length; }
            set
            {
                // Null terminate
                EnsureCapacity(checked(value + 1));
                _buffer[value] = '\0';

                _length = value;
            }
        }

        /// <summary>
        /// True if the buffer contains the given character.
        /// </summary>
        public unsafe bool Contains(char value)
        {
            fixed (char* start = _buffer)
            {
                int length = _length;
                for (int i = 0; i < length; i++)
                {
                    if (start[i] == value) return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Returns true if the buffer starts with the given string.
        /// </summary>
        public bool StartsWith(string value)
        {
            if (value == null) throw new ArgumentNullException(nameof(value));
            if (_length < value.Length) return false;
            return SubstringEquals(value, startIndex: 0, count: value.Length);
        }

        /// <summary>
        /// Returns true if the specified StringBuffer substring equals the given value.
        /// </summary>
        /// <param name="value">The value to compare against the specified substring.</param>
        /// <param name="startIndex">Start index of the sub string.</param>
        /// <param name="count">Length of the substring, or -1 to check all remaining.</param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Thrown if <paramref name="startIndex"/> or <paramref name="count"/> are outside the range
        /// of the buffer's length.
        /// </exception>
        public unsafe bool SubstringEquals(string value, int startIndex = 0, int count = -1)
        {
            if (value == null) return false;
            if (count < -1) throw new ArgumentOutOfRangeException(nameof(count));
            if (startIndex > _length) throw new ArgumentOutOfRangeException(nameof(startIndex));

            int realCount = count == -1 ? _length - startIndex : (int)count;
            if (checked(startIndex + realCount) > _length) throw new ArgumentOutOfRangeException(nameof(count));

            int length = value.Length;

            // Check the substring length against the input length
            if (realCount != length) return false;

            fixed (char* valueStart = value)
            fixed (char* bufferStart = _buffer)
            {
                char* subStringStart = bufferStart + startIndex;

                for (int i = 0; i < length; i++)
                {
                    if (subStringStart[i] != valueStart[i]) return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Append the given buffer.
        /// </summary>
        /// <param name="value">The buffer to append.</param>
        /// <param name="startIndex">The index in the input buffer to start appending from.</param>
        /// <param name="count">The count of characters to copy from the buffer string.</param>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Thrown if <paramref name="startIndex"/> or <paramref name="count"/> are outside the range
        /// of <paramref name="value"/> characters.
        /// </exception>
        public void Append(ref StringBuffer value, int startIndex = 0)
        {
            if (value.Length == 0) return;

            value.CopyTo(
                bufferIndex: startIndex,
                destination: ref this,
                destinationIndex: _length,
                count: value.Length);
        }

        /// <summary>
        /// Append the given buffer.
        /// </summary>
        /// <param name="value">The buffer to append.</param>
        /// <param name="startIndex">The index in the input buffer to start appending from.</param>
        /// <param name="count">The count of characters to copy from the buffer string.</param>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is null.</exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Thrown if <paramref name="startIndex"/> or <paramref name="count"/> are outside the range
        /// of <paramref name="value"/> characters.
        /// </exception>
        public void Append(ref StringBuffer value, int startIndex, int count)
        {
            if (count == 0) return;

            value.CopyTo(
                bufferIndex: startIndex,
                destination: ref this,
                destinationIndex: _length,
                count: count);
        }

        /// <summary>
        /// Copy contents to the specified buffer. Destination index must be within current destination length.
        /// Will grow the destination buffer if needed.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Thrown if <paramref name="bufferIndex"/> or <paramref name="destinationIndex"/> or <paramref name="count"/> are outside the range
        /// of <paramref name="value"/> characters.
        /// </exception>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="destination"/> is null.</exception>
        public void CopyTo(int bufferIndex, ref StringBuffer destination, int destinationIndex, int count)
        {
            if (destinationIndex > destination._length) throw new ArgumentOutOfRangeException(nameof(destinationIndex));
            if (bufferIndex >= _length) throw new ArgumentOutOfRangeException(nameof(bufferIndex));
            if (_length < checked(bufferIndex + count)) throw new ArgumentOutOfRangeException(nameof(count));

            if (count == 0) return;
            int lastIndex = checked(destinationIndex + count);
            if (destination.Length < lastIndex) destination.Length = lastIndex;

            Array.Copy(UnderlyingArray, bufferIndex, destination.UnderlyingArray, destinationIndex, count);
        }

        /// <summary>
        /// Copy contents from the specified string into the buffer at the given index. Start index must be within the current length of
        /// the buffer, will grow as necessary.
        /// </summary>
        public void CopyFrom(int bufferIndex, string source, int sourceIndex = 0, int count = -1)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            if (bufferIndex > _length) throw new ArgumentOutOfRangeException(nameof(bufferIndex));
            if (sourceIndex < 0 || sourceIndex > source.Length) throw new ArgumentOutOfRangeException(nameof(sourceIndex));
            if (count == -1) count = source.Length - sourceIndex;
            if (count < 0 || source.Length - count < sourceIndex) throw new ArgumentOutOfRangeException(nameof(count));

            if (count == 0) return;
            int lastIndex = bufferIndex + (int)count;
            if (_length < lastIndex) Length = lastIndex;

            source.CopyTo(sourceIndex, UnderlyingArray, bufferIndex, count);
        }

        /// <summary>
        /// Trim the specified values from the end of the buffer. If nothing is specified, nothing is trimmed.
        /// </summary>
        public void TrimEnd(char[] values)
        {
            if (values == null || values.Length == 0 || _length == 0) return;

            while (_length > 0 && Array.IndexOf(values, _buffer[_length - 1]) >= 0)
            {
                Length = _length - 1;
            }
        }

        /// <summary>
        /// String representation of the entire buffer. If the buffer is larger than the maximum size string (int.MaxValue) this will throw.
        /// </summary>
        /// <exception cref="InvalidOperationException">Thrown if the buffer is too big to fit into a string.</exception>
        public override string ToString()
        {
            return new string(_buffer, startIndex: 0, length: _length);
        }

        /// <summary>
        /// Get the given substring in the buffer.
        /// </summary>
        /// <param name="count">Count of characters to take, or remaining characters from <paramref name="startIndex"/> if -1.</param>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Thrown if <paramref name="startIndex"/> or <paramref name="count"/> are outside the range of the buffer's length
        /// or count is greater than the maximum string size (int.MaxValue).
        /// </exception>
        public string Substring(int startIndex, int count = -1)
        {
            if (startIndex > (_length == 0 ? 0 : _length - 1)) throw new ArgumentOutOfRangeException(nameof(startIndex));
            if (count < -1) throw new ArgumentOutOfRangeException(nameof(count));

            int realCount = count == -1 ? _length - startIndex : (int)count;
            if (realCount > int.MaxValue || checked(startIndex + realCount) > _length) throw new ArgumentOutOfRangeException(nameof(count));

            // The buffer could be bigger than will fit into a string, but the substring might fit. As the starting
            // index might be bigger than int we need to index ourselves.
            return new string(_buffer, startIndex: startIndex, length: realCount);
        }

        public void Free()
        {
            ArrayPool<char>.Shared.Return(_buffer);
            _buffer = null;
            _length = 0;
        }
    }
}