Line data Source code
1 : #include "wok_write.h"
2 :
3 : #include <errno.h>
4 : #include <stdio.h>
5 : #include <stdlib.h>
6 : #include <string.h>
7 : #include <sys/stat.h>
8 : #include <unistd.h>
9 :
10 : /// [TODO]: Make this into include? As this hash function will be use very often
11 : /// [TODO]: Maybe we can use Fxhash which consume 8 bytes at a time instead of 1
12 109 : WOK_READONLY u64 wok_byte_hash(const char *restrict bytes, usize n) {
13 109 : u64 h = UINT64_C(0xCBF29CE484222325);
14 75599 : for (usize i = 0; i < n; i++) {
15 75490 : h ^= (unsigned char)bytes[i];
16 75490 : h *= UINT64_C(0x100000001B3);
17 : }
18 109 : return h;
19 : }
20 :
21 108 : WokFileStamp wok_file_stamp(const char *path) {
22 108 : WokFileStamp s = {0};
23 108 : struct stat st;
24 108 : if (stat(path, &st) != 0) return s;
25 107 : FILE *fp = fopen(path, "rb");
26 107 : if (!fp) return s;
27 107 : if (fseek(fp, 0, SEEK_END) != 0) {
28 0 : fclose(fp);
29 0 : return s;
30 : }
31 107 : long sz = ftell(fp);
32 107 : if (sz < 0) {
33 0 : fclose(fp);
34 0 : return s;
35 : }
36 107 : rewind(fp);
37 107 : char *buf = (char *)malloc((usize)sz + 1);
38 107 : if (!buf) {
39 0 : fclose(fp);
40 0 : return s;
41 : }
42 107 : usize got = fread(buf, 1, (usize)sz, fp);
43 107 : fclose(fp);
44 107 : s.hash = wok_byte_hash(buf, got);
45 107 : s.size = (u64)got;
46 : #if defined(__APPLE__)
47 : s.mtime_sec = st.st_mtimespec.tv_sec;
48 : s.mtime_nsec = st.st_mtimespec.tv_nsec;
49 : #else
50 107 : s.mtime_sec = st.st_mtim.tv_sec;
51 107 : s.mtime_nsec = st.st_mtim.tv_nsec;
52 : #endif
53 107 : s.valid = true;
54 107 : free(buf);
55 107 : return s;
56 : }
57 :
58 13 : bool wok_write_permitted(WokFileStamp at_read, WokFileStamp now) {
59 : // An unreadable file at either end is not something we may overwrite: we
60 : // cannot show it is the file we formatted.
61 13 : if (!at_read.valid || !now.valid) return false;
62 14 : return at_read.hash == now.hash && at_read.size == now.size;
63 : }
64 :
65 6 : WokWriteResult wok_write_atomic(const char *path, const char *data, usize n,
66 : WokFileStamp at_read) {
67 6 : WokFileStamp now = wok_file_stamp(path);
68 6 : if (!wok_write_permitted(at_read, now)) return WOK_WRITE_MOVED;
69 :
70 : // Already canonical: do not touch the file at all, so mtimes stay meaningful
71 : // and a build system is not woken up by a formatter that changed nothing.
72 3 : if (now.size == (u64)n && wok_byte_hash(data, n) == now.hash)
73 : return WOK_WRITE_UNCHANGED;
74 :
75 : // The temp must share a directory with the target, or rename() crosses a
76 : // filesystem boundary and stops being atomic.
77 2 : usize plen = strlen(path);
78 2 : char *tmp = (char *)malloc(plen + 16);
79 2 : if (!tmp) return WOK_WRITE_FAILED;
80 2 : snprintf(tmp, plen + 16, "%s.wokfmt.tmp", path);
81 :
82 2 : FILE *out = fopen(tmp, "wb");
83 2 : if (!out) {
84 0 : free(tmp);
85 0 : return WOK_WRITE_FAILED;
86 : }
87 2 : bool ok = fwrite(data, 1, n, out) == n;
88 2 : if (fclose(out) != 0) ok = false;
89 :
90 2 : if (ok) {
91 2 : struct stat st;
92 2 : if (stat(path, &st) == 0) chmod(tmp, st.st_mode & 07777);
93 : // Last check before the swap. The window between here and rename() cannot
94 : // be closed without a filesystem compare-and-swap, which POSIX does not
95 : // provide; it is now bounded by a single syscall rather than by the whole
96 : // format.
97 2 : WokFileStamp again = wok_file_stamp(path);
98 2 : if (!wok_write_permitted(at_read, again)) {
99 0 : unlink(tmp);
100 0 : free(tmp);
101 0 : return WOK_WRITE_MOVED;
102 : }
103 2 : if (rename(tmp, path) != 0) ok = false;
104 : }
105 :
106 2 : if (!ok) {
107 0 : unlink(tmp); // the original is untouched either way
108 0 : free(tmp);
109 0 : return WOK_WRITE_FAILED;
110 : }
111 2 : free(tmp);
112 2 : return WOK_WRITE_OK;
113 : }
|