dte test coverage


Directory: ./
File: src/load-save.c
Date: 2025-02-14 16:55:22
Exec Total Coverage
Lines: 158 247 64.0%
Functions: 14 15 93.3%
Branches: 60 137 43.8%

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