dte test coverage


Directory: ./
File: src/load-save.c
Date: 2025-09-07 23:01:39
Exec Total Coverage
Lines: 156 242 64.5%
Functions: 13 13 100.0%
Branches: 60 132 45.5%

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