dte test coverage


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