dte test coverage


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 50.0% high: ≥ 85.0%
Coverage Exec / Excl / Total
Lines: 63.0% 155 / 0 / 246
Functions: 100.0% 12 / 0 / 12
Branches: 45.0% 63 / 10 / 150

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 "util/debug.h"
15 #include "util/fd.h"
16 #include "util/list.h"
17 #include "util/log.h"
18 #include "util/numtostr.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 const GlobalOptions *gopts,
29 StringView text,
30 size_t *longest_line
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, gopts->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 = gopts->utf8_bom;
55 }
56
57 34 return file_decoder_read(buffer, text, longest_line);
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 = buffer_get_last_block(buffer);
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 (
109 Buffer *buffer,
110 const GlobalOptions *gopts,
111 int fd,
112 size_t *longest_line
113 ) {
114 34 const size_t map_size = 64 * 1024;
115 34 size_t size = buffer->file.size;
116 34 char *text = NULL;
117 34 bool mapped = false;
118 34 bool ret = false;
119
120
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 33 times.
34 if (size >= map_size) {
121 // NOTE: size must be greater than 0
122 1 text = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
123
1/2
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 7 not taken.
1 if (text != MAP_FAILED) {
124 1 advise_sequential(text, size);
125 1 mapped = true;
126 1 goto decode;
127 }
128 LOG_ERRNO("mmap() failed");
129 text = NULL;
130 }
131
132
2/2
✓ Branch 8 → 9 taken 25 times.
✓ Branch 8 → 15 taken 8 times.
33 if (likely(size > 0)) {
133 25 text = malloc(size);
134
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 25 times.
25 if (unlikely(!text)) {
135 goto error;
136 }
137 25 ssize_t rc = xread_all(fd, text, size);
138
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 25 times.
25 if (unlikely(rc < 0)) {
139 goto error;
140 }
141 25 size = rc;
142 } else {
143 // st_size is zero for some files in /proc
144 8 size_t alloc = map_size;
145 8 BUG_ON(!IS_POWER_OF_2(alloc));
146 8 text = malloc(alloc);
147
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 18 taken 8 times.
8 if (unlikely(!text)) {
148 goto error;
149 }
150 size_t pos = 0;
151 10 while (1) {
152 10 ssize_t rc = xread_all(fd, text + pos, alloc - pos);
153
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 10 times.
10 if (rc < 0) {
154 goto error;
155 }
156
2/2
✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 27 taken 8 times.
10 if (rc == 0) {
157 break;
158 }
159 2 pos += rc;
160
1/2
✓ Branch 22 → 17 taken 2 times.
✗ Branch 22 → 23 not taken.
2 if (pos == alloc) {
161 size_t new_alloc = alloc << 1;
162 if (unlikely(alloc >= new_alloc)) {
163 errno = EOVERFLOW;
164 goto error;
165 }
166 alloc = new_alloc;
167 char *new_text = realloc(text, alloc);
168 if (unlikely(!new_text)) {
169 goto error;
170 }
171 text = new_text;
172 }
173 }
174 size = pos;
175 }
176
177 34 decode:
178 34 ret = decode_and_add_blocks(buffer, gopts, string_view(text, size), longest_line);
179
180 34 error:
181
2/2
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 32 taken 33 times.
34 if (mapped) {
182 1 int r = munmap(text, size); // Can only fail due to usage error
183
1/2
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 33 taken 1 time.
1 LOG_ERRNO_ON(r, "munmap");
184 } else {
185 33 free(text);
186 }
187
188
1/2
✓ Branch 33 → 34 taken 34 times.
✗ Branch 33 → 35 not taken.
34 if (ret) {
189 34 fixup_blocks(buffer);
190 }
191
192 34 return ret;
193 }
194
195 78 static bool size_exceeds_limit ( // NOLINT(readability-function-size)
196 ErrorBuffer *ebuf,
197 const char *filename,
198 const char *size_name,
199 const char *limit_name,
200 const char *extra_msg,
201 uintmax_t size,
202 uintmax_t limit
203 ) {
204
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 7 taken 78 times.
78 if (likely(!limit || size <= limit)) {
205 return false;
206 }
207
208 char limit_str[PRECISE_FILESIZE_STR_MAX];
209 char size_str[HRSIZE_MAX];
210 filesize_to_str_precise(limit, limit_str);
211 human_readable_size(size, size_str);
212
213 error_msg (
214 ebuf,
215 "%s size (%s) exceeds '%s' option (%s)%s: %s",
216 size_name, size_str, limit_name, limit_str, extra_msg, filename
217 );
218
219 return true;
220 }
221
222 27 bool load_buffer (
223 Buffer *buffer,
224 const char *filename,
225 const GlobalOptions *gopts,
226 ErrorBuffer *ebuf,
227 bool must_exist
228 ) {
229 27 BUG_ON(buffer->abs_filename);
230 27 BUG_ON(!list_empty(&buffer->blocks));
231
232 27 int fd = xopen(filename, O_RDONLY | O_CLOEXEC, 0);
233
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 19 taken 27 times.
27 if (fd < 0) {
234 if (errno != ENOENT) {
235 return error_msg(ebuf, "Error opening %s: %s", filename, strerror(errno));
236 }
237 if (must_exist) {
238 return error_msg(ebuf, "File %s does not exist", filename);
239 }
240 if (!buffer->encoding) {
241 buffer->encoding = encoding_from_type(UTF8);
242 buffer->bom = gopts->utf8_bom;
243 }
244 Block *blk = block_new(1);
245 list_insert_before(&blk->node, &buffer->blocks);
246 return true;
247 }
248
249 27 FileInfo *info = &buffer->file;
250
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 24 taken 27 times.
27 if (!buffer_fstat(info, fd)) {
251 error_msg(ebuf, "fstat failed on %s: %s", filename, strerror(errno));
252 goto error;
253 }
254
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 27 taken 26 times.
27 if (!S_ISREG(info->mode)) {
255 1 error_msg(ebuf, "Not a regular file %s", filename);
256 1 goto error;
257 }
258
259
1/2
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 30 taken 26 times.
26 if (unlikely(info->size < 0)) {
260 error_msg(ebuf, "Invalid file size: %jd", (intmax_t)info->size);
261 goto error;
262 }
263
264 26 uintmax_t size = info->size;
265 26 uintmax_t fslimit = gopts->filesize_limit;
266
1/2
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 26 times.
26 if (size_exceeds_limit(ebuf, filename, "File", "filesize-limit", "", size, fslimit)) {
267 goto error;
268 }
269
270 26 size_t longest_line;
271
1/2
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 38 taken 26 times.
26 if (!read_blocks(buffer, gopts, fd, &longest_line)) {
272 error_msg(ebuf, "Error reading %s: %s", filename, strerror(errno));
273 goto error;
274 }
275
276 26 static const char msg[] = ", setting syntax=false";
277 26 uintmax_t sslimit = gopts->syntax_size_limit;
278 26 uintmax_t sllimit = gopts->syntax_line_limit;
279 26 if (
280
1/2
✓ Branch 38 → 39 taken 26 times.
✗ Branch 38 → 44 not taken.
26 buffer->options.syntax
281
1/2
✓ Branch 40 → 41 taken 26 times.
✗ Branch 40 → 43 not taken.
26 && (
282 26 size_exceeds_limit(ebuf, filename, "File", "syntax-size-limit", msg, size, sslimit)
283
1/2
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 26 times.
26 || size_exceeds_limit(ebuf, filename, "Longest line", "syntax-line-limit", msg, longest_line, sllimit)
284 )
285 ) {
286 buffer->options.syntax = false;
287 }
288
289 26 BUG_ON(!buffer->encoding);
290 26 xclose(fd);
291 26 return true;
292
293 1 error:
294 1 xclose(fd);
295 1 return false;
296 }
297
298 22 static bool write_buffer(const Buffer *buffer, const FileSaveContext *ctx, int fd)
299 {
300 22 ErrorBuffer *ebuf = ctx->ebuf;
301 22 FileEncoder enc = file_encoder(ctx->encoding, ctx->crlf, fd);
302 22 size_t size = 0;
303
304
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 12 taken 22 times.
22 if (unlikely(ctx->write_bom)) {
305 const EncodingType type = lookup_encoding(ctx->encoding);
306 const ByteOrderMark *bom = get_bom_for_encoding(type);
307 if (bom->len && xwrite_all(fd, bom->bytes, bom->len) < 0) {
308 file_encoder_free(&enc);
309 return error_msg_errno(ebuf, "write");
310 }
311 size += bom->len;
312 }
313
314 22 const Block *blk;
315
2/2
✓ Branch 18 → 13 taken 22 times.
✓ Branch 18 → 19 taken 22 times.
44 block_for_each(blk, &buffer->blocks) {
316 22 ssize_t rc = file_encoder_write(&enc, blk->data, blk->size, blk->nl);
317
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 17 taken 22 times.
22 if (rc < 0) {
318 file_encoder_free(&enc);
319 return error_msg_errno(ebuf, "write");
320 }
321 22 size += rc;
322 }
323
324 22 size_t nr_errors = file_encoder_get_nr_errors(&enc);
325 22 file_encoder_free(&enc);
326
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 25 taken 22 times.
22 if (nr_errors > 0) {
327 // Any real error hides this message
328 error_msg (
329 ebuf,
330 "Warning: %zu non-reversible character conversion%s; file saved",
331 nr_errors,
332 (nr_errors > 1) ? "s" : ""
333 );
334 }
335
336 // Need to truncate if writing to existing file
337
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 22 times.
22 if (xftruncate(fd, size)) {
338 return error_msg_errno(ebuf, "ftruncate");
339 }
340
341 return true;
342 }
343
344 22 static int xmkstemp_cloexec(char *path_template)
345 {
346 22 int fd;
347 #if HAVE_MKOSTEMP
348 44 do {
349 22 fd = mkostemp(path_template, O_CLOEXEC);
350
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));
351 22 return fd;
352 #endif
353
354 do {
355 fd = mkstemp(path_template); // NOLINT(*-unsafe-functions)
356 } while (unlikely(fd == -1 && errno == EINTR));
357
358 if (fd == -1) {
359 return fd;
360 }
361
362 if (unlikely(!fd_set_cloexec(fd, true))) {
363 xclose(fd);
364 return -1;
365 }
366
367 return fd;
368 }
369
370 22 static int tmp_file (
371 const char *filename,
372 const FileInfo *info,
373 mode_t new_file_mode,
374 char *buf,
375 size_t buflen
376 ) {
377
1/2
✓ Branch 2 → 3 taken 22 times.
✗ Branch 2 → 25 not taken.
22 if (str_has_prefix(filename, "/tmp/")) {
378 // Don't use temporary file when saving file in /tmp because crontab
379 // command doesn't like the file to be replaced
380 return -1;
381 }
382
383 22 const StringView dir = path_slice_dirname(filename);
384 22 const StringView base = path_slice_basename(strview(filename));
385 22 size_t required_buflen = dir.length + base.length + sizeof("/.tmp..XXXXXX");
386
387
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 7 taken 22 times.
22 if (unlikely(buflen < required_buflen)) {
388 LOG_ERROR("%s() buffer of size %zu insufficient", __func__, buflen);
389 buf[0] = '\0';
390 return -1;
391 }
392
393 // "<dir>/.tmp.<base>.XXXXXX"
394 22 xmempcpy4 (
395 buf,
396 22 dir.data, dir.length,
397 STRN("/.tmp."),
398 22 base.data, base.length,
399 STRN(".XXXXXX") + 1
400 );
401
402 22 int fd = xmkstemp_cloexec(buf);
403
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 12 taken 22 times.
22 if (fd == -1) {
404 // No write permission to the directory?
405 LOG_ERRNO("xmkstemp_cloexec() failed");
406 buf[0] = '\0';
407 return -1;
408 }
409
410
2/2
✓ Branch 12 → 13 taken 20 times.
✓ Branch 12 → 17 taken 2 times.
22 if (!info->mode) {
411 // New file
412
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 25 taken 20 times.
20 if (xfchmod(fd, new_file_mode) != 0) {
413 LOG_WARNING("failed to set file mode: %s", strerror(errno));
414 }
415 return fd;
416 }
417
418 // Preserve ownership and mode of the original file if possible
419
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 2 times.
2 if (xfchown(fd, info->uid, info->gid) != 0) {
420 LOG_WARNING("failed to preserve file ownership: %s", strerror(errno));
421 }
422
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 25 taken 2 times.
2 if (xfchmod(fd, info->mode) != 0) {
423 LOG_WARNING("failed to preserve file mode: %s", strerror(errno));
424 }
425
426 return fd;
427 }
428
429 22 bool save_buffer(Buffer *buffer, const char *filename, const FileSaveContext *ctx)
430 {
431 22 ErrorBuffer *ebuf = ctx->ebuf;
432 22 BUG_ON(!ctx->encoding);
433 22 char tmp[8192];
434 22 tmp[0] = '\0';
435 22 int fd = -1;
436
437
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 22 times.
22 if (ctx->hardlinks) {
438 LOG_INFO("target file has hard links; writing in-place");
439 } else {
440 // Try to use temporary file (safer)
441 22 fd = tmp_file(filename, &buffer->file, ctx->new_file_mode, tmp, sizeof(tmp));
442 }
443
444
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 13 taken 22 times.
22 if (fd < 0) {
445 // Overwrite the original file directly (if it exists).
446 // Ownership is preserved automatically if the file exists.
447 mode_t mode = buffer->file.mode;
448 if (mode == 0) {
449 // New file
450 mode = ctx->new_file_mode;
451 }
452 fd = xopen(filename, O_CREAT | O_TRUNC | O_WRONLY | O_CLOEXEC, mode);
453 if (fd < 0) {
454 return error_msg_errno(ebuf, "open");
455 }
456 }
457
458
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 22 times.
22 if (!write_buffer(buffer, ctx, fd)) {
459 goto error;
460 }
461
462
1/4
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 21 taken 22 times.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 21 not taken.
22 if (buffer->options.fsync && xfsync(fd) != 0) {
463 error_msg_errno(ebuf, "fsync");
464 goto error;
465 }
466
467 22 SystemErrno err = xclose(fd);
468 22 fd = -1;
469
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 26 taken 22 times.
22 if (err != 0) {
470 error_msg(ebuf, "close: %s", strerror(err));
471 goto error;
472 }
473
474
2/4
✓ Branch 26 → 27 taken 22 times.
✗ Branch 26 → 31 not taken.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 31 taken 22 times.
22 if (tmp[0] && rename(tmp, filename)) {
475 error_msg_errno(ebuf, "rename");
476 goto error;
477 }
478
479 22 buffer_stat(&buffer->file, filename);
480 22 return true;
481
482 error:
483 xclose(fd);
484 if (tmp[0]) {
485 int r = unlink(tmp);
486 LOG_ERRNO_ON(r, "unlink");
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 return false;
494 }
495