dte test coverage


Directory: ./
File: src/load-save.c
Date: 2025-07-04 20:13:35
Exec Total Coverage
Lines: 154 236 65.3%
Functions: 13 13 100.0%
Branches: 60 132 45.5%

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