dte test coverage


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 50.0% high: ≥ 85.0%
Coverage Exec / Excl / Total
Lines: 62.5% 145 / 0 / 232
Functions: 100.0% 11 / 0 / 11
Branches: 44.7% 59 / 10 / 142

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 ErrorBuffer *errbuf,
30 StringView text
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, gopts, errbuf, text);
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 = BLOCK(buffer->blocks.prev);
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 ErrorBuffer *ebuf,
112 int fd
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, ebuf, string_view(text, size));
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 27 bool load_buffer (
196 Buffer *buffer,
197 const char *filename,
198 const GlobalOptions *gopts,
199 ErrorBuffer *ebuf,
200 bool must_exist
201 ) {
202 27 BUG_ON(buffer->abs_filename);
203 27 BUG_ON(!list_empty(&buffer->blocks));
204
205 27 int fd = xopen(filename, O_RDONLY | O_CLOEXEC, 0);
206
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 19 taken 27 times.
27 if (fd < 0) {
207 if (errno != ENOENT) {
208 return error_msg(ebuf, "Error opening %s: %s", filename, strerror(errno));
209 }
210 if (must_exist) {
211 return error_msg(ebuf, "File %s does not exist", filename);
212 }
213 if (!buffer->encoding) {
214 buffer->encoding = encoding_from_type(UTF8);
215 buffer->bom = gopts->utf8_bom;
216 }
217 Block *blk = block_new(1);
218 list_insert_before(&blk->node, &buffer->blocks);
219 return true;
220 }
221
222 27 FileInfo *info = &buffer->file;
223
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 24 taken 27 times.
27 if (!buffer_fstat(info, fd)) {
224 error_msg(ebuf, "fstat failed on %s: %s", filename, strerror(errno));
225 goto error;
226 }
227
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 27 taken 26 times.
27 if (!S_ISREG(info->mode)) {
228 1 error_msg(ebuf, "Not a regular file %s", filename);
229 1 goto error;
230 }
231
232 26 off_t size = info->size;
233
1/2
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 30 taken 26 times.
26 if (unlikely(size < 0)) {
234 error_msg(ebuf, "Invalid file size: %jd", (intmax_t)size);
235 goto error;
236 }
237
238 26 uintmax_t size_limit = gopts->filesize_limit;
239
1/2
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 34 taken 26 times.
26 if (unlikely(size_limit && (uintmax_t)size > size_limit)) {
240 char limit_str[PRECISE_FILESIZE_STR_MAX];
241 filesize_to_str_precise(size_limit, limit_str);
242 error_msg (
243 ebuf,
244 "File size (%ju) exceeds 'filesize-limit' option (%s): %s",
245 (uintmax_t)size, limit_str, filename
246 );
247 goto error;
248 }
249
250
1/2
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 39 taken 26 times.
26 if (!read_blocks(buffer, gopts, ebuf, fd)) {
251 error_msg(ebuf, "Error reading %s: %s", filename, strerror(errno));
252 goto error;
253 }
254
255 26 BUG_ON(!buffer->encoding);
256 26 xclose(fd);
257 26 return true;
258
259 1 error:
260 1 xclose(fd);
261 1 return false;
262 }
263
264 22 static bool write_buffer(const Buffer *buffer, const FileSaveContext *ctx, int fd)
265 {
266 22 ErrorBuffer *ebuf = ctx->ebuf;
267 22 FileEncoder enc = file_encoder(ctx->encoding, ctx->crlf, fd);
268 22 size_t size = 0;
269
270
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 12 taken 22 times.
22 if (unlikely(ctx->write_bom)) {
271 const EncodingType type = lookup_encoding(ctx->encoding);
272 const ByteOrderMark *bom = get_bom_for_encoding(type);
273 if (bom->len && xwrite_all(fd, bom->bytes, bom->len) < 0) {
274 file_encoder_free(&enc);
275 return error_msg_errno(ebuf, "write");
276 }
277 size += bom->len;
278 }
279
280 22 const Block *blk;
281
2/2
✓ Branch 18 → 13 taken 22 times.
✓ Branch 18 → 19 taken 22 times.
44 block_for_each(blk, &buffer->blocks) {
282 22 ssize_t rc = file_encoder_write(&enc, blk->data, blk->size, blk->nl);
283
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 17 taken 22 times.
22 if (rc < 0) {
284 file_encoder_free(&enc);
285 return error_msg_errno(ebuf, "write");
286 }
287 22 size += rc;
288 }
289
290 22 size_t nr_errors = file_encoder_get_nr_errors(&enc);
291 22 file_encoder_free(&enc);
292
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 25 taken 22 times.
22 if (nr_errors > 0) {
293 // Any real error hides this message
294 error_msg (
295 ebuf,
296 "Warning: %zu non-reversible character conversion%s; file saved",
297 nr_errors,
298 (nr_errors > 1) ? "s" : ""
299 );
300 }
301
302 // Need to truncate if writing to existing file
303
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 22 times.
22 if (xftruncate(fd, size)) {
304 return error_msg_errno(ebuf, "ftruncate");
305 }
306
307 return true;
308 }
309
310 22 static int xmkstemp_cloexec(char *path_template)
311 {
312 22 int fd;
313 #if HAVE_MKOSTEMP
314 44 do {
315 22 fd = mkostemp(path_template, O_CLOEXEC);
316
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));
317 22 return fd;
318 #endif
319
320 do {
321 fd = mkstemp(path_template); // NOLINT(*-unsafe-functions)
322 } while (unlikely(fd == -1 && errno == EINTR));
323
324 if (fd == -1) {
325 return fd;
326 }
327
328 if (unlikely(!fd_set_cloexec(fd, true))) {
329 xclose(fd);
330 return -1;
331 }
332
333 return fd;
334 }
335
336 22 static int tmp_file (
337 const char *filename,
338 const FileInfo *info,
339 mode_t new_file_mode,
340 char *buf,
341 size_t buflen
342 ) {
343
1/2
✓ Branch 2 → 3 taken 22 times.
✗ Branch 2 → 25 not taken.
22 if (str_has_prefix(filename, "/tmp/")) {
344 // Don't use temporary file when saving file in /tmp because crontab
345 // command doesn't like the file to be replaced
346 return -1;
347 }
348
349 22 const StringView dir = path_slice_dirname(filename);
350 22 const StringView base = strview(path_basename(filename));
351 22 size_t required_buflen = dir.length + base.length + sizeof("/.tmp..XXXXXX");
352
353
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 22 times.
22 if (unlikely(buflen < required_buflen)) {
354 LOG_ERROR("%s() buffer of size %zu insufficient", __func__, buflen);
355 buf[0] = '\0';
356 return -1;
357 }
358
359 // "<dir>/.tmp.<base>.XXXXXX"
360 22 xmempcpy4 (
361 buf,
362 22 dir.data, dir.length,
363 STRN("/.tmp."),
364 22 base.data, base.length,
365 STRN(".XXXXXX") + 1
366 );
367
368 22 int fd = xmkstemp_cloexec(buf);
369
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 11 taken 22 times.
22 if (fd == -1) {
370 // No write permission to the directory?
371 LOG_ERRNO("xmkstemp_cloexec() failed");
372 buf[0] = '\0';
373 return -1;
374 }
375
376
2/2
✓ Branch 11 → 12 taken 20 times.
✓ Branch 11 → 17 taken 2 times.
22 if (!info->mode) {
377 // New file
378
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 16 taken 20 times.
20 if (xfchmod(fd, new_file_mode) != 0) {
379 LOG_WARNING("failed to set file mode: %s", strerror(errno));
380 }
381 20 return fd;
382 }
383
384 // Preserve ownership and mode of the original file if possible
385
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 2 times.
2 if (xfchown(fd, info->uid, info->gid) != 0) {
386 LOG_WARNING("failed to preserve file ownership: %s", strerror(errno));
387 }
388
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 25 taken 2 times.
2 if (xfchmod(fd, info->mode) != 0) {
389 LOG_WARNING("failed to preserve file mode: %s", strerror(errno));
390 }
391
392 return fd;
393 }
394
395 22 bool save_buffer(Buffer *buffer, const char *filename, const FileSaveContext *ctx)
396 {
397 22 ErrorBuffer *ebuf = ctx->ebuf;
398 22 BUG_ON(!ctx->encoding);
399 22 char tmp[8192];
400 22 tmp[0] = '\0';
401 22 int fd = -1;
402
403
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 22 times.
22 if (ctx->hardlinks) {
404 LOG_INFO("target file has hard links; writing in-place");
405 } else {
406 // Try to use temporary file (safer)
407 22 fd = tmp_file(filename, &buffer->file, ctx->new_file_mode, tmp, sizeof(tmp));
408 }
409
410
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 13 taken 22 times.
22 if (fd < 0) {
411 // Overwrite the original file directly (if it exists).
412 // Ownership is preserved automatically if the file exists.
413 mode_t mode = buffer->file.mode;
414 if (mode == 0) {
415 // New file
416 mode = ctx->new_file_mode;
417 }
418 fd = xopen(filename, O_CREAT | O_TRUNC | O_WRONLY | O_CLOEXEC, mode);
419 if (fd < 0) {
420 return error_msg_errno(ebuf, "open");
421 }
422 }
423
424
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 22 times.
22 if (!write_buffer(buffer, ctx, fd)) {
425 goto error;
426 }
427
428
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) {
429 error_msg_errno(ebuf, "fsync");
430 goto error;
431 }
432
433 22 SystemErrno err = xclose(fd);
434 22 fd = -1;
435
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 26 taken 22 times.
22 if (err != 0) {
436 error_msg(ebuf, "close: %s", strerror(err));
437 goto error;
438 }
439
440
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)) {
441 error_msg_errno(ebuf, "rename");
442 goto error;
443 }
444
445 22 buffer_stat(&buffer->file, filename);
446 22 return true;
447
448 error:
449 xclose(fd);
450 if (tmp[0]) {
451 int r = unlink(tmp);
452 LOG_ERRNO_ON(r, "unlink");
453 } else {
454 // Not using temporary file, therefore mtime may have changed.
455 // Update stat to avoid "File has been modified by someone else"
456 // error later when saving the file again.
457 buffer_stat(&buffer->file, filename);
458 }
459 return false;
460 }
461