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