dte test coverage


Directory: ./
File: src/load-save.c
Date: 2024-12-21 16:03:22
Exec Total Coverage
Lines: 157 244 64.3%
Functions: 14 15 93.3%
Branches: 61 139 43.9%

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 "error.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 30 static bool decode_and_add_blocks(Buffer *buffer, const unsigned char *buf, size_t size, bool utf8_bom)
25 {
26 30 EncodingType bom_type = detect_encoding_from_bom(buf, size);
27
3/4
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 25 times.
30 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 not taken.
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✗ Branch 3 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 taken 25 times.
✓ Branch 1 taken 5 times.
30 if (!buffer->encoding) {
49 25 buffer->encoding = encoding_from_type(UTF8);
50 25 buffer->bom = utf8_bom;
51 }
52
53 30 return file_decoder_read(buffer, buf, size);
54 }
55
56 30 static void fixup_blocks(Buffer *buffer)
57 {
58
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 27 times.
30 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 27 Block *lastblk = BLOCK(buffer->blocks.prev);
65 27 BUG_ON(!lastblk);
66 27 size_t n = lastblk->size;
67
2/4
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 27 times.
27 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 42 static bool update_file_info(FileInfo *info, const struct stat *st)
94 {
95 42 *info = (FileInfo) {
96 42 .size = st->st_size,
97 42 .mode = st->st_mode,
98 42 .gid = st->st_gid,
99 42 .uid = st->st_uid,
100 42 .dev = st->st_dev,
101 42 .ino = st->st_ino,
102 42 .mtime = *get_stat_mtime(st),
103 };
104 42 return true;
105 }
106
107 16 static bool buffer_stat(FileInfo *info, const char *filename)
108 {
109 16 struct stat st;
110
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 16 times.
16 return !stat(filename, &st) && update_file_info(info, &st);
111 }
112
113 26 static bool buffer_fstat(FileInfo *info, int fd)
114 {
115 26 struct stat st;
116
2/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 26 times.
26 return !fstat(fd, &st) && update_file_info(info, &st);
117 }
118
119 30 bool read_blocks(Buffer *buffer, int fd, bool utf8_bom)
120 {
121 30 const size_t map_size = 64 * 1024;
122 30 size_t size = buffer->file.size;
123 30 unsigned char *buf = NULL;
124 30 bool mapped = false;
125 30 bool ret = false;
126
127
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 29 times.
30 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 taken 1 times.
✗ Branch 1 not taken.
1 if (buf != MAP_FAILED) {
131 1 advise_sequential(buf, size);
132 1 mapped = true;
133 1 goto decode;
134 }
135 29 buf = NULL;
136 }
137
138
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 5 times.
29 if (likely(size > 0)) {
139 24 buf = malloc(size);
140
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 if (unlikely(!buf)) {
141 goto error;
142 }
143 24 ssize_t rc = xread_all(fd, buf, size);
144
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 if (unlikely(rc < 0)) {
145 goto error;
146 }
147 24 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 not taken.
✓ Branch 1 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 not taken.
✓ Branch 1 taken 7 times.
7 if (rc < 0) {
160 goto error;
161 }
162
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 5 times.
7 if (rc == 0) {
163 break;
164 }
165 2 pos += rc;
166
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 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 30 decode:
184 30 ret = decode_and_add_blocks(buffer, buf, size, utf8_bom);
185
186 30 error:
187
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 29 times.
30 if (mapped) {
188 1 munmap(buf, size);
189 } else {
190 29 free(buf);
191 }
192
193
1/2
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
30 if (ret) {
194 30 fixup_blocks(buffer);
195 }
196
197 30 return ret;
198 }
199
200 // Convert size in bytes to size in MiB (rounded up, for any remainder)
201 79 static uintmax_t filesize_in_mib(uintmax_t nbytes)
202 {
203 79 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 26 bool load_buffer(Buffer *buffer, const char *filename, const GlobalOptions *gopts, bool must_exist)
216 {
217 26 BUG_ON(buffer->abs_filename);
218 26 BUG_ON(!list_empty(&buffer->blocks));
219
220 26 int fd = xopen(filename, O_RDONLY | O_CLOEXEC, 0);
221
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
26 if (fd < 0) {
222 if (errno != ENOENT) {
223 return error_msg("Error opening %s: %s", filename, strerror(errno));
224 }
225 if (must_exist) {
226 return error_msg("File %s does not exist", filename);
227 }
228 if (!buffer->encoding) {
229 buffer->encoding = encoding_from_type(UTF8);
230 buffer->bom = gopts->utf8_bom;
231 }
232 Block *blk = block_new(1);
233 list_insert_before(&blk->node, &buffer->blocks);
234 return true;
235 }
236
237 26 FileInfo *info = &buffer->file;
238
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
26 if (!buffer_fstat(info, fd)) {
239 error_msg("fstat failed on %s: %s", filename, strerror(errno));
240 goto error;
241 }
242
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 25 times.
26 if (!S_ISREG(info->mode)) {
243 1 error_msg("Not a regular file %s", filename);
244 1 goto error;
245 }
246
247 25 off_t size = info->size;
248
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 25 times.
25 if (unlikely(size < 0)) {
249 error_msg("Invalid file size: %jd", (intmax_t)size);
250 goto error;
251 }
252
253 25 unsigned int limit_mib = gopts->filesize_limit;
254
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 if (limit_mib > 0) {
255 25 uintmax_t size_mib = filesize_in_mib(size);
256
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 25 times.
25 if (unlikely(size_mib > limit_mib)) {
257 error_msg (
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 0 not taken.
✓ Branch 1 taken 25 times.
25 if (!read_blocks(buffer, fd, gopts->utf8_bom)) {
266 error_msg("Error reading %s: %s", filename, strerror(errno));
267 goto error;
268 }
269
270 25 BUG_ON(!buffer->encoding);
271 25 xclose(fd);
272 25 return true;
273
274 1 error:
275 1 xclose(fd);
276 1 return false;
277 }
278
279 16 static bool write_buffer (
280 Buffer *buffer,
281 FileEncoder *enc,
282 const ByteOrderMark *bom,
283 int fd
284 ) {
285 16 size_t size = 0;
286
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (bom) {
287 size = bom->len;
288 if (size && xwrite_all(fd, bom->bytes, size) < 0) {
289 return error_msg_errno("write");
290 }
291 }
292
293 16 const Block *blk;
294
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 16 times.
32 block_for_each(blk, &buffer->blocks) {
295 16 ssize_t rc = file_encoder_write(enc, blk->data, blk->size);
296
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (rc < 0) {
297 return error_msg_errno("write");
298 }
299 16 size += rc;
300 }
301
302 16 size_t nr_errors = file_encoder_get_nr_errors(enc);
303
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (nr_errors > 0) {
304 // Any real error hides this message
305 error_msg (
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 not taken.
✓ Branch 1 taken 16 times.
16 if (xftruncate(fd, size)) {
314 return error_msg_errno("ftruncate");
315 }
316
317 return true;
318 }
319
320 16 static int xmkstemp_cloexec(char *path_template)
321 {
322 16 int fd;
323 #if HAVE_MKOSTEMP
324 32 do {
325 16 fd = mkostemp(path_template, O_CLOEXEC);
326
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
16 } while (unlikely(fd == -1 && errno == EINTR));
327 16 return fd;
328 #endif
329
330 do {
331 fd = mkstemp(path_template);
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 int e = errno;
340 xclose(fd);
341 errno = e;
342 return -1;
343 }
344
345 return fd;
346 }
347
348 16 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 0 taken 16 times.
✗ Branch 1 not taken.
16 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 16 const char *base = path_basename(filename);
362 16 const StringView dir = path_slice_dirname(filename);
363 16 const int dlen = (int)dir.length;
364 16 int n = snprintf(buf, buflen, "%.*s/.tmp.%s.XXXXXX", dlen, dir.data, base);
365
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 16 times.
16 if (unlikely(n <= 0 || n >= buflen)) {
366 buf[0] = '\0';
367 return -1;
368 }
369
370 16 int fd = xmkstemp_cloexec(buf);
371
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (fd == -1) {
372 // No write permission to the directory?
373 buf[0] = '\0';
374 return -1;
375 }
376
377
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 2 times.
16 if (!info->mode) {
378 // New file
379
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
14 if (xfchmod(fd, new_file_mode) != 0) {
380 LOG_WARNING("failed to set file mode: %s", strerror(errno));
381 }
382 14 return fd;
383 }
384
385 // Preserve ownership and mode of the original file if possible
386
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (xfchown(fd, info->uid, info->gid) != 0) {
387 LOG_WARNING("failed to preserve file ownership: %s", strerror(errno));
388 }
389
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (xfchmod(fd, info->mode) != 0) {
390 LOG_WARNING("failed to preserve file mode: %s", strerror(errno));
391 }
392
393 return fd;
394 }
395
396 static int xfsync(int fd)
397 {
398 #if HAVE_FSYNC
399 retry:
400 if (fsync(fd) == 0) {
401 return 0;
402 }
403
404 switch (errno) {
405 // EINVAL is ignored because it just means "operation not possible
406 // on this descriptor" rather than indicating an actual error
407 case EINVAL:
408 case ENOTSUP:
409 case ENOSYS:
410 return 0;
411 case EINTR:
412 goto retry;
413 }
414
415 return -1;
416 #else
417 (void)fd;
418 return 0;
419 #endif
420 }
421
422 16 bool save_buffer(Buffer *buffer, const char *filename, const FileSaveContext *ctx)
423 {
424 16 BUG_ON(!ctx->encoding);
425
426 16 char tmp[8192];
427 16 tmp[0] = '\0';
428 16 int fd = -1;
429
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (ctx->hardlinks) {
430 LOG_INFO("target file has hard links; writing in-place");
431 } else {
432 // Try to use temporary file (safer)
433 16 fd = tmp_file(filename, &buffer->file, ctx->new_file_mode, tmp, sizeof(tmp));
434 }
435
436
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (fd < 0) {
437 // Overwrite the original file directly (if it exists).
438 // Ownership is preserved automatically if the file exists.
439 mode_t mode = buffer->file.mode;
440 if (mode == 0) {
441 // New file
442 mode = ctx->new_file_mode;
443 }
444 fd = xopen(filename, O_CREAT | O_TRUNC | O_WRONLY | O_CLOEXEC, mode);
445 if (fd < 0) {
446 return error_msg_errno("open");
447 }
448 }
449
450 16 const ByteOrderMark *bom = NULL;
451
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (unlikely(ctx->write_bom)) {
452 bom = get_bom_for_encoding(lookup_encoding(ctx->encoding));
453 }
454
455 16 FileEncoder enc = file_encoder(ctx->encoding, ctx->crlf, fd);
456
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (!write_buffer(buffer, &enc, bom, fd)) {
457 goto error;
458 }
459
460
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
16 if (buffer->options.fsync && xfsync(fd) != 0) {
461 error_msg_errno("fsync");
462 goto error;
463 }
464
465 16 int r = xclose(fd);
466 16 fd = -1;
467
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (r != 0) {
468 error_msg_errno("close");
469 goto error;
470 }
471
472
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 16 times.
16 if (tmp[0] && rename(tmp, filename)) {
473 error_msg_errno("rename");
474 goto error;
475 }
476
477 16 file_encoder_free(&enc);
478 16 buffer_stat(&buffer->file, filename);
479 16 return true;
480
481 error:
482 xclose(fd);
483 file_encoder_free(&enc);
484
485 if (tmp[0]) {
486 unlink(tmp);
487 } else {
488 // Not using temporary file, therefore mtime may have changed.
489 // Update stat to avoid "File has been modified by someone else"
490 // error later when saving the file again.
491 buffer_stat(&buffer->file, filename);
492 }
493
494 return false;
495 }
496