-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmappedfile.cpp
More file actions
603 lines (537 loc) · 22.5 KB
/
mappedfile.cpp
File metadata and controls
603 lines (537 loc) · 22.5 KB
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
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
// Copyright (c) 2024-2025 Pyarelal Knowles, MIT License
#include <algorithm>
#include <cstdio>
#include <decodeless/mappedfile.hpp>
#include <filesystem>
#include <fstream>
#include <gtest/gtest.h>
#include <ostream>
#include <span>
using namespace decodeless;
class MappedFileFixture : public ::testing::Test {
protected:
void SetUp() override {
std::ofstream ofile(m_tmpFile, std::ios::binary);
int d = 42;
ofile.write(reinterpret_cast<char*>(&d), sizeof(d));
}
void TearDown() override { fs::remove(m_tmpFile); }
fs::path m_tmpFile = fs::path{testing::TempDir()} / "test.dat";
};
TEST_F(MappedFileFixture, ReadOnly) {
file mapped(m_tmpFile);
EXPECT_EQ(*static_cast<const int*>(mapped.data()), 42);
}
TEST_F(MappedFileFixture, Writable) {
{
writable_file mapped(m_tmpFile);
ASSERT_GE(mapped.size(), sizeof(int));
*static_cast<int*>(mapped.data()) = 123;
}
{
std::ifstream ifile(m_tmpFile, std::ios::binary);
int contents;
ifile.read(reinterpret_cast<char*>(&contents), sizeof(contents));
EXPECT_EQ(contents, 123);
}
}
TEST_F(MappedFileFixture, WritableSync) {
writable_file mapped(m_tmpFile);
ASSERT_GE(mapped.size(), sizeof(int));
*static_cast<int*>(mapped.data()) = 123;
mapped.sync();
{
// This test will always pass even without the .sync() as it just reads the same pages in
// memory
std::ifstream ifile(m_tmpFile, std::ios::binary);
int contents;
ifile.read(reinterpret_cast<char*>(&contents), sizeof(contents));
EXPECT_EQ(contents, 123);
}
}
#ifdef _WIN32
TEST_F(MappedFileFixture, FileHandle) {
detail::FileHandle file(m_tmpFile, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr);
EXPECT_TRUE(file); // a bit pointless - would have thrown if not
}
TEST(MappedFile, Create) {
fs::path tmpFile2 = fs::path{testing::TempDir()} / "test2.dat";
EXPECT_FALSE(fs::exists(tmpFile2));
if (fs::exists(tmpFile2))
fs::remove(tmpFile2);
{
detail::FileHandle file(tmpFile2, GENERIC_READ | GENERIC_WRITE, 0, nullptr, CREATE_NEW,
FILE_ATTRIBUTE_NORMAL, nullptr);
EXPECT_EQ(file.size(), 0);
file.setPointer(sizeof(int));
file.setEndOfFile();
EXPECT_EQ(file.size(), sizeof(int));
detail::FileMappingHandle mapped(file, nullptr, PAGE_READWRITE, file.size());
detail::FileMappingView view(mapped, FILE_MAP_WRITE);
*reinterpret_cast<int*>(view.address()) = 42;
}
{
std::ifstream ifile(tmpFile2, std::ios::binary);
int contents;
ifile.read(reinterpret_cast<char*>(&contents), sizeof(contents));
EXPECT_EQ(contents, 42);
}
EXPECT_TRUE(fs::exists(tmpFile2));
fs::remove(tmpFile2);
EXPECT_FALSE(fs::exists(tmpFile2));
}
TEST(MappedFile, Reserve) {
fs::path tmpFile2 = fs::path{testing::TempDir()} / "test2.dat";
{
// Create a new file
detail::FileHandle file(tmpFile2, GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CREATE_NEW,
FILE_ATTRIBUTE_NORMAL, nullptr);
EXPECT_EQ(fs::file_size(tmpFile2), 0);
// https://stackoverflow.com/questions/44101966/adding-new-bytes-to-memory-mapped-file -
// thanks, RbMm!
size_t initialSize = sizeof(int);
detail::NtifsSection ntifs;
detail::Section<PAGE_READWRITE> section(
ntifs, SECTION_MAP_WRITE | SECTION_MAP_READ | SECTION_EXTEND_SIZE, 0, initialSize,
SEC_COMMIT, file);
EXPECT_EQ(fs::file_size(tmpFile2), initialSize);
// Reserve 1mb of memory and map the file
LONGLONG reserved = 1024 * 1024;
detail::SectionView<PAGE_READWRITE> view(ntifs, section,
detail::NtifsSection::CurrentProcess(), 0, 0, 0,
reserved, detail::ViewUnmap, MEM_RESERVE);
EXPECT_EQ(view.query().Type, MEM_MAPPED);
EXPECT_EQ(view.query().State, MEM_COMMIT);
EXPECT_EQ(view.query(detail::pageSize()).Type, MEM_MAPPED);
EXPECT_EQ(view.query(detail::pageSize()).State, MEM_RESERVE); // not COMMIT
// Write to it
*reinterpret_cast<int*>(view.address()) = 42;
// Resize the file
auto newSize = detail::pageSize() * 2 + 31;
section.extend(newSize);
EXPECT_EQ(fs::file_size(tmpFile2), newSize);
EXPECT_EQ(view.query(detail::pageSize()).Type, MEM_MAPPED);
EXPECT_EQ(view.query(detail::pageSize()).State, MEM_COMMIT); // now COMMIT
// Check the contents is still there and write to the new region
EXPECT_EQ(*reinterpret_cast<int*>(view.address()), 42);
reinterpret_cast<char*>(view.address())[section.size() - 1] = 'M';
}
fs::remove(tmpFile2);
}
// TODO: FILE_MAP_LARGE_PAGES
TEST_F(MappedFileFixture, WinAllocationGranulairty) {
SYSTEM_INFO info;
GetSystemInfo(&info);
EXPECT_GE(info.dwAllocationGranularity, sizeof(std::max_align_t));
EXPECT_GE(info.dwAllocationGranularity, 4096u);
}
#else
TEST_F(MappedFileFixture, LinuxFileDescriptor) {
detail::FileDescriptor fd(m_tmpFile, O_RDONLY);
EXPECT_NE(fd, -1);
}
TEST_F(MappedFileFixture, LinuxOvermapResizeWrite) {
size_t overmapSize = 1024 * 1024;
EXPECT_EQ(fs::file_size(m_tmpFile), sizeof(int));
{
detail::FileDescriptor fd(m_tmpFile, O_RDWR);
detail::MemoryMapRW mapped(nullptr, overmapSize, MAP_SHARED, fd, 0);
fd.truncate(overmapSize);
std::span data(reinterpret_cast<uint8_t*>(mapped.address()), mapped.size());
data.back() = 142;
}
EXPECT_EQ(fs::file_size(m_tmpFile), overmapSize);
{
std::ifstream ifile(m_tmpFile, std::ios::binary);
uint8_t lastByte;
ifile.seekg(overmapSize - sizeof(lastByte));
ifile.read(reinterpret_cast<char*>(&lastByte), sizeof(lastByte));
EXPECT_EQ(lastByte, 142);
}
}
TEST(MappedFile, LinuxCreate) {
fs::path tmpFile2 = fs::path{testing::TempDir()} / "test2.dat";
EXPECT_FALSE(fs::exists(tmpFile2));
{
detail::FileDescriptor fd(tmpFile2, O_CREAT | O_RDWR);
EXPECT_EQ(fd.size(), 0);
fd.truncate(sizeof(int));
EXPECT_EQ(fd.size(), sizeof(int));
detail::MemoryMap<PROT_READ | PROT_WRITE> mapped(nullptr, fd.size(), MAP_SHARED, fd, 0);
*reinterpret_cast<int*>(mapped.address()) = 42;
}
{
std::ifstream ifile(tmpFile2, std::ios::binary);
int contents;
ifile.read(reinterpret_cast<char*>(&contents), sizeof(contents));
EXPECT_EQ(contents, 42);
}
EXPECT_TRUE(fs::exists(tmpFile2));
fs::remove(tmpFile2);
EXPECT_FALSE(fs::exists(tmpFile2));
}
TEST_F(MappedFileFixture, LinuxReserve) {
detail::MemoryMap<PROT_NONE> reserved(nullptr, detail::pageSize() * 4,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
detail::FileDescriptor fd(m_tmpFile, O_RDONLY);
detail::MemoryMapRO mapped(reserved.address(), fd.size(), MAP_FIXED | MAP_SHARED_VALIDATE, fd,
0);
EXPECT_EQ(mapped.address(), reserved.address());
EXPECT_EQ(*reinterpret_cast<const int*>(mapped.address()), 42);
}
TEST(MappedFile, LinuxResize) {
// Reserve some virtual address space
detail::MemoryMap<PROT_NONE> reserved(nullptr, detail::pageSize() * 4,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// Create a new file
fs::path tmpFile2 = fs::path{testing::TempDir()} / "test2.dat";
detail::FileDescriptor fd(tmpFile2, O_CREAT | O_RDWR);
EXPECT_EQ(fs::file_size(tmpFile2), 0);
fd.truncate(sizeof(int));
EXPECT_EQ(fs::file_size(tmpFile2), fd.size());
// Map it to the reserved range and write to it
int* originalPointer;
{
detail::MemoryMapRW mapped(reserved.address(), fd.size(), MAP_FIXED | MAP_SHARED_VALIDATE,
fd, 0);
originalPointer = reinterpret_cast<int*>(mapped.address());
*originalPointer = 42;
}
// Verify trying to use the reserved address space doesn't work
void* addressUsed = reinterpret_cast<std::byte*>(reserved.address()) + detail::pageSize();
EXPECT_THROW(detail::MemoryMap<PROT_NONE>(addressUsed, detail::pageSize(),
MAP_FIXED_NOREPLACE | MAP_PRIVATE | MAP_ANONYMOUS, -1,
0),
mapping_error);
// Try to map the reserved range with a hint, but expect to get a different address
{
detail::MemoryMapRW differentReserved(addressUsed, detail::pageSize(),
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
EXPECT_NE(reserved.address(), differentReserved.address());
EXPECT_NE(addressUsed, differentReserved.address());
}
// Resize the file
auto newSize = detail::pageSize() * 2 + 31;
fd.truncate(newSize);
EXPECT_EQ(fs::file_size(tmpFile2), fd.size());
// Map it again to the same virtual address range, check the contents is
// still there and write to the new region
{
detail::MemoryMapRW mapped(reserved.address(), fd.size(), MAP_FIXED | MAP_SHARED_VALIDATE,
fd, 0);
EXPECT_EQ(originalPointer, reinterpret_cast<int*>(mapped.address()));
EXPECT_EQ(*originalPointer, 42);
reinterpret_cast<char*>(mapped.address())[mapped.size() - 1] = 'M';
}
// Validate the new region
{
EXPECT_EQ(fs::file_size(tmpFile2), newSize);
std::ifstream ifile(tmpFile2, std::ios::binary);
EXPECT_TRUE(ifile.good());
ifile.seekg(newSize - 1);
EXPECT_TRUE(ifile.good());
EXPECT_EQ(ifile.get(), 'M');
}
fs::remove(tmpFile2);
EXPECT_FALSE(fs::exists(tmpFile2));
}
std::vector<unsigned char> getResidency(void* base, size_t size) {
std::vector<unsigned char> result(size / getpagesize(), 0u);
int ret = mincore(base, size, result.data());
if (ret != 0)
throw detail::LastError();
return result;
}
TEST_F(MappedFileFixture, LinuxResidencyAfterTruncate) {
// Map the 1-int sized file to a much larger range
EXPECT_EQ(fs::file_size(m_tmpFile), sizeof(int));
size_t newSize = 16 * 1024 * 1024;
detail::FileDescriptor fd(m_tmpFile, O_RDWR);
detail::MemoryMap<PROT_READ | PROT_WRITE> mapped(nullptr, newSize, MAP_SHARED, fd, 0);
// Increase the file size to match the mapping and check no pages are
// resident yet. Actually the first page is, but at least the rest should be
// untouched.
fd.truncate(newSize);
EXPECT_TRUE(
std::ranges::all_of(getResidency(mapped.address(getpagesize()), newSize - getpagesize()),
[](unsigned char c) { return (c & 1u) == 0; }));
// Fill the conents of the file and confirm pages are allocated
std::ranges::fill(std::span(reinterpret_cast<uint8_t*>(mapped.address()), newSize),
uint8_t(0xff));
EXPECT_TRUE(std::ranges::all_of(getResidency(mapped.address(), newSize),
[](unsigned char c) { return (c & 1u) == 1; }));
// Empty the file and check those pages are no longer resident
fd.truncate(0);
EXPECT_TRUE(std::ranges::all_of(getResidency(mapped.address(), newSize),
[](unsigned char c) { return (c & 1u) == 0; }));
EXPECT_EQ(fs::file_size(m_tmpFile), 0);
}
TEST(MappedMemory, LinuxResidencyAfterDecommit) {
const size_t page_size = getpagesize();
const size_t reserve_size = page_size * 64; // 64 pages total
const size_t commit_size = page_size * 4; // We'll use 4 pages
// Reserve virtual address space (uncommitted, inaccessible)
void* base =
mmap(nullptr, reserve_size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
ASSERT_NE(base, MAP_FAILED) << "Failed to mmap reserved space";
EXPECT_TRUE(std::ranges::all_of(getResidency(base, commit_size),
[](unsigned char c) { return (c & 1u) == 0; }));
// Commit a portion with PROT_READ | PROT_WRITE
int prot_result = mprotect(base, commit_size, PROT_READ | PROT_WRITE);
ASSERT_EQ(prot_result, 0) << "Failed to mprotect committed region";
EXPECT_TRUE(std::ranges::all_of(getResidency(base, commit_size),
[](unsigned char c) { return (c & 1u) == 0; }));
// Touch the memory to ensure it's backed by RAM
std::span committed(static_cast<std::byte*>(base), commit_size);
std::ranges::fill(committed, std::byte(0xAB));
// Verify pages are resident using mincore
EXPECT_TRUE(std::ranges::all_of(getResidency(base, commit_size),
[](unsigned char c) { return (c & 1u) == 1; }));
// Decommit
#if 0
void* remap = mmap(base, commit_size, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED, -1, 0);
ASSERT_EQ(remap, base) << "Failed to remap to decommit pages";
#else
// See MADV_FREE discussion here: https://github.com/golang/go/issues/42330
prot_result = mprotect(base, commit_size, PROT_NONE);
ASSERT_EQ(prot_result, 0) << "Failed to mprotect committed region back to PROT_NONE";
int madvise_result = madvise(base, commit_size, MADV_DONTNEED);
ASSERT_EQ(madvise_result, 0) << "Failed to release pages with madvise";
#endif
EXPECT_TRUE(std::ranges::all_of(getResidency(base, commit_size),
[](uint8_t c) { return (c & 1u) == 0; }));
// Cleanup
munmap(base, reserve_size);
}
#endif
TEST(MappedMemory, ResizeMemory) {
const char str[] = "hello world!";
{
resizable_memory file(0, 10000);
EXPECT_EQ(file.data(), nullptr);
EXPECT_EQ(file.size(), 0);
#ifdef _WIN32
EXPECT_THROW(file.resize(10001), std::bad_alloc);
#else
EXPECT_THROW(file.resize(10001), std::bad_alloc);
#endif
file.resize(13);
EXPECT_EQ(file.size(), 13);
std::span fileStr(reinterpret_cast<char*>(file.data()), std::size(str));
std::ranges::copy(str, fileStr.begin());
EXPECT_TRUE(std::ranges::equal(str, fileStr));
void* before = file.data();
file.resize(1500);
EXPECT_EQ(file.size(), 1500);
EXPECT_EQ(file.data(), before);
EXPECT_TRUE(std::ranges::equal(str, fileStr));
file.resize(10000);
EXPECT_EQ(file.size(), 10000);
EXPECT_EQ(file.data(), before);
EXPECT_TRUE(std::ranges::equal(str, fileStr));
std::ranges::copy(std::string("EOF"),
reinterpret_cast<char*>(file.data()) + file.size() - 3);
// Test the move assignment operator. On linux the file is briefly
// mapped twice. Windows gets an error ("The requested operation cannot
// be performed on a file with a user-mapped section open.") so another
// temporary file is mapped.
#ifdef _WIN32
file = resizable_memory(0, 1500);
EXPECT_NE(file.data(), before);
#endif
file = resizable_memory(0, 1500);
#ifndef _WIN32
EXPECT_NE(file.data(), before);
#endif
}
}
TEST(MappedMemory, ResizeMemoryExtended) {
size_t nextBytes = 1;
resizable_memory memory(nextBytes, 1llu << 32); // 4gb of virtual memory pls
void* data = memory.data();
uint8_t* bytes = reinterpret_cast<uint8_t*>(data);
bytes[0] = 1;
// Grow
while ((nextBytes *= 2) <= 256 * 1024 * 1024) {
EXPECT_NO_THROW(memory.resize(nextBytes));
EXPECT_EQ(data, memory.data());
uint8_t b = 0;
// Verify memory from previous resizes remains intact
for (size_t i = 1; i < nextBytes; i *= 2)
EXPECT_EQ(bytes[i - 1], ++b);
bytes[nextBytes - 1] = ++b;
}
// Shrink
while ((nextBytes /= 2) > 1) {
EXPECT_NO_THROW(memory.resize(nextBytes));
EXPECT_EQ(data, memory.data());
uint8_t b = 0;
// Verify memory from previous resizes remains intact
for (size_t i = 1; i < nextBytes; i *= 2)
EXPECT_EQ(bytes[i - 1], ++b);
}
}
TEST_F(MappedFileFixture, ResizeFile) {
fs::path tmpFile2 = fs::path{testing::TempDir()} / "test2.dat";
const char str[] = "hello world!";
{
resizable_file file(tmpFile2, 10000);
EXPECT_EQ(file.data(), nullptr);
EXPECT_EQ(file.size(), 0);
EXPECT_THROW(file.resize(10001), std::bad_alloc);
file.resize(13);
EXPECT_EQ(file.size(), 13);
std::span fileStr(reinterpret_cast<char*>(file.data()), std::size(str));
std::ranges::copy(str, fileStr.begin());
EXPECT_TRUE(std::ranges::equal(str, fileStr));
void* before = file.data();
file.resize(1500);
EXPECT_EQ(file.size(), 1500);
EXPECT_EQ(file.data(), before);
EXPECT_TRUE(std::ranges::equal(str, fileStr));
file.resize(10000);
EXPECT_EQ(file.size(), 10000);
EXPECT_EQ(file.data(), before);
EXPECT_TRUE(std::ranges::equal(str, fileStr));
std::ranges::copy(std::string("EOF"),
reinterpret_cast<char*>(file.data()) + file.size() - 3);
// Test the move assignment operator. On linux the file is briefly
// mapped twice. Windows gets an error ("The requested operation cannot
// be performed on a file with a user-mapped section open.") so another
// temporary file is mapped.
#ifdef _WIN32
file = resizable_file(m_tmpFile, 10000);
EXPECT_NE(file.data(), before);
#endif
file = resizable_file(tmpFile2, 10000);
#ifndef _WIN32
EXPECT_NE(file.data(), before);
#endif
EXPECT_EQ(file.size(), 10000);
std::span eof(reinterpret_cast<char*>(file.data()) + file.size() - 3, 3);
EXPECT_TRUE(std::ranges::equal(std::string("EOF"), eof));
}
EXPECT_THROW((void)resizable_file(tmpFile2, 1499), std::bad_alloc);
{
resizable_file file(tmpFile2, 20000);
EXPECT_EQ(file.size(), 10000);
std::span eof(reinterpret_cast<char*>(file.data()) + file.size() - 3, 3);
EXPECT_TRUE(std::ranges::equal(std::string("EOF"), eof));
file.resize(13);
std::span fileStr(reinterpret_cast<char*>(file.data()), std::size(str));
EXPECT_TRUE(std::ranges::equal(str, fileStr));
}
fs::remove(tmpFile2);
EXPECT_FALSE(fs::exists(tmpFile2));
}
TEST_F(MappedFileFixture, ResizeFileExtended) {
fs::path tmpFile2 = fs::path{testing::TempDir()} / "test2.dat";
{
resizable_file file(tmpFile2, 1llu << 32); // 4gb of virtual memory pls
file.resize(1);
void* data = file.data();
uint8_t* bytes = reinterpret_cast<uint8_t*>(data);
bytes[0] = 1;
// Grow
size_t nextBytes = 1;
while ((nextBytes *= 2) <= 256 * 1024 * 1024) {
EXPECT_NO_THROW(file.resize(nextBytes));
EXPECT_EQ(data, file.data());
uint8_t b = 0;
// Verify memory from previous resizes remains intact
for (size_t i = 1; i < nextBytes; i *= 2)
EXPECT_EQ(bytes[i - 1], ++b);
bytes[nextBytes - 1] = ++b;
}
// Shrink
while ((nextBytes /= 2) > 1) {
EXPECT_NO_THROW(file.resize(nextBytes));
EXPECT_EQ(data, file.data());
uint8_t b = 0;
// Verify memory from previous resizes remains intact
for (size_t i = 1; i < nextBytes; i *= 2)
EXPECT_EQ(bytes[i - 1], ++b);
}
}
fs::remove(tmpFile2);
EXPECT_FALSE(fs::exists(tmpFile2));
}
TEST_F(MappedFileFixture, ResizableFileSize) {
size_t lastSize = fs::file_size(m_tmpFile);
size_t sizes[] = {0, 1, 2, 4000, 4095, 4096, 4097, 10000, 0, 4097, 4096, 4095, 42};
for (size_t size : sizes) {
resizable_file file(m_tmpFile, 10000);
EXPECT_EQ(file.size(), lastSize);
file.resize(size);
EXPECT_EQ(file.size(), size);
lastSize = size;
}
EXPECT_EQ(fs::file_size(m_tmpFile), lastSize);
}
TEST_F(MappedFileFixture, ResizableFileSync) {
resizable_file file(m_tmpFile, 10000);
size_t sizes[] = {1, 2, 4000, 4095, 4096, 4097, 10000, 1, 4097, 4096, 4095, 42};
for (size_t size : sizes) {
file.resize(size);
static_cast<char*>(file.data())[size - 1] = '*';
file.sync(size - 1, 1);
{
// This is pointless because we're just reading the same pages, whether they're flushed
// to disk or not. Tried reading metadata like std::filesystem::last_write_time, but not
// reliable enough for testing.
std::ifstream ifile(m_tmpFile, std::ios::binary);
char c;
ifile.seekg(size - 1);
ifile.read(&c, 1);
EXPECT_EQ(c, '*');
}
static_cast<char*>(file.data())[size - 1] = '_';
file.sync();
}
}
TEST(MappedFile, Empty) {
fs::path tmpFile2 = fs::path{testing::TempDir()} / "test2.dat";
EXPECT_FALSE(fs::exists(tmpFile2));
{
size_t maxSize = 4096;
resizable_file file(tmpFile2, maxSize);
EXPECT_TRUE(fs::exists(tmpFile2));
EXPECT_EQ(file.size(), 0);
}
EXPECT_TRUE(fs::exists(tmpFile2));
EXPECT_EQ(fs::file_size(tmpFile2), 0);
fs::remove(tmpFile2);
EXPECT_FALSE(fs::exists(tmpFile2));
}
TEST_F(MappedFileFixture, ClearExisting) {
EXPECT_EQ(fs::file_size(m_tmpFile), sizeof(int));
{
size_t maxSize = 4096;
resizable_file file(m_tmpFile, maxSize);
EXPECT_EQ(file.size(), sizeof(int));
file.resize(0);
}
EXPECT_EQ(fs::file_size(m_tmpFile), 0);
}
TEST(MappedFile, Readme) {
fs::path tmpFile2 = fs::path{testing::TempDir()} / "test2.dat";
{
size_t maxSize = 4096;
resizable_file file(tmpFile2, maxSize);
EXPECT_EQ(file.size(), 0);
EXPECT_EQ(file.data(), nullptr);
// Resize and write some data
file.resize(sizeof(int) * 10);
int* numbers = reinterpret_cast<int*>(file.data());
numbers[9] = 9;
// Resize again. Pointer remains valid and there's more space
file.resize(sizeof(int) * 100);
EXPECT_EQ(numbers[9], 9);
numbers[99] = 99;
}
fs::remove(tmpFile2);
EXPECT_FALSE(fs::exists(tmpFile2));
}