fortrangoingonforty/fgof-watch / 1a5c74c

Browse files

Prune ignored subtrees

Authored by espadonne
SHA
1a5c74c897d3c0655981e2b3ebfc2890f6b46ed8
Parents
a377eef
Tree
a8af1eb

3 changed files

StatusFile+-
M src/fgof_watch.f90 65 2
M src/fgof_watch_poll.c 142 4
M test/test_watch_errors.f90 79 29
src/fgof_watch.f90modified
@@ -22,10 +22,14 @@ module fgof_watch
2222
   public :: set_ignore_prefixes
2323
 
2424
   interface
25
-    integer(c_int) function fgof_watch_collect_snapshot_c(root, recursive, buffer, buffer_len) bind(C, name="fgof_watch_collect_snapshot")
25
+    integer(c_int) function fgof_watch_collect_snapshot_c(root, recursive, ignore_hidden, prefix_count, prefix_stride, prefixes, buffer, buffer_len) bind(C, name="fgof_watch_collect_snapshot")
2626
       import :: c_char, c_int, c_ptr, c_size_t
2727
       character(kind=c_char), intent(in) :: root(*)
2828
       integer(c_int), value :: recursive
29
+      integer(c_int), value :: ignore_hidden
30
+      integer(c_int), value :: prefix_count
31
+      integer(c_int), value :: prefix_stride
32
+      character(kind=c_char), intent(in) :: prefixes(*)
2933
       type(c_ptr), intent(out) :: buffer
3034
       integer(c_size_t), intent(out) :: buffer_len
3135
     end function fgof_watch_collect_snapshot_c
@@ -179,16 +183,29 @@ contains
179183
     integer(c_int) :: status
180184
     integer(c_size_t) :: raw_len
181185
     character(kind=c_char), allocatable :: c_root(:)
186
+    character(kind=c_char), allocatable :: c_prefixes(:)
187
+    integer(c_int) :: prefix_count
188
+    integer(c_int) :: prefix_stride
182189
     character(kind=c_char), pointer :: raw_chars(:)
183190
     character(len=:), allocatable :: text
184191
 
185192
     c_root = to_c_string(root)
193
+    call pack_ignore_prefixes(options, prefix_count, prefix_stride, c_prefixes)
186194
     raw_ptr = c_null_ptr
187195
     raw_len = 0_c_size_t
188196
     status_code = 0
189197
     status_message = ""
190198
 
191
-    status = fgof_watch_collect_snapshot_c(c_root, merge(1_c_int, 0_c_int, options%recursive), raw_ptr, raw_len)
199
+    status = fgof_watch_collect_snapshot_c( &
200
+      c_root, &
201
+      merge(1_c_int, 0_c_int, options%recursive), &
202
+      merge(1_c_int, 0_c_int, options%ignore_hidden), &
203
+      prefix_count, &
204
+      prefix_stride, &
205
+      c_prefixes, &
206
+      raw_ptr, &
207
+      raw_len &
208
+    )
192209
     if (status /= 0_c_int) then
193210
       allocate(entries(0))
194211
       if (c_associated(raw_ptr)) call fgof_watch_free_buffer_c(raw_ptr)
@@ -212,6 +229,45 @@ contains
212229
     call sort_entries(entries)
213230
   end subroutine collect_snapshot
214231
 
232
+  subroutine pack_ignore_prefixes(options, count, stride, buffer)
233
+    type(watch_options), intent(in) :: options
234
+    integer(c_int), intent(out) :: count
235
+    integer(c_int), intent(out) :: stride
236
+    character(kind=c_char), allocatable, intent(out) :: buffer(:)
237
+    integer :: i
238
+    integer :: j
239
+    integer :: width
240
+    integer :: offset
241
+
242
+    if (.not. allocated(options%ignore_prefixes)) then
243
+      count = 0_c_int
244
+      stride = 0_c_int
245
+      buffer = empty_c_string()
246
+      return
247
+    end if
248
+
249
+    if (size(options%ignore_prefixes) == 0) then
250
+      count = 0_c_int
251
+      stride = 0_c_int
252
+      buffer = empty_c_string()
253
+      return
254
+    end if
255
+
256
+    width = max_string_length(options%ignore_prefixes) + 1
257
+    count = int(size(options%ignore_prefixes), c_int)
258
+    stride = int(width, c_int)
259
+    allocate(buffer(size(options%ignore_prefixes) * width))
260
+    buffer = c_null_char
261
+
262
+    do i = 1, size(options%ignore_prefixes)
263
+      offset = (i - 1) * width
264
+      do j = 1, len_trim(options%ignore_prefixes(i))
265
+        buffer(offset + j) = options%ignore_prefixes(i)(j:j)
266
+      end do
267
+      buffer(offset + len_trim(options%ignore_prefixes(i)) + 1) = c_null_char
268
+    end do
269
+  end subroutine pack_ignore_prefixes
270
+
215271
   subroutine filter_entries(root, options, entries)
216272
     character(len=*), intent(in) :: root
217273
     type(watch_options), intent(in) :: options
@@ -820,6 +876,13 @@ contains
820876
     buf(n + 1) = c_null_char
821877
   end function to_c_string
822878
 
879
+  function empty_c_string() result(buf)
880
+    character(kind=c_char), allocatable :: buf(:)
881
+
882
+    allocate(buf(1))
883
+    buf(1) = c_null_char
884
+  end function empty_c_string
885
+
823886
   subroutine append_entry(entries, entry)
824887
     type(watch_entry), allocatable, intent(inout) :: entries(:)
825888
     type(watch_entry), intent(in) :: entry
src/fgof_watch_poll.cmodified
@@ -14,6 +14,101 @@ typedef struct {
1414
     size_t cap;
1515
 } fgof_watch_buffer;
1616
 
17
+static const char *fgof_watch_basename(const char *path) {
18
+    const char *slash;
19
+
20
+    slash = strrchr(path, '/');
21
+    if (slash == NULL) {
22
+        return path;
23
+    }
24
+    return slash + 1;
25
+}
26
+
27
+static const char *fgof_watch_relative_path(const char *root, const char *path) {
28
+    size_t root_len;
29
+
30
+    if (strcmp(path, root) == 0) {
31
+        return fgof_watch_basename(root);
32
+    }
33
+
34
+    root_len = strlen(root);
35
+    if (strncmp(path, root, root_len) == 0 && path[root_len] == '/') {
36
+        return path + root_len + 1;
37
+    }
38
+
39
+    return path;
40
+}
41
+
42
+static int fgof_watch_contains_hidden_segment(const char *path) {
43
+    const char *segment_start;
44
+    const char *cursor;
45
+
46
+    if (path == NULL || path[0] == '\0') {
47
+        return 0;
48
+    }
49
+
50
+    segment_start = path;
51
+    for (cursor = path; ; ++cursor) {
52
+        if (*cursor != '/' && *cursor != '\0') {
53
+            continue;
54
+        }
55
+        if (cursor > segment_start && segment_start[0] == '.') {
56
+            return 1;
57
+        }
58
+        if (*cursor == '\0') {
59
+            break;
60
+        }
61
+        segment_start = cursor + 1;
62
+    }
63
+
64
+    return 0;
65
+}
66
+
67
+static int fgof_watch_matches_prefix(const char *path, int prefix_count, int prefix_stride, const char *prefixes) {
68
+    int i;
69
+    const char *prefix;
70
+    size_t prefix_len;
71
+
72
+    if (prefix_count <= 0 || prefix_stride <= 0 || prefixes == NULL) {
73
+        return 0;
74
+    }
75
+
76
+    for (i = 0; i < prefix_count; ++i) {
77
+        prefix = prefixes + (i * prefix_stride);
78
+        prefix_len = strlen(prefix);
79
+        if (prefix_len == 0) {
80
+            continue;
81
+        }
82
+        if (strcmp(path, prefix) == 0) {
83
+            return 1;
84
+        }
85
+        if (strncmp(path, prefix, prefix_len) == 0 && path[prefix_len] == '/') {
86
+            return 1;
87
+        }
88
+    }
89
+
90
+    return 0;
91
+}
92
+
93
+static int fgof_watch_should_ignore(
94
+    const char *root,
95
+    const char *path,
96
+    int ignore_hidden,
97
+    int prefix_count,
98
+    int prefix_stride,
99
+    const char *prefixes
100
+) {
101
+    if (ignore_hidden && fgof_watch_contains_hidden_segment(fgof_watch_relative_path(root, path))) {
102
+        return 1;
103
+    }
104
+
105
+    if (fgof_watch_matches_prefix(path, prefix_count, prefix_stride, prefixes)) {
106
+        return 1;
107
+    }
108
+
109
+    return 0;
110
+}
111
+
17112
 static int fgof_watch_append_text(fgof_watch_buffer *buffer, const char *text, size_t text_len) {
18113
     char *grown;
19114
     size_t needed;
@@ -86,11 +181,25 @@ static int fgof_watch_append_entry(fgof_watch_buffer *buffer, const char *path,
86181
     return fgof_watch_append_text(buffer, line, (size_t)line_len);
87182
 }
88183
 
89
-static int fgof_watch_visit(const char *path, int recursive, int depth, fgof_watch_buffer *buffer) {
184
+static int fgof_watch_visit(
185
+    const char *root,
186
+    const char *path,
187
+    int recursive,
188
+    int depth,
189
+    int ignore_hidden,
190
+    int prefix_count,
191
+    int prefix_stride,
192
+    const char *prefixes,
193
+    fgof_watch_buffer *buffer
194
+) {
90195
     DIR *dirp;
91196
     struct dirent *entry;
92197
     struct stat st;
93198
 
199
+    if (fgof_watch_should_ignore(root, path, ignore_hidden, prefix_count, prefix_stride, prefixes)) {
200
+        return 0;
201
+    }
202
+
94203
     if (lstat(path, &st) != 0) {
95204
         if (errno == ENOENT || errno == ENOTDIR) {
96205
             return 0;
@@ -135,7 +244,17 @@ static int fgof_watch_visit(const char *path, int recursive, int depth, fgof_wat
135244
         }
136245
 
137246
         snprintf(child, child_len, "%s/%s", path, entry->d_name);
138
-        status = fgof_watch_visit(child, recursive, depth + 1, buffer);
247
+        status = fgof_watch_visit(
248
+            root,
249
+            child,
250
+            recursive,
251
+            depth + 1,
252
+            ignore_hidden,
253
+            prefix_count,
254
+            prefix_stride,
255
+            prefixes,
256
+            buffer
257
+        );
139258
         free(child);
140259
 
141260
         if (status != 0) {
@@ -148,7 +267,16 @@ static int fgof_watch_visit(const char *path, int recursive, int depth, fgof_wat
148267
     return 0;
149268
 }
150269
 
151
-int fgof_watch_collect_snapshot(const char *root, int recursive, char **buffer_out, size_t *buffer_len_out) {
270
+int fgof_watch_collect_snapshot(
271
+    const char *root,
272
+    int recursive,
273
+    int ignore_hidden,
274
+    int prefix_count,
275
+    int prefix_stride,
276
+    const char *prefixes,
277
+    char **buffer_out,
278
+    size_t *buffer_len_out
279
+) {
152280
     fgof_watch_buffer buffer;
153281
     int status;
154282
 
@@ -163,7 +291,17 @@ int fgof_watch_collect_snapshot(const char *root, int recursive, char **buffer_o
163291
         return 0;
164292
     }
165293
 
166
-    status = fgof_watch_visit(root, recursive != 0, 0, &buffer);
294
+    status = fgof_watch_visit(
295
+        root,
296
+        root,
297
+        recursive != 0,
298
+        0,
299
+        ignore_hidden != 0,
300
+        prefix_count,
301
+        prefix_stride,
302
+        prefixes,
303
+        &buffer
304
+    );
167305
     if (status != 0) {
168306
         free(buffer.data);
169307
         return status;
test/test_watch_errors.f90modified
@@ -1,34 +1,84 @@
11
 program test_watch_errors
2
-  use fgof_watch, only : init_watch, poll_watch, reset_watch
3
-  use fgof_watch_types, only : FGOF_WATCH_ERR_NONE, FGOF_WATCH_ERR_SNAPSHOT_FAILED, watch_event, watch_session
2
+  use fgof_watch, only : init_watch, poll_watch, reset_watch, set_ignore_prefixes
3
+  use fgof_watch_types, only : FGOF_WATCH_ERR_NONE, FGOF_WATCH_ERR_SNAPSHOT_FAILED, watch_event, watch_options, watch_session
44
   use watch_test_support, only : chmod_mode, ensure_clean_dir, expect_no_events, make_dir, remove_tree, write_text
55
   implicit none
66
 
7
-  character(len=*), parameter :: root = "build/watch-tests-errors"
8
-  character(len=*), parameter :: locked_dir = "build/watch-tests-errors/locked"
9
-  character(len=*), parameter :: locked_file = "build/watch-tests-errors/locked/file.txt"
10
-  type(watch_event), allocatable :: events(:)
11
-  type(watch_session) :: session
12
-
13
-  call ensure_clean_dir(root)
14
-  call make_dir(locked_dir)
15
-  call write_text(locked_file, "alpha")
16
-
17
-  call init_watch(session, root)
18
-  if (session%last_error_code /= FGOF_WATCH_ERR_NONE) error stop "initial snapshot should succeed"
19
-
20
-  call chmod_mode(locked_dir, "000")
21
-  events = poll_watch(session)
22
-  call expect_no_events(events, "snapshot failure should not emit false remove events")
23
-  if (session%last_error_code /= FGOF_WATCH_ERR_SNAPSHOT_FAILED) error stop "snapshot failure should set the session error code"
24
-  if (.not. allocated(session%last_error_message)) error stop "snapshot failure should preserve an error message"
25
-  if (len(session%last_error_message) == 0) error stop "snapshot failure message should not be empty"
26
-
27
-  call chmod_mode(locked_dir, "755")
28
-  events = poll_watch(session)
29
-  call expect_no_events(events, "state should recover cleanly after access is restored")
30
-  if (session%last_error_code /= FGOF_WATCH_ERR_NONE) error stop "successful poll should clear the session error"
31
-
32
-  call reset_watch(session)
33
-  call remove_tree(root)
7
+  call test_runtime_snapshot_failure()
8
+  call test_hidden_pruning_avoids_failure()
9
+  call test_prefix_pruning_avoids_failure()
10
+
11
+contains
12
+
13
+  subroutine test_runtime_snapshot_failure()
14
+    character(len=*), parameter :: root = "build/watch-tests-errors"
15
+    character(len=*), parameter :: locked_dir = "build/watch-tests-errors/locked"
16
+    character(len=*), parameter :: locked_file = "build/watch-tests-errors/locked/file.txt"
17
+    type(watch_event), allocatable :: events(:)
18
+    type(watch_session) :: session
19
+
20
+    call ensure_clean_dir(root)
21
+    call make_dir(locked_dir)
22
+    call write_text(locked_file, "alpha")
23
+
24
+    call init_watch(session, root)
25
+    if (session%last_error_code /= FGOF_WATCH_ERR_NONE) error stop "initial snapshot should succeed"
26
+
27
+    call chmod_mode(locked_dir, "000")
28
+    events = poll_watch(session)
29
+    call expect_no_events(events, "snapshot failure should not emit false remove events")
30
+    if (session%last_error_code /= FGOF_WATCH_ERR_SNAPSHOT_FAILED) error stop "snapshot failure should set the session error code"
31
+    if (.not. allocated(session%last_error_message)) error stop "snapshot failure should preserve an error message"
32
+    if (len(session%last_error_message) == 0) error stop "snapshot failure message should not be empty"
33
+
34
+    call chmod_mode(locked_dir, "755")
35
+    events = poll_watch(session)
36
+    call expect_no_events(events, "state should recover cleanly after access is restored")
37
+    if (session%last_error_code /= FGOF_WATCH_ERR_NONE) error stop "successful poll should clear the session error"
38
+
39
+    call reset_watch(session)
40
+    call remove_tree(root)
41
+  end subroutine test_runtime_snapshot_failure
42
+
43
+  subroutine test_hidden_pruning_avoids_failure()
44
+    character(len=*), parameter :: root = "build/watch-tests-hidden-prune"
45
+    character(len=*), parameter :: hidden_dir = "build/watch-tests-hidden-prune/.cache"
46
+    character(len=*), parameter :: hidden_file = "build/watch-tests-hidden-prune/.cache/file.txt"
47
+    type(watch_options) :: options
48
+    type(watch_session) :: session
49
+
50
+    call ensure_clean_dir(root)
51
+    call make_dir(hidden_dir)
52
+    call write_text(hidden_file, "alpha")
53
+    call chmod_mode(hidden_dir, "000")
54
+
55
+    options%ignore_hidden = .true.
56
+    call init_watch(session, root, options)
57
+    if (session%last_error_code /= FGOF_WATCH_ERR_NONE) error stop "ignored hidden subtree should be pruned before snapshot failure"
58
+
59
+    call chmod_mode(hidden_dir, "755")
60
+    call reset_watch(session)
61
+    call remove_tree(root)
62
+  end subroutine test_hidden_pruning_avoids_failure
63
+
64
+  subroutine test_prefix_pruning_avoids_failure()
65
+    character(len=*), parameter :: root = "build/watch-tests-prefix-prune"
66
+    character(len=*), parameter :: vendor_dir = "build/watch-tests-prefix-prune/vendor"
67
+    character(len=*), parameter :: vendor_file = "build/watch-tests-prefix-prune/vendor/file.txt"
68
+    type(watch_options) :: options
69
+    type(watch_session) :: session
70
+
71
+    call ensure_clean_dir(root)
72
+    call make_dir(vendor_dir)
73
+    call write_text(vendor_file, "alpha")
74
+    call chmod_mode(vendor_dir, "000")
75
+
76
+    call set_ignore_prefixes(options, [character(len=len(vendor_dir)) :: vendor_dir])
77
+    call init_watch(session, root, options)
78
+    if (session%last_error_code /= FGOF_WATCH_ERR_NONE) error stop "ignored prefix subtree should be pruned before snapshot failure"
79
+
80
+    call chmod_mode(vendor_dir, "755")
81
+    call reset_watch(session)
82
+    call remove_tree(root)
83
+  end subroutine test_prefix_pruning_avoids_failure
3484
 end program test_watch_errors