dte test coverage


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 50.0% high: ≥ 85.0%
Coverage Exec / Excl / Total
Lines: 64.2% 156 / 0 / 243
Functions: 100.0% 13 / 0 / 13
Branches: 44.8% 60 / 18 / 152

src/load-save.c
Line Branch Exec Source
1 #include "build-defs.h"
2 #include <errno.h>
3 #include <stdint.h>
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <string.h>
7 #include <sys/mman.h>
8 #include <sys/stat.h>
9 #include <unistd.h>
10 #include "load-save.h"
11 #include "block.h"
12 #include "convert.h"
13 #include "encoding.h"
14 #include "options.h"
15 #include "util/debug.h"
16 #include "util/fd.h"
17 #include "util/list.h"
18 #include "util/log.h"
19 #include "util/path.h"
20 #include "util/str-util.h"
21 #include "util/time-util.h"
22 #include "util/xadvise.h"
23 #include "util/xreadwrite.h"
24 #include "util/xstring.h"
25
26 34 static bool decode_and_add_blocks (
27 Buffer *buffer,
28 ErrorBuffer *errbuf,
29 const char *text,
30 size_t text_len,
31 bool utf8_bom
32 ) {
33 34 EncodingType bom_type = detect_encoding_from_bom(text, text_len);
34
3/4
✓ Branch 3 → 4 taken 26 times.
✓ Branch 3 → 11 taken 8 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 17 taken 26 times.
34 if (!buffer->encoding && bom_type != UNKNOWN_ENCODING) {
35 const char *enc = encoding_from_type(bom_type);
36 if (!conversion_supported_by_iconv(enc, "UTF-8")) {
37 // TODO: Use error_msg() and return false here?
38 LOG_NOTICE("file has %s BOM, but conversion unsupported", enc);
39 enc = encoding_from_type(UTF8);
40 }
41 buffer_set_encoding(buffer, enc, utf8_bom);
42 }
43
44 // Skip BOM only if it matches the specified file encoding
45
1/4
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 17 taken 8 times.
✗ Branch 13 → 14 not taken.
✗ Branch 13 → 17 not taken.
8 if (bom_type != UNKNOWN_ENCODING && bom_type == lookup_encoding(buffer->encoding)) {
46 const ByteOrderMark *bom = get_bom_for_encoding(bom_type);
47 if (bom) {
48 const size_t bom_len = bom->len;
49 text += bom_len;
50 text_len -= bom_len;
51 buffer->bom = true;
52 }
53 }
54
55
2/2
✓ Branch 17 → 18 taken 26 times.
✓ Branch 17 → 20 taken 8 times.
34 if (!buffer->encoding) {
56 26 buffer->encoding = encoding_from_type(UTF8);
57 26 buffer->bom = utf8_bom;
58 }
59
60 34 return file_decoder_read(buffer, errbuf, text, text_len);
61 }
62
63 34 static void fixup_blocks(Buffer *buffer)
64 {
65
2/2
✓ Branch 2 → 3 taken 6 times.
✓ Branch 2 → 6 taken 28 times.
34 if (list_empty(&buffer->blocks)) {
66 6 Block *blk = block_new(1);
67 6 list_insert_before(&blk->node, &buffer->blocks);
68 6 return;
69 }
70
71 28 Block *lastblk = BLOCK(buffer->blocks.prev);
72 28 BUG_ON(!lastblk);
73 28 size_t n = lastblk->size;
74
2/4
✓ Branch 8 → 9 taken 28 times.
✗ Branch 8 → 12 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 12 taken 28 times.
28 if (n && lastblk->data[n - 1] != '\n') {
75 // Incomplete lines are not allowed because they're special
76 // cases and cause lots of trouble
77 block_grow(lastblk, n + 1);
78 lastblk->data[n] = '\n';
79 lastblk->size++;
80 lastblk->nl++;
81 buffer->nl++;
82 }
83 }
84
85 49 static bool update_file_info(FileInfo *info, const struct stat *st)
86 {
87 49 *info = (FileInfo) {
88 49 .size = st->st_size,
89 49 .mode = st->st_mode,
90 49 .gid = st->st_gid,
91 49 .uid = st->st_uid,
92 49 .dev = st->st_dev,
93 49 .ino = st->st_ino,
94 49 .mtime = *get_stat_mtime(st),
95 };
96 49 return true;
97 }
98
99 22 static bool buffer_stat(FileInfo *info, const char *filename)
100 {
101 22 struct stat st;
102
2/4
✓ Branch 3 → 4 taken 22 times.
✗ Branch 3 → 7 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 22 times.
22 return !stat(filename, &st) && update_file_info(info, &st);
103 }
104
105 27 static bool buffer_fstat(FileInfo *info, int fd)
106 {
107 27 struct stat st;
108
2/4
✓ Branch 3 → 4 taken 27 times.
✗ Branch 3 → 7 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 27 times.
27 return !fstat(fd, &st) && update_file_info(info, &st);
109 }
110
111 34 bool read_blocks(Buffer *buffer, ErrorBuffer *ebuf, int fd, bool utf8_bom)
112 {
113 34 const size_t map_size = 64 * 1024;
114 34 size_t size = buffer->file.size;
115 34 char *text = NULL;
116 34 bool mapped = false;
117 34 bool ret = false;
118
119
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 33 times.
34 if (size >= map_size) {
120 // NOTE: size must be greater than 0
121 1 text = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
122
1/2
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 7 not taken.
1 if (text != MAP_FAILED) {
123 1 advise_sequential(text, size);
124 1 mapped = true;
125 1 goto decode;
126 }
127 LOG_ERRNO("mmap() failed");
128 text = NULL;
129 }
130
131
2/2
✓ Branch 8 → 9 taken 25 times.
✓ Branch 8 → 15 taken 8 times.
33 if (likely(size > 0)) {
132 25 text = malloc(size);
133
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 25 times.
25 if (unlikely(!text)) {
134 goto error;
135 }
136 25 ssize_t rc = xread_all(fd, text, size);
137
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 25 times.
25 if (unlikely(rc < 0)) {
138 goto error;
139 }
140 25 size = rc;
141 } else {
142 // st_size is zero for some files in /proc
143 8 size_t alloc = map_size;
144 8 BUG_ON(!IS_POWER_OF_2(alloc));
145 8 text = malloc(alloc);
146
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 18 taken 8 times.
8 if (unlikely(!text)) {
147 goto error;
148 }
149 size_t pos = 0;
150 10 while (1) {
151 10 ssize_t rc = xread_all(fd, text + pos, alloc - pos);
152
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 10 times.
10 if (rc < 0) {
153 goto error;
154 }
155
2/2
✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 27 taken 8 times.
10 if (rc == 0) {
156 break;
157 }
158 2 pos += rc;
159
1/2
✓ Branch 22 → 17 taken 2 times.
✗ Branch 22 → 23 not taken.
2 if (pos == alloc) {
160 size_t new_alloc = alloc << 1;
161 if (unlikely(alloc >= new_alloc)) {
162 errno = EOVERFLOW;
163 goto error;
164 }
165 alloc = new_alloc;
166 char *new_text = realloc(text, alloc);
167 if (unlikely(!new_text)) {
168 goto error;
169 }
170 text = new_text;
171 }
172 }
173 size = pos;
174 }
175
176 34 decode:
177 34 ret = decode_and_add_blocks(buffer, ebuf, text, size, utf8_bom);
178
179 34 error:
180
2/2
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 32 taken 33 times.
34 if (mapped) {
181 1 int r = munmap(text, size); // Can only fail due to usage error
182
1/2
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 33 taken 1 time.
1 LOG_ERRNO_ON(r, "munmap");
183 } else {
184 33 free(text);
185 }
186
187
1/2
✓ Branch 33 → 34 taken 34 times.
✗ Branch 33 → 35 not taken.
34 if (ret) {
188 34 fixup_blocks(buffer);
189 }
190
191 34 return ret;
192 }
193
194 // Convert size in bytes to size in MiB (rounded up, for any remainder)
195 98 static uintmax_t filesize_in_mib(uintmax_t nbytes)
196 {
197 98 return (nbytes >> 20) + !!(nbytes & 0xFFFFF);
198 }
199
200 24 UNITTEST {
201 24 const uintmax_t seven_mib = 7UL << 20;
202 // NOLINTBEGIN(bugprone-assert-side-effect)
203 24 BUG_ON(filesize_in_mib(seven_mib) != 7);
204 24 BUG_ON(filesize_in_mib(seven_mib - 1) != 7);
205 24 BUG_ON(filesize_in_mib(seven_mib + 1) != 8);
206 // NOLINTEND(bugprone-assert-side-effect)
207 24 }
208
209 27 bool load_buffer (
210 Buffer *buffer,
211 const char *filename,
212 const GlobalOptions *gopts,
213 ErrorBuffer *ebuf,
214 bool must_exist
215 ) {
216 27 BUG_ON(buffer->abs_filename);
217 27 BUG_ON(!list_empty(&buffer->blocks));
218
219 27 int fd = xopen(filename, O_RDONLY | O_CLOEXEC, 0);
220
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 19 taken 27 times.
27 if (fd < 0) {
221 if (errno != ENOENT) {
222 return error_msg(ebuf, "Error opening %s: %s", filename, strerror(errno));
223 }
224 if (must_exist) {
225 return error_msg(ebuf, "File %s does not exist", filename);
226 }
227 if (!buffer->encoding) {
228 buffer->encoding = encoding_from_type(UTF8);
229 buffer->bom = gopts->utf8_bom;
230 }
231 Block *blk = block_new(1);
232 list_insert_before(&blk->node, &buffer->blocks);
233 return true;
234 }
235
236 27 FileInfo *info = &buffer->file;
237
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 24 taken 27 times.
27 if (!buffer_fstat(info, fd)) {
238 error_msg(ebuf, "fstat failed on %s: %s", filename, strerror(errno));
239 goto error;
240 }
241
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 27 taken 26 times.
27 if (!S_ISREG(info->mode)) {
242 1 error_msg(ebuf, "Not a regular file %s", filename);
243 1 goto error;
244 }
245
246 26 off_t size = info->size;
247
1/2
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 30 taken 26 times.
26 if (unlikely(size < 0)) {
248 error_msg(ebuf, "Invalid file size: %jd", (intmax_t)size);
249 goto error;
250 }
251
252 26 unsigned int limit_mib = gopts->filesize_limit;
253
1/2
✓ Branch 30 → 31 taken 26 times.
✗ Branch 30 → 34 not taken.
26 if (limit_mib > 0) {
254 26 uintmax_t size_mib = filesize_in_mib(size);
255
1/2
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 34 taken 26 times.
26 if (unlikely(size_mib > limit_mib)) {
256 error_msg (
257 ebuf,
258 "File size (%juMiB) exceeds 'filesize-limit' option (%uMiB): %s",
259 size_mib, limit_mib, filename
260 );
261 goto error;
262 }
263 }
264
265
1/2
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 39 taken 26 times.
26 if (!read_blocks(buffer, ebuf, fd, gopts->utf8_bom)) {
266 error_msg(ebuf, "Error reading %s: %s", filename, strerror(errno));
267 goto error;
268 }
269
270 26 BUG_ON(!buffer->encoding);
271 26 xclose(fd);
272 26 return true;
273
274 1 error:
275 1 xclose(fd);
276 1 return false;
277 }
278
279 22 static bool write_buffer(const Buffer *buffer, const FileSaveContext *ctx, int fd)
280 {
281 22 ErrorBuffer *ebuf = ctx->ebuf;
282 22 FileEncoder enc = file_encoder(ctx->encoding, ctx->crlf, fd);
283 22 size_t size = 0;
284
285
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 12 taken 22 times.
22 if (unlikely(ctx->write_bom)) {
286 const EncodingType type = lookup_encoding(ctx->encoding);
287 const ByteOrderMark *bom = get_bom_for_encoding(type);
288 if (bom->len && xwrite_all(fd, bom->bytes, bom->len) < 0) {
289 file_encoder_free(&enc);
290 return error_msg_errno(ebuf, "write");
291 }
292 size += bom->len;
293 }
294
295 22 const Block *blk;
296
2/2
✓ Branch 18 → 13 taken 22 times.
✓ Branch 18 → 19 taken 22 times.
44 block_for_each(blk, &buffer->blocks) {
297 22 ssize_t rc = file_encoder_write(&enc, blk->data, blk->size, blk->nl);
298
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 17 taken 22 times.
22 if (rc < 0) {
299 file_encoder_free(&enc);
300 return error_msg_errno(ebuf, "write");
301 }
302 22 size += rc;
303 }
304
305 22 size_t nr_errors = file_encoder_get_nr_errors(&enc);
306 22 file_encoder_free(&enc);
307
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 25 taken 22 times.
22 if (nr_errors > 0) {
308 // Any real error hides this message
309 error_msg (
310 ebuf,
311 "Warning: %zu non-reversible character conversion%s; file saved",
312 nr_errors,
313 (nr_errors > 1) ? "s" : ""
314 );
315 }
316
317 // Need to truncate if writing to existing file
318
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 22 times.
22 if (xftruncate(fd, size)) {
319 return error_msg_errno(ebuf, "ftruncate");
320 }
321
322 return true;
323 }
324
325 22 static int xmkstemp_cloexec(char *path_template)
326 {
327 22 int fd;
328 #if HAVE_MKOSTEMP
329 44 do {
330 22 fd = mkostemp(path_template, O_CLOEXEC);
331
1/4
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 22 times.
✗ Branch 5 → 3 not taken.
✗ Branch 5 → 6 not taken.
22 } while (unlikely(fd == -1 && errno == EINTR));
332 22 return fd;
333 #endif
334
335 do {
336 fd = mkstemp(path_template); // NOLINT(*-unsafe-functions)
337 } while (unlikely(fd == -1 && errno == EINTR));
338
339 if (fd == -1) {
340 return fd;
341 }
342
343 if (unlikely(!fd_set_cloexec(fd, true))) {
344 xclose(fd);
345 return -1;
346 }
347
348 return fd;
349 }
350
351 22 static int tmp_file (
352 const char *filename,
353 const FileInfo *info,
354 mode_t new_file_mode,
355 char *buf,
356 size_t buflen
357 ) {
358
1/2
✓ Branch 2 → 3 taken 22 times.
✗ Branch 2 → 25 not taken.
22 if (str_has_prefix(filename, "/tmp/")) {
359 // Don't use temporary file when saving file in /tmp because crontab
360 // command doesn't like the file to be replaced
361 return -1;
362 }
363
364 22 const StringView dir = path_slice_dirname(filename);
365 22 const StringView base = strview(path_basename(filename));
366 22 size_t required_buflen = dir.length + base.length + sizeof("/.tmp..XXXXXX");
367
368
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 22 times.
22 if (unlikely(buflen < required_buflen)) {
369 LOG_ERROR("%s() buffer of size %zu insufficient", __func__, buflen);
370 buf[0] = '\0';
371 return -1;
372 }
373
374 // "<dir>/.tmp.<base>.XXXXXX"
375 22 xmempcpy4 (
376 buf,
377 22 dir.data, dir.length,
378 STRN("/.tmp."),
379 22 base.data, base.length,
380 STRN(".XXXXXX") + 1
381 );
382
383 22 int fd = xmkstemp_cloexec(buf);
384
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 11 taken 22 times.
22 if (fd == -1) {
385 // No write permission to the directory?
386 LOG_ERRNO("xmkstemp_cloexec() failed");
387 buf[0] = '\0';
388 return -1;
389 }
390
391
2/2
✓ Branch 11 → 12 taken 20 times.
✓ Branch 11 → 17 taken 2 times.
22 if (!info->mode) {
392 // New file
393
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 16 taken 20 times.
20 if (xfchmod(fd, new_file_mode) != 0) {
394 LOG_WARNING("failed to set file mode: %s", strerror(errno));
395 }
396 20 return fd;
397 }
398
399 // Preserve ownership and mode of the original file if possible
400
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 2 times.
2 if (xfchown(fd, info->uid, info->gid) != 0) {
401 LOG_WARNING("failed to preserve file ownership: %s", strerror(errno));
402 }
403
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 25 taken 2 times.
2 if (xfchmod(fd, info->mode) != 0) {
404 LOG_WARNING("failed to preserve file mode: %s", strerror(errno));
405 }
406
407 return fd;
408 }
409
410 22 bool save_buffer(Buffer *buffer, const char *filename, const FileSaveContext *ctx)
411 {
412 22 ErrorBuffer *ebuf = ctx->ebuf;
413 22 BUG_ON(!ebuf);
414 22 BUG_ON(!ctx->encoding);
415 22 char tmp[8192];
416 22 tmp[0] = '\0';
417 22 int fd = -1;
418
419
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 22 times.
22 if (ctx->hardlinks) {
420 LOG_INFO("target file has hard links; writing in-place");
421 } else {
422 // Try to use temporary file (safer)
423 22 fd = tmp_file(filename, &buffer->file, ctx->new_file_mode, tmp, sizeof(tmp));
424 }
425
426
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 15 taken 22 times.
22 if (fd < 0) {
427 // Overwrite the original file directly (if it exists).
428 // Ownership is preserved automatically if the file exists.
429 mode_t mode = buffer->file.mode;
430 if (mode == 0) {
431 // New file
432 mode = ctx->new_file_mode;
433 }
434 fd = xopen(filename, O_CREAT | O_TRUNC | O_WRONLY | O_CLOEXEC, mode);
435 if (fd < 0) {
436 return error_msg_errno(ebuf, "open");
437 }
438 }
439
440
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 22 times.
22 if (!write_buffer(buffer, ctx, fd)) {
441 goto error;
442 }
443
444
1/4
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 23 taken 22 times.
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 23 not taken.
22 if (buffer->options.fsync && xfsync(fd) != 0) {
445 error_msg_errno(ebuf, "fsync");
446 goto error;
447 }
448
449 22 SystemErrno err = xclose(fd);
450 22 fd = -1;
451
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 28 taken 22 times.
22 if (err != 0) {
452 error_msg(ebuf, "close: %s", strerror(err));
453 goto error;
454 }
455
456
2/4
✓ Branch 28 → 29 taken 22 times.
✗ Branch 28 → 33 not taken.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 33 taken 22 times.
22 if (tmp[0] && rename(tmp, filename)) {
457 error_msg_errno(ebuf, "rename");
458 goto error;
459 }
460
461 22 buffer_stat(&buffer->file, filename);
462 22 return true;
463
464 error:
465 xclose(fd);
466 if (tmp[0]) {
467 int r = unlink(tmp);
468 LOG_ERRNO_ON(r, "unlink");
469 } else {
470 // Not using temporary file, therefore mtime may have changed.
471 // Update stat to avoid "File has been modified by someone else"
472 // error later when saving the file again.
473 buffer_stat(&buffer->file, filename);
474 }
475 return false;
476 }
477