1 | //===- llvm/unittest/Support/MemoryBufferTest.cpp - MemoryBuffer tests ----===// |
2 | // |
3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | // See https://llvm.org/LICENSE.txt for license information. |
5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | // |
9 | // This file implements unit tests for the MemoryBuffer support class. |
10 | // |
11 | //===----------------------------------------------------------------------===// |
12 | |
13 | #include "llvm/Support/MemoryBuffer.h" |
14 | #include "llvm/Support/SmallVectorMemoryBuffer.h" |
15 | #include "llvm/ADT/ScopeExit.h" |
16 | #include "llvm/Support/FileSystem.h" |
17 | #include "llvm/Support/FileUtilities.h" |
18 | #include "llvm/Support/Process.h" |
19 | #include "llvm/Support/raw_ostream.h" |
20 | #include "llvm/Testing/Support/Error.h" |
21 | #include "gtest/gtest.h" |
22 | #if LLVM_ENABLE_THREADS |
23 | #include <thread> |
24 | #endif |
25 | #if LLVM_ON_UNIX |
26 | #include <unistd.h> |
27 | #endif |
28 | #if _WIN32 |
29 | #include <windows.h> |
30 | #endif |
31 | |
32 | using namespace llvm; |
33 | |
34 | #define ASSERT_NO_ERROR(x) \ |
35 | if (std::error_code ASSERT_NO_ERROR_ec = x) { \ |
36 | SmallString<128> MessageStorage; \ |
37 | raw_svector_ostream Message(MessageStorage); \ |
38 | Message << #x ": did not return errc::success.\n" \ |
39 | << "error number: " << ASSERT_NO_ERROR_ec.value() << "\n" \ |
40 | << "error message: " << ASSERT_NO_ERROR_ec.message() << "\n"; \ |
41 | GTEST_FATAL_FAILURE_(MessageStorage.c_str()); \ |
42 | } else { \ |
43 | } |
44 | |
45 | #define ASSERT_ERROR(x) \ |
46 | if (!x) { \ |
47 | SmallString<128> MessageStorage; \ |
48 | raw_svector_ostream Message(MessageStorage); \ |
49 | Message << #x ": did not return a failure error code.\n"; \ |
50 | GTEST_FATAL_FAILURE_(MessageStorage.c_str()); \ |
51 | } |
52 | |
53 | namespace { |
54 | |
55 | class MemoryBufferTest : public testing::Test { |
56 | protected: |
57 | MemoryBufferTest() |
58 | : data("this is some data" ) |
59 | { } |
60 | |
61 | void SetUp() override {} |
62 | |
63 | /// Common testing for different modes of getOpenFileSlice. |
64 | /// Creates a temporary file with known contents, and uses |
65 | /// MemoryBuffer::getOpenFileSlice to map it. |
66 | /// If \p Reopen is true, the file is closed after creating and reopened |
67 | /// anew before using MemoryBuffer. |
68 | void testGetOpenFileSlice(bool Reopen); |
69 | |
70 | typedef std::unique_ptr<MemoryBuffer> OwningBuffer; |
71 | |
72 | std::string data; |
73 | }; |
74 | |
75 | TEST_F(MemoryBufferTest, get) { |
76 | // Default name and null-terminator flag |
77 | OwningBuffer MB1(MemoryBuffer::getMemBuffer(InputData: data)); |
78 | EXPECT_NE(nullptr, MB1.get()); |
79 | |
80 | // RequiresNullTerminator = false |
81 | OwningBuffer MB2(MemoryBuffer::getMemBuffer(InputData: data, BufferName: "one" , RequiresNullTerminator: false)); |
82 | EXPECT_NE(nullptr, MB2.get()); |
83 | |
84 | // RequiresNullTerminator = true |
85 | OwningBuffer MB3(MemoryBuffer::getMemBuffer(InputData: data, BufferName: "two" , RequiresNullTerminator: true)); |
86 | EXPECT_NE(nullptr, MB3.get()); |
87 | |
88 | // verify all 3 buffers point to the same address |
89 | EXPECT_EQ(MB1->getBufferStart(), MB2->getBufferStart()); |
90 | EXPECT_EQ(MB2->getBufferStart(), MB3->getBufferStart()); |
91 | |
92 | // verify the original data is unmodified after deleting the buffers |
93 | MB1.reset(); |
94 | MB2.reset(); |
95 | MB3.reset(); |
96 | EXPECT_EQ("this is some data" , data); |
97 | } |
98 | |
99 | TEST_F(MemoryBufferTest, getOpenFile) { |
100 | int FD; |
101 | SmallString<64> TestPath; |
102 | ASSERT_EQ(sys::fs::createTemporaryFile("MemoryBufferTest_getOpenFile" , "temp" , |
103 | FD, TestPath), |
104 | std::error_code()); |
105 | |
106 | FileRemover Cleanup(TestPath); |
107 | raw_fd_ostream OF(FD, /*shouldClose*/ true); |
108 | OF << "12345678" ; |
109 | OF.close(); |
110 | |
111 | { |
112 | Expected<sys::fs::file_t> File = sys::fs::openNativeFileForRead(Name: TestPath); |
113 | ASSERT_THAT_EXPECTED(File, Succeeded()); |
114 | auto OnExit = |
115 | make_scope_exit(F: [&] { ASSERT_NO_ERROR(sys::fs::closeFile(*File)); }); |
116 | ErrorOr<OwningBuffer> MB = MemoryBuffer::getOpenFile(FD: *File, Filename: TestPath, FileSize: 6); |
117 | ASSERT_NO_ERROR(MB.getError()); |
118 | EXPECT_EQ("123456" , MB.get()->getBuffer()); |
119 | } |
120 | { |
121 | Expected<sys::fs::file_t> File = sys::fs::openNativeFileForWrite( |
122 | Name: TestPath, Disp: sys::fs::CD_OpenExisting, Flags: sys::fs::OF_None); |
123 | ASSERT_THAT_EXPECTED(File, Succeeded()); |
124 | auto OnExit = |
125 | make_scope_exit(F: [&] { ASSERT_NO_ERROR(sys::fs::closeFile(*File)); }); |
126 | ASSERT_ERROR(MemoryBuffer::getOpenFile(*File, TestPath, 6).getError()); |
127 | } |
128 | } |
129 | |
130 | TEST_F(MemoryBufferTest, NullTerminator4K) { |
131 | // Test that a file with size that is a multiple of the page size can be null |
132 | // terminated correctly by MemoryBuffer. |
133 | int TestFD; |
134 | SmallString<64> TestPath; |
135 | sys::fs::createTemporaryFile(Prefix: "MemoryBufferTest_NullTerminator4K" , Suffix: "temp" , |
136 | ResultFD&: TestFD, ResultPath&: TestPath); |
137 | FileRemover Cleanup(TestPath); |
138 | raw_fd_ostream OF(TestFD, true, /*unbuffered=*/true); |
139 | for (unsigned i = 0; i < 4096 / 16; ++i) { |
140 | OF << "0123456789abcdef" ; |
141 | } |
142 | OF.close(); |
143 | |
144 | ErrorOr<OwningBuffer> MB = MemoryBuffer::getFile(Filename: TestPath.c_str()); |
145 | std::error_code EC = MB.getError(); |
146 | ASSERT_FALSE(EC); |
147 | |
148 | const char *BufData = MB.get()->getBufferStart(); |
149 | EXPECT_EQ('f', BufData[4095]); |
150 | EXPECT_EQ('\0', BufData[4096]); |
151 | } |
152 | |
153 | TEST_F(MemoryBufferTest, copy) { |
154 | // copy with no name |
155 | OwningBuffer MBC1(MemoryBuffer::getMemBufferCopy(InputData: data)); |
156 | EXPECT_NE(nullptr, MBC1.get()); |
157 | |
158 | // copy with a name |
159 | OwningBuffer MBC2(MemoryBuffer::getMemBufferCopy(InputData: data, BufferName: "copy" )); |
160 | EXPECT_NE(nullptr, MBC2.get()); |
161 | |
162 | // verify the two copies do not point to the same place |
163 | EXPECT_NE(MBC1->getBufferStart(), MBC2->getBufferStart()); |
164 | |
165 | // check that copies from defaulted stringrefs don't trigger UB. |
166 | OwningBuffer MBC3(MemoryBuffer::getMemBufferCopy(InputData: StringRef{})); |
167 | EXPECT_NE(nullptr, MBC3.get()); |
168 | } |
169 | |
170 | #if LLVM_ENABLE_THREADS |
171 | TEST_F(MemoryBufferTest, createFromPipe) { |
172 | sys::fs::file_t pipes[2]; |
173 | #if LLVM_ON_UNIX |
174 | ASSERT_EQ(::pipe(pipes), 0) << strerror(errno); |
175 | #else |
176 | ASSERT_TRUE(::CreatePipe(&pipes[0], &pipes[1], nullptr, 0)) |
177 | << ::GetLastError(); |
178 | #endif |
179 | auto ReadCloser = make_scope_exit(F: [&] { sys::fs::closeFile(F&: pipes[0]); }); |
180 | std::thread Writer([&] { |
181 | auto WriteCloser = make_scope_exit(F: [&] { sys::fs::closeFile(F&: pipes[1]); }); |
182 | for (unsigned i = 0; i < 5; ++i) { |
183 | std::this_thread::sleep_for(rtime: std::chrono::milliseconds(10)); |
184 | #if LLVM_ON_UNIX |
185 | ASSERT_EQ(::write(pipes[1], "foo" , 3), 3) << strerror(errno); |
186 | #else |
187 | DWORD Written; |
188 | ASSERT_TRUE(::WriteFile(pipes[1], "foo" , 3, &Written, nullptr)) |
189 | << ::GetLastError(); |
190 | ASSERT_EQ(Written, 3u); |
191 | #endif |
192 | } |
193 | }); |
194 | ErrorOr<OwningBuffer> MB = |
195 | MemoryBuffer::getOpenFile(FD: pipes[0], Filename: "pipe" , /*FileSize*/ -1); |
196 | Writer.join(); |
197 | ASSERT_NO_ERROR(MB.getError()); |
198 | EXPECT_EQ(MB.get()->getBuffer(), "foofoofoofoofoo" ); |
199 | } |
200 | #endif |
201 | |
202 | TEST_F(MemoryBufferTest, make_new) { |
203 | // 0-sized buffer |
204 | OwningBuffer Zero(WritableMemoryBuffer::getNewUninitMemBuffer(Size: 0)); |
205 | EXPECT_NE(nullptr, Zero.get()); |
206 | |
207 | // uninitialized buffer with no name |
208 | OwningBuffer One(WritableMemoryBuffer::getNewUninitMemBuffer(Size: 321)); |
209 | EXPECT_NE(nullptr, One.get()); |
210 | |
211 | // uninitialized buffer with name |
212 | OwningBuffer Two(WritableMemoryBuffer::getNewUninitMemBuffer(Size: 123, BufferName: "bla" )); |
213 | EXPECT_NE(nullptr, Two.get()); |
214 | |
215 | // 0-initialized buffer with no name |
216 | OwningBuffer Three(WritableMemoryBuffer::getNewMemBuffer(Size: 321, BufferName: data)); |
217 | EXPECT_NE(nullptr, Three.get()); |
218 | for (size_t i = 0; i < 321; ++i) |
219 | EXPECT_EQ(0, Three->getBufferStart()[0]); |
220 | |
221 | // 0-initialized buffer with name |
222 | OwningBuffer Four(WritableMemoryBuffer::getNewMemBuffer(Size: 123, BufferName: "zeros" )); |
223 | EXPECT_NE(nullptr, Four.get()); |
224 | for (size_t i = 0; i < 123; ++i) |
225 | EXPECT_EQ(0, Four->getBufferStart()[0]); |
226 | |
227 | // uninitialized buffer with rollover size |
228 | OwningBuffer Five( |
229 | WritableMemoryBuffer::getNewUninitMemBuffer(SIZE_MAX, BufferName: "huge" )); |
230 | EXPECT_EQ(nullptr, Five.get()); |
231 | } |
232 | |
233 | TEST_F(MemoryBufferTest, getNewAligned) { |
234 | auto CheckAlignment = [](size_t AlignmentValue) { |
235 | Align Alignment(AlignmentValue); |
236 | OwningBuffer AlignedBuffer = |
237 | WritableMemoryBuffer::getNewUninitMemBuffer(Size: 0, BufferName: "" , Alignment); |
238 | EXPECT_TRUE(isAddrAligned(Alignment, AlignedBuffer->getBufferStart())); |
239 | }; |
240 | |
241 | // Test allocation with different alignments. |
242 | CheckAlignment(16); |
243 | CheckAlignment(32); |
244 | CheckAlignment(64); |
245 | CheckAlignment(128); |
246 | CheckAlignment(256); |
247 | } |
248 | |
249 | void MemoryBufferTest::testGetOpenFileSlice(bool Reopen) { |
250 | // Test that MemoryBuffer::getOpenFile works properly when no null |
251 | // terminator is requested and the size is large enough to trigger |
252 | // the usage of memory mapping. |
253 | int TestFD; |
254 | SmallString<64> TestPath; |
255 | // Create a temporary file and write data into it. |
256 | sys::fs::createTemporaryFile(Prefix: "prefix" , Suffix: "temp" , ResultFD&: TestFD, ResultPath&: TestPath); |
257 | FileRemover Cleanup(TestPath); |
258 | // OF is responsible for closing the file; If the file is not |
259 | // reopened, it will be unbuffered so that the results are |
260 | // immediately visible through the fd. |
261 | raw_fd_ostream OF(TestFD, true, !Reopen); |
262 | for (int i = 0; i < 60000; ++i) { |
263 | OF << "0123456789" ; |
264 | } |
265 | |
266 | if (Reopen) { |
267 | OF.close(); |
268 | EXPECT_FALSE(sys::fs::openFileForRead(TestPath.c_str(), TestFD)); |
269 | } |
270 | |
271 | ErrorOr<OwningBuffer> Buf = MemoryBuffer::getOpenFileSlice( |
272 | FD: sys::fs::convertFDToNativeFile(FD: TestFD), Filename: TestPath.c_str(), |
273 | MapSize: 40000, // Size |
274 | Offset: 80000 // Offset |
275 | ); |
276 | |
277 | std::error_code EC = Buf.getError(); |
278 | EXPECT_FALSE(EC); |
279 | |
280 | StringRef BufData = Buf.get()->getBuffer(); |
281 | EXPECT_EQ(BufData.size(), 40000U); |
282 | EXPECT_EQ(BufData[0], '0'); |
283 | EXPECT_EQ(BufData[9], '9'); |
284 | } |
285 | |
286 | TEST_F(MemoryBufferTest, getOpenFileNoReopen) { |
287 | testGetOpenFileSlice(Reopen: false); |
288 | } |
289 | |
290 | TEST_F(MemoryBufferTest, getOpenFileReopened) { |
291 | testGetOpenFileSlice(Reopen: true); |
292 | } |
293 | |
294 | TEST_F(MemoryBufferTest, slice) { |
295 | // Create a file that is six pages long with different data on each page. |
296 | int FD; |
297 | SmallString<64> TestPath; |
298 | sys::fs::createTemporaryFile(Prefix: "MemoryBufferTest_Slice" , Suffix: "temp" , ResultFD&: FD, ResultPath&: TestPath); |
299 | FileRemover Cleanup(TestPath); |
300 | raw_fd_ostream OF(FD, true, /*unbuffered=*/true); |
301 | for (unsigned i = 0; i < 0x2000 / 8; ++i) { |
302 | OF << "12345678" ; |
303 | } |
304 | for (unsigned i = 0; i < 0x2000 / 8; ++i) { |
305 | OF << "abcdefgh" ; |
306 | } |
307 | for (unsigned i = 0; i < 0x2000 / 8; ++i) { |
308 | OF << "ABCDEFGH" ; |
309 | } |
310 | OF.close(); |
311 | |
312 | // Try offset of one page. |
313 | ErrorOr<OwningBuffer> MB = MemoryBuffer::getFileSlice(Filename: TestPath.str(), |
314 | MapSize: 0x4000, Offset: 0x1000); |
315 | std::error_code EC = MB.getError(); |
316 | ASSERT_FALSE(EC); |
317 | EXPECT_EQ(0x4000UL, MB.get()->getBufferSize()); |
318 | |
319 | StringRef BufData = MB.get()->getBuffer(); |
320 | EXPECT_TRUE(BufData.substr(0x0000,8).equals("12345678" )); |
321 | EXPECT_TRUE(BufData.substr(0x0FF8,8).equals("12345678" )); |
322 | EXPECT_TRUE(BufData.substr(0x1000,8).equals("abcdefgh" )); |
323 | EXPECT_TRUE(BufData.substr(0x2FF8,8).equals("abcdefgh" )); |
324 | EXPECT_TRUE(BufData.substr(0x3000,8).equals("ABCDEFGH" )); |
325 | EXPECT_TRUE(BufData.substr(0x3FF8,8).equals("ABCDEFGH" )); |
326 | |
327 | // Try non-page aligned. |
328 | ErrorOr<OwningBuffer> MB2 = MemoryBuffer::getFileSlice(Filename: TestPath.str(), |
329 | MapSize: 0x3000, Offset: 0x0800); |
330 | EC = MB2.getError(); |
331 | ASSERT_FALSE(EC); |
332 | EXPECT_EQ(0x3000UL, MB2.get()->getBufferSize()); |
333 | |
334 | StringRef BufData2 = MB2.get()->getBuffer(); |
335 | EXPECT_TRUE(BufData2.substr(0x0000,8).equals("12345678" )); |
336 | EXPECT_TRUE(BufData2.substr(0x17F8,8).equals("12345678" )); |
337 | EXPECT_TRUE(BufData2.substr(0x1800,8).equals("abcdefgh" )); |
338 | EXPECT_TRUE(BufData2.substr(0x2FF8,8).equals("abcdefgh" )); |
339 | } |
340 | |
341 | TEST_F(MemoryBufferTest, writableSlice) { |
342 | // Create a file initialized with some data |
343 | int FD; |
344 | SmallString<64> TestPath; |
345 | sys::fs::createTemporaryFile(Prefix: "MemoryBufferTest_WritableSlice" , Suffix: "temp" , ResultFD&: FD, |
346 | ResultPath&: TestPath); |
347 | FileRemover Cleanup(TestPath); |
348 | raw_fd_ostream OF(FD, true); |
349 | for (unsigned i = 0; i < 0x1000; ++i) |
350 | OF << "0123456789abcdef" ; |
351 | OF.close(); |
352 | |
353 | { |
354 | auto MBOrError = |
355 | WritableMemoryBuffer::getFileSlice(Filename: TestPath.str(), MapSize: 0x6000, Offset: 0x2000); |
356 | ASSERT_FALSE(MBOrError.getError()); |
357 | // Write some data. It should be mapped private, so that upon completion |
358 | // the original file contents are not modified. |
359 | WritableMemoryBuffer &MB = **MBOrError; |
360 | ASSERT_EQ(0x6000u, MB.getBufferSize()); |
361 | char *Start = MB.getBufferStart(); |
362 | ASSERT_EQ(MB.getBufferEnd(), MB.getBufferStart() + MB.getBufferSize()); |
363 | ::memset(s: Start, c: 'x', n: MB.getBufferSize()); |
364 | } |
365 | |
366 | auto MBOrError = MemoryBuffer::getFile(Filename: TestPath); |
367 | ASSERT_FALSE(MBOrError.getError()); |
368 | auto &MB = **MBOrError; |
369 | ASSERT_EQ(0x10000u, MB.getBufferSize()); |
370 | for (size_t i = 0; i < MB.getBufferSize(); i += 0x10) |
371 | EXPECT_EQ("0123456789abcdef" , MB.getBuffer().substr(i, 0x10)) << "i: " << i; |
372 | } |
373 | |
374 | TEST_F(MemoryBufferTest, writeThroughFile) { |
375 | // Create a file initialized with some data |
376 | int FD; |
377 | SmallString<64> TestPath; |
378 | sys::fs::createTemporaryFile(Prefix: "MemoryBufferTest_WriteThrough" , Suffix: "temp" , ResultFD&: FD, |
379 | ResultPath&: TestPath); |
380 | FileRemover Cleanup(TestPath); |
381 | raw_fd_ostream OF(FD, true); |
382 | OF << "0123456789abcdef" ; |
383 | OF.close(); |
384 | { |
385 | auto MBOrError = WriteThroughMemoryBuffer::getFile(Filename: TestPath); |
386 | ASSERT_FALSE(MBOrError.getError()); |
387 | // Write some data. It should be mapped readwrite, so that upon completion |
388 | // the original file contents are modified. |
389 | WriteThroughMemoryBuffer &MB = **MBOrError; |
390 | ASSERT_EQ(16u, MB.getBufferSize()); |
391 | char *Start = MB.getBufferStart(); |
392 | ASSERT_EQ(MB.getBufferEnd(), MB.getBufferStart() + MB.getBufferSize()); |
393 | ::memset(s: Start, c: 'x', n: MB.getBufferSize()); |
394 | } |
395 | |
396 | auto MBOrError = MemoryBuffer::getFile(Filename: TestPath); |
397 | ASSERT_FALSE(MBOrError.getError()); |
398 | auto &MB = **MBOrError; |
399 | ASSERT_EQ(16u, MB.getBufferSize()); |
400 | EXPECT_EQ("xxxxxxxxxxxxxxxx" , MB.getBuffer()); |
401 | } |
402 | |
403 | TEST_F(MemoryBufferTest, mmapVolatileNoNull) { |
404 | // Verify that `MemoryBuffer::getOpenFile` will use mmap when |
405 | // `RequiresNullTerminator = false`, `IsVolatile = true`, and the file is |
406 | // large enough to use mmap. |
407 | // |
408 | // This is done because Clang should use this mode to open module files, and |
409 | // falling back to malloc for them causes a huge memory usage increase. |
410 | |
411 | int FD; |
412 | SmallString<64> TestPath; |
413 | ASSERT_NO_ERROR(sys::fs::createTemporaryFile( |
414 | "MemoryBufferTest_mmapVolatileNoNull" , "temp" , FD, TestPath)); |
415 | FileRemover Cleanup(TestPath); |
416 | raw_fd_ostream OF(FD, true); |
417 | // Create a file large enough to mmap. 4 pages should be enough. |
418 | unsigned PageSize = sys::Process::getPageSizeEstimate(); |
419 | unsigned FileWrites = (PageSize * 4) / 8; |
420 | for (unsigned i = 0; i < FileWrites; ++i) |
421 | OF << "01234567" ; |
422 | OF.close(); |
423 | |
424 | Expected<sys::fs::file_t> File = sys::fs::openNativeFileForRead(Name: TestPath); |
425 | ASSERT_THAT_EXPECTED(File, Succeeded()); |
426 | auto OnExit = |
427 | make_scope_exit(F: [&] { ASSERT_NO_ERROR(sys::fs::closeFile(*File)); }); |
428 | |
429 | auto MBOrError = MemoryBuffer::getOpenFile(FD: *File, Filename: TestPath, |
430 | /*FileSize=*/-1, /*RequiresNullTerminator=*/false, /*IsVolatile=*/true); |
431 | ASSERT_NO_ERROR(MBOrError.getError()) |
432 | OwningBuffer MB = std::move(*MBOrError); |
433 | EXPECT_EQ(MB->getBufferKind(), MemoryBuffer::MemoryBuffer_MMap); |
434 | EXPECT_EQ(MB->getBufferSize(), std::size_t(FileWrites * 8)); |
435 | EXPECT_TRUE(MB->getBuffer().starts_with("01234567" )); |
436 | } |
437 | |
438 | // Test that SmallVector without a null terminator gets one. |
439 | TEST(SmallVectorMemoryBufferTest, WithoutNullTerminatorRequiresNullTerminator) { |
440 | SmallString<0> Data("some data" ); |
441 | |
442 | SmallVectorMemoryBuffer MB(std::move(Data), |
443 | /*RequiresNullTerminator=*/true); |
444 | EXPECT_EQ(MB.getBufferSize(), 9u); |
445 | EXPECT_EQ(MB.getBufferEnd()[0], '\0'); |
446 | } |
447 | |
448 | // Test that SmallVector with a null terminator keeps it. |
449 | TEST(SmallVectorMemoryBufferTest, WithNullTerminatorRequiresNullTerminator) { |
450 | SmallString<0> Data("some data" ); |
451 | Data.push_back(Elt: '\0'); |
452 | Data.pop_back(); |
453 | |
454 | SmallVectorMemoryBuffer MB(std::move(Data), |
455 | /*RequiresNullTerminator=*/true); |
456 | EXPECT_EQ(MB.getBufferSize(), 9u); |
457 | EXPECT_EQ(MB.getBufferEnd()[0], '\0'); |
458 | } |
459 | |
460 | } // namespace |
461 | |