1 /*
2 Copyright 2021 Northern.tech AS
3
4 This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
5
6 This program is free software; you can redistribute it and/or modify it
7 under the terms of the GNU General Public License as published by the
8 Free Software Foundation; version 3.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
18
19 To the extent this program is licensed as part of the Enterprise
20 versions of CFEngine, the applicable Commercial Open Source License
21 (COSL) may apply to this file if you as a licensee so wish it. See
22 included file COSL.txt.
23 */
24
25 #include <test.h>
26
27 #include <files_lib.h> /* FullWrite */
28 #include <misc_lib.h> /* xsnprintf */
29
30
31 /* CopyRegularFileDisk() is the function we are testing. */
32 #include <files_copy.h>
33
34
35 /* WARNING on Solaris 11 with ZFS, stat.st_nblocks==1 if you check the file
36 * right after closing it, and it changes to the right value (i.e. how many
37 * 512 chunks the file has) a few seconds later!!! So we force a sync() on
38 * all tests to avoid this! */
39 bool do_sync = true;
40 // TODO detect in runtime if the filesystem needs to be synced!
41
42 #define MAYBE_SYNC_NOW if (do_sync) sync()
43
44
45 char TEST_DIR[] = "/tmp/files_copy_test-XXXXXX";
46 char TEST_SUBDIR[256];
47 char TEST_SRC_FILE[256];
48 char TEST_DST_FILE[256];
49 #define TEST_FILENAME "testfile"
50
51 /* Size must be enough to contain several disk blocks. */
52 int blk_size; /* defined during init() */
53 #define TESTFILE_SIZE (blk_size * 8)
54 #define SHORT_REGION (blk_size / 2) /* not enough for hole */
55 #define LONG_REGION (blk_size * 2) /* hole for sure */
56 #define GARBAGE "blahblehblohbluebloblebli"
57 #define GARBAGE_LEN (sizeof(GARBAGE) - 1)
58
59
60 /* Notice if even one test failed so that we don't clean up. */
61 #define NTESTS 8
62 bool test_has_run[NTESTS + 1];
63 bool success [NTESTS + 1];
64
65
66 /* Some filesystems don't support sparse files at all (swap fs on Solaris is
67 * one example). We create a fully sparse file (seek 1MB further) and check if
68 * it's sparse. If not, WE SKIP ALL SPARSENESS TESTS but we still run this
69 * unit test in order to test for integrity of data. */
70 bool SPARSE_SUPPORT_OK = true;
71
FsSupportsSparseFiles(const char * filename)72 static bool FsSupportsSparseFiles(const char *filename)
73 {
74 #ifdef __hpux
75 Log(LOG_LEVEL_NOTICE, "HP-UX detected, skipping sparseness tests!"
76 " Not sure why, but on HP-UX with 'vxfs' filesystem,"
77 " the sparse files generated have /sometimes/ greater"
78 " 'disk usage' than their true size, and this is verified by du");
79 return false;
80 #endif
81 #ifdef __APPLE__
82 Log(LOG_LEVEL_NOTICE, "OS X detected, skipping sparseness tests!");
83 return false;
84 #endif
85
86 int fd = open(filename, O_CREAT | O_WRONLY | O_BINARY, 0700);
87 assert_int_not_equal(fd, -1);
88
89 /* 8MB for our temporary sparse file sounds good. */
90 const int sparse_file_size = 8 * 1024 * 1024;
91
92 off_t s_ret = lseek(fd, sparse_file_size, SEEK_CUR);
93 assert_int_equal(s_ret, sparse_file_size);
94
95 /* Make sure the file is not truncated by writing one byte
96 and taking it back. */
97 ssize_t w_ret = write(fd, "", 1);
98 assert_int_equal(w_ret, 1);
99
100 int tr_ret = ftruncate(fd, sparse_file_size);
101 assert_int_equal(tr_ret, 0);
102
103 /* On ZFS the file needs to be synced, else stat()
104 reports a temporary value for st_blocks! */
105 fsync(fd);
106
107 int c_ret = close(fd);
108 assert_int_equal(c_ret, 0);
109
110 struct stat statbuf;
111 int st_ret = stat(filename, &statbuf);
112 assert_int_not_equal(st_ret, -1);
113
114 int u_ret = unlink(filename); /* clean up */
115 assert_int_equal(u_ret, 0);
116
117 /* ACTUAL TEST: IS THE FILE SPARSE? */
118 if (ST_NBYTES(statbuf) < statbuf.st_size)
119 {
120 return true;
121 }
122 else
123 {
124 return false;
125 }
126 }
127
128
129
init(void)130 static void init(void)
131 {
132 LogSetGlobalLevel(LOG_LEVEL_DEBUG);
133
134 char *ok = mkdtemp(TEST_DIR);
135 assert_int_not_equal(ok, NULL);
136
137 /* Set blk_size */
138 struct stat statbuf;
139 int ret1 = stat(TEST_DIR, &statbuf);
140 assert_int_not_equal(ret1, -1);
141
142 blk_size = ST_BLKSIZE(statbuf);
143 Log(LOG_LEVEL_NOTICE,
144 "Running sparse file tests with blocksize=%d TESTFILE_SIZE=%d",
145 blk_size, TESTFILE_SIZE);
146 Log(LOG_LEVEL_NOTICE, "Temporary directory: %s", TEST_DIR);
147
148 // /tmp/files_copy_test-XXXXXX/subdir
149 xsnprintf(TEST_SUBDIR, sizeof(TEST_SUBDIR), "%s/%s",
150 TEST_DIR, "subdir");
151 // /tmp/files_copy_test-XXXXXX/testfile
152 xsnprintf(TEST_SRC_FILE, sizeof(TEST_SRC_FILE), "%s/%s",
153 TEST_DIR, TEST_FILENAME);
154 // /tmp/files_copy_test-XXXXXX/subdir/testfile
155 xsnprintf(TEST_DST_FILE, sizeof(TEST_DST_FILE), "%s/%s",
156 TEST_SUBDIR, TEST_FILENAME);
157
158 int ret2 = mkdir(TEST_SUBDIR, 0700);
159 assert_int_equal(ret2, 0);
160
161 SPARSE_SUPPORT_OK = true;
162 if (!FsSupportsSparseFiles(TEST_DST_FILE))
163 {
164 Log(LOG_LEVEL_NOTICE,
165 "filesystem for directory '%s' doesn't seem to support sparse files!"
166 " TEST WILL ONLY VERIFY FILE INTEGRITY!", TEST_DIR);
167
168 SPARSE_SUPPORT_OK = false;
169 }
170
171 test_has_run[0] = true;
172 success[0] = true;
173 }
174
finalise(void)175 static void finalise(void)
176 {
177 /* Do not remove evidence if even one test has failed. */
178 bool all_success = true;
179 for (int i = 0; i < NTESTS; i++)
180 {
181 if (test_has_run[i])
182 {
183 all_success = all_success && success[i];
184 }
185 }
186 if (!all_success)
187 {
188 Log(LOG_LEVEL_NOTICE,
189 "Skipping cleanup of test data because of tests failing");
190 return;
191 }
192
193 int ret1 = unlink(TEST_DST_FILE);
194 assert_int_equal(ret1, 0);
195 int ret2 = rmdir(TEST_SUBDIR);
196 assert_int_equal(ret2, 0);
197 int ret3 = unlink(TEST_SRC_FILE);
198 assert_int_equal(ret3, 0);
199 int ret5 = rmdir(TEST_DIR);
200 assert_int_equal(ret5, 0);
201 }
202
203
FillBufferWithGarbage(char * buf,size_t buf_size)204 static void FillBufferWithGarbage(char *buf, size_t buf_size)
205 {
206 for (size_t i = 0; i < TESTFILE_SIZE; i += GARBAGE_LEN)
207 {
208 memcpy(&buf[i], GARBAGE,
209 MIN(buf_size - i, GARBAGE_LEN));
210 }
211 }
212
213 /* Fill a buffer with non-NULL garbage. */
WriteBufferToFile(const char * name,const void * buf,size_t count)214 static void WriteBufferToFile(const char *name, const void *buf, size_t count)
215 {
216 int fd = open(name, O_CREAT | O_WRONLY | O_TRUNC | O_BINARY, 0700);
217 assert_int_not_equal(fd, -1);
218
219 ssize_t written = FullWrite(fd, buf, count);
220 assert_int_equal(written, count);
221
222 int close_ret = close(fd);
223 assert_int_not_equal(close_ret, -1);
224 }
225
CompareFileToBuffer(const char * filename,const void * buf,size_t buf_size)226 static bool CompareFileToBuffer(const char *filename,
227 const void *buf, size_t buf_size)
228 {
229 FILE *f = fopen(filename, "rb");
230 assert_int_not_equal(f, NULL);
231
232 size_t total = 0;
233 char filebuf[DEV_BSIZE];
234 size_t n;
235 while ((n = fread(filebuf, 1, sizeof(filebuf), f)) != 0)
236 {
237 bool differ = (total + n > buf_size);
238
239 differ = differ || (memcmp(filebuf, buf + total, n) != 0);
240
241 if (differ)
242 {
243 Log(LOG_LEVEL_DEBUG,
244 "file differs from buffer at pos %zu len %zu",
245 total, n);
246 fclose(f);
247 return false;
248 }
249
250 total += n;
251 }
252
253 bool error_happened = (ferror(f) != 0);
254 assert_false(error_happened);
255
256 if (total != buf_size)
257 {
258 Log(LOG_LEVEL_DEBUG, "filesize:%zu buffersize:%zu ==> differ",
259 total, buf_size);
260 fclose(f);
261 return false;
262 }
263
264 fclose(f);
265 return true;
266 }
267
268 /* TODO isolate important code and move to files_lib.c. */
FileIsSparse(const char * filename)269 static bool FileIsSparse(const char *filename)
270 {
271 MAYBE_SYNC_NOW;
272
273 struct stat statbuf;
274 int ret = stat(filename, &statbuf);
275 assert_int_not_equal(ret, -1);
276
277 Log(LOG_LEVEL_DEBUG,
278 " st_size=%ju ST_NBYTES=%ju ST_NBLOCKS=%ju ST_BLKSIZE=%ju DEV_BSIZE=%ju",
279 (uintmax_t) statbuf.st_size, (uintmax_t) ST_NBYTES(statbuf),
280 (uintmax_t) ST_NBLOCKS(statbuf), (uintmax_t) ST_BLKSIZE(statbuf),
281 (uintmax_t) DEV_BSIZE);
282
283 if (statbuf.st_size <= ST_NBYTES(statbuf))
284 {
285 Log(LOG_LEVEL_DEBUG, "File is probably non-sparse");
286 return false;
287 }
288 else
289 {
290 /* We definitely know the file is sparse, since the allocated bytes
291 * are less than the real size. */
292 Log(LOG_LEVEL_DEBUG, "File is definitely sparse");
293 return true;
294 }
295 }
296
297 const char *srcfile = TEST_SRC_FILE;
298 const char *dstfile = TEST_DST_FILE;
299
test_sparse_files_1(void)300 static void test_sparse_files_1(void)
301 {
302 Log(LOG_LEVEL_VERBOSE,
303 "No zeros in the file, the output file must be non-sparse");
304
305 char *buf = xmalloc(TESTFILE_SIZE);
306
307 FillBufferWithGarbage(buf, TESTFILE_SIZE);
308
309 WriteBufferToFile(srcfile, buf, TESTFILE_SIZE);
310
311 /* ACTUAL TEST */
312 bool ret = CopyRegularFileDisk(srcfile, dstfile);
313 assert_true(ret);
314
315 if (SPARSE_SUPPORT_OK)
316 {
317 bool is_sparse = FileIsSparse(dstfile);
318 assert_false(is_sparse);
319 }
320
321 bool data_ok = CompareFileToBuffer(dstfile, buf, TESTFILE_SIZE);
322 assert_true(data_ok);
323
324 free(buf);
325 test_has_run[1] = true;
326 success [1] = true;
327 }
328
test_sparse_files_2(void)329 static void test_sparse_files_2(void)
330 {
331 Log(LOG_LEVEL_VERBOSE,
332 "File starting with few zeroes, the output file must be non-sparse.");
333
334 char *buf = xmalloc(TESTFILE_SIZE);
335
336 FillBufferWithGarbage(buf, TESTFILE_SIZE);
337 memset(buf, 0, SHORT_REGION);
338
339 WriteBufferToFile(srcfile, buf, TESTFILE_SIZE);
340
341 /* ACTUAL TEST */
342 bool ret = CopyRegularFileDisk(srcfile, dstfile);
343 assert_true(ret);
344
345 if (SPARSE_SUPPORT_OK)
346 {
347 bool is_sparse = FileIsSparse(dstfile);
348 assert_false(is_sparse);
349 }
350
351 bool data_ok = CompareFileToBuffer(dstfile, buf, TESTFILE_SIZE);
352 assert_true(data_ok);
353
354 free(buf);
355 test_has_run[2] = true;
356 success [2] = true;
357 }
358
test_sparse_files_3(void)359 static void test_sparse_files_3(void)
360 {
361 Log(LOG_LEVEL_VERBOSE,
362 "File with few zeroes in the middle, the output file must be non-sparse");
363
364 char *buf = xmalloc(TESTFILE_SIZE);
365
366 FillBufferWithGarbage(buf, TESTFILE_SIZE);
367 memset(&buf[TESTFILE_SIZE / 2], 0, SHORT_REGION);
368
369 WriteBufferToFile(srcfile, buf, TESTFILE_SIZE);
370
371 /* ACTUAL TEST */
372 bool ret = CopyRegularFileDisk(srcfile, dstfile);
373 assert_true(ret);
374
375 if (SPARSE_SUPPORT_OK)
376 {
377 bool is_sparse = FileIsSparse(dstfile);
378 assert_false(is_sparse);
379 }
380
381 bool data_ok = CompareFileToBuffer(dstfile, buf, TESTFILE_SIZE);
382 assert_true(data_ok);
383
384 free(buf);
385 test_has_run[3] = true;
386 success [3] = true;
387 }
388
test_sparse_files_4(void)389 static void test_sparse_files_4(void)
390 {
391 Log(LOG_LEVEL_VERBOSE,
392 "File ending with few zeroes, the output file must be non-sparse");
393
394 char *buf = xmalloc(TESTFILE_SIZE);
395
396 FillBufferWithGarbage(buf, TESTFILE_SIZE);
397 memset(&buf[TESTFILE_SIZE - SHORT_REGION], 0, SHORT_REGION);
398
399 WriteBufferToFile(srcfile, buf, TESTFILE_SIZE);
400
401 /* ACTUAL TEST */
402 bool ret = CopyRegularFileDisk(srcfile, dstfile);
403 assert_true(ret);
404
405 if (SPARSE_SUPPORT_OK)
406 {
407 bool is_sparse = FileIsSparse(dstfile);
408 assert_false(is_sparse);
409 }
410
411 bool data_ok = CompareFileToBuffer(dstfile, buf, TESTFILE_SIZE);
412 assert_true(data_ok);
413
414 free(buf);
415 test_has_run[4] = true;
416 success [4] = true;
417 }
418
test_sparse_files_5(void)419 static void test_sparse_files_5(void)
420 {
421 Log(LOG_LEVEL_VERBOSE,
422 "File starting with many zeroes, the output file must be sparse");
423
424 char *buf = xmalloc(TESTFILE_SIZE);
425
426 FillBufferWithGarbage(buf, TESTFILE_SIZE);
427 memset(buf, 0, LONG_REGION);
428
429 WriteBufferToFile(srcfile, buf, TESTFILE_SIZE);
430
431 /* ACTUAL TEST */
432 bool ret = CopyRegularFileDisk(srcfile, dstfile);
433 assert_true(ret);
434
435 if (SPARSE_SUPPORT_OK)
436 {
437 bool is_sparse = FileIsSparse(dstfile);
438 assert_true(is_sparse);
439 }
440
441 bool data_ok = CompareFileToBuffer(dstfile, buf, TESTFILE_SIZE);
442 assert_true(data_ok);
443
444 free(buf);
445 test_has_run[5] = true;
446 success [5] = true;
447 }
448
test_sparse_files_6(void)449 static void test_sparse_files_6(void)
450 {
451 Log(LOG_LEVEL_VERBOSE,
452 "Many zeroes in the middle of the file, the output file must be sparse");
453
454 char *buf = xmalloc(TESTFILE_SIZE);
455
456 FillBufferWithGarbage(buf, TESTFILE_SIZE);
457 memset(&buf[TESTFILE_SIZE / 2 - 7], 0, LONG_REGION);
458
459 WriteBufferToFile(srcfile, buf, TESTFILE_SIZE);
460
461 /* ACTUAL TEST */
462 bool ret = CopyRegularFileDisk(srcfile, dstfile);
463 assert_true(ret);
464
465 if (SPARSE_SUPPORT_OK)
466 {
467 bool is_sparse = FileIsSparse(dstfile);
468 assert_true(is_sparse);
469 }
470
471 bool data_ok = CompareFileToBuffer(dstfile, buf, TESTFILE_SIZE);
472 assert_true(data_ok);
473
474 free(buf);
475 test_has_run[6] = true;
476 success [6] = true;
477 }
478
test_sparse_files_7(void)479 static void test_sparse_files_7(void)
480 {
481 Log(LOG_LEVEL_VERBOSE,
482 "File ending with many zeroes, the output file must be sparse");
483
484 char *buf = xmalloc(TESTFILE_SIZE);
485
486 FillBufferWithGarbage(buf, TESTFILE_SIZE);
487 memset(&buf[TESTFILE_SIZE - LONG_REGION], 0, LONG_REGION);
488
489 WriteBufferToFile(srcfile, buf, TESTFILE_SIZE);
490
491 /* ACTUAL TEST */
492 bool ret = CopyRegularFileDisk(srcfile, dstfile);
493 assert_true(ret);
494
495 if (SPARSE_SUPPORT_OK)
496 {
497 bool is_sparse = FileIsSparse(dstfile);
498 assert_true(is_sparse);
499 }
500
501 bool data_ok = CompareFileToBuffer(dstfile, buf, TESTFILE_SIZE);
502 assert_true(data_ok);
503
504 free(buf);
505 test_has_run[7] = true;
506 success [7] = true;
507 }
508
test_sparse_files_8(void)509 static void test_sparse_files_8(void)
510 {
511 Log(LOG_LEVEL_VERBOSE,
512 "Special Case: File ending with few (DEV_BSIZE-1) zeroes,"
513 " at block size barrier; so the last bytes written are a hole and are seek()ed,"
514 " but the output file can't be sparse since the hole isn't block-sized");
515
516 char *buf = xmalloc(TESTFILE_SIZE + DEV_BSIZE - 1);
517
518 FillBufferWithGarbage(buf, TESTFILE_SIZE);
519 memset(&buf[TESTFILE_SIZE], 0, DEV_BSIZE - 1);
520
521 WriteBufferToFile(srcfile, buf, TESTFILE_SIZE + DEV_BSIZE - 1);
522
523 /* ACTUAL TEST */
524 bool ret = CopyRegularFileDisk(srcfile, dstfile);
525 assert_true(ret);
526
527 if (SPARSE_SUPPORT_OK)
528 {
529 bool is_sparse = FileIsSparse(dstfile);
530 assert_false(is_sparse);
531 }
532
533 bool data_ok = CompareFileToBuffer(dstfile, buf, TESTFILE_SIZE + DEV_BSIZE - 1);
534 assert_true(data_ok);
535
536 free(buf);
537 test_has_run[8] = true;
538 success [8] = true;
539 }
540
541
542
main()543 int main()
544 {
545 PRINT_TEST_BANNER();
546
547 const UnitTest tests[] = {
548 unit_test(init),
549 unit_test(test_sparse_files_1),
550 unit_test(test_sparse_files_2),
551 unit_test(test_sparse_files_3),
552 unit_test(test_sparse_files_4),
553 unit_test(test_sparse_files_5),
554 unit_test(test_sparse_files_6),
555 unit_test(test_sparse_files_7),
556 unit_test(test_sparse_files_8),
557 unit_test(finalise),
558 };
559
560 int ret = run_tests(tests);
561
562 return ret;
563 }
564