add meson support
[protos/libecoli.git] / src / ecoli_node_file.c
1 /* SPDX-License-Identifier: BSD-3-Clause
2  * Copyright 2016, Olivier MATZ <zer0@droids-corp.org>
3  */
4
5 #include <sys/types.h>
6 #include <sys/stat.h>
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <string.h>
10 #include <errno.h>
11 #include <unistd.h>
12 #include <dirent.h>
13
14 #include <ecoli_log.h>
15 #include <ecoli_malloc.h>
16 #include <ecoli_test.h>
17 #include <ecoli_strvec.h>
18 #include <ecoli_string.h>
19 #include <ecoli_node.h>
20 #include <ecoli_parse.h>
21 #include <ecoli_complete.h>
22 #include <ecoli_node_file.h>
23
24 EC_LOG_TYPE_REGISTER(node_file);
25
26 struct ec_node_file {
27         struct ec_node gen;
28
29         /* below functions pointers are only useful for test */
30         int (*lstat)(const char *pathname, struct stat *buf);
31         DIR *(*opendir)(const char *name);
32         struct dirent *(*readdir)(DIR *dirp);
33         int (*closedir)(DIR *dirp);
34         int (*dirfd)(DIR *dirp);
35         int (*fstatat)(int dirfd, const char *pathname, struct stat *buf,
36                 int flags);
37 };
38
39 static int
40 ec_node_file_parse(const struct ec_node *gen_node,
41                 struct ec_parse *state,
42                 const struct ec_strvec *strvec)
43 {
44         (void)gen_node;
45         (void)state;
46
47         if (ec_strvec_len(strvec) == 0)
48                 return EC_PARSE_NOMATCH;
49
50         return 1;
51 }
52
53 /*
54  * Almost the same than dirname (3) and basename (3) except that:
55  * - it always returns a substring of the given path, which can
56  *   be empty.
57  * - the behavior is different when the path finishes with a '/'
58  * - the path argument is not modified
59  * - the outputs are allocated and must be freed with ec_free().
60  *
61  *   path       dirname   basename       split_path
62  *   /usr/lib   /usr      lib          /usr/     lib
63  *   /usr/      /         usr          /usr/
64  *   usr        .         usr                    usr
65  *   /          /         /            /
66  *   .          .         .                      .
67  *   ..         .         ..                     ..
68  */
69 static int split_path(const char *path, char **dname_p, char **bname_p)
70 {
71         char *last_slash;
72         size_t dirlen;
73         char *dname, *bname;
74
75         *dname_p = NULL;
76         *bname_p = NULL;
77
78         last_slash = strrchr(path, '/');
79         if (last_slash == NULL)
80                 dirlen = 0;
81         else
82                 dirlen = last_slash - path + 1;
83
84         dname = ec_strdup(path);
85         if (dname == NULL)
86                 return -1;
87         dname[dirlen] = '\0';
88
89         bname = ec_strdup(path + dirlen);
90         if (bname == NULL) {
91                 ec_free(dname);
92                 return -1;
93         }
94
95         *dname_p = dname;
96         *bname_p = bname;
97
98         return 0;
99 }
100
101 static int
102 ec_node_file_complete(const struct ec_node *gen_node,
103                 struct ec_comp *comp,
104                 const struct ec_strvec *strvec)
105 {
106         struct ec_node_file *node = (struct ec_node_file *)gen_node;
107         char *dname = NULL, *bname = NULL, *effective_dir;
108         struct ec_comp_item *item = NULL;
109         enum ec_comp_type type;
110         struct stat st, st2;
111         const char *input;
112         size_t bname_len;
113         struct dirent *de = NULL;
114         DIR *dir = NULL;
115         char *comp_str = NULL;
116         char *disp_str = NULL;
117         int is_dir = 0;
118
119         /*
120          * Example with this file tree:
121          * /
122          * ├── dir1
123          * │   ├── file1
124          * │   ├── file2
125          * │   └── subdir
126          * │       └── file3
127          * ├── dir2
128          * │   └── file4
129          * └── file5
130          *
131          * Input     Output completions
132          *   /       [dir1/, dir2/, file5]
133          *   /d      [dir1/, dir2/]
134          *   /f      [file5]
135          *   /dir1/  [file1, file2, subdir/]
136          *
137          *
138          *
139          */
140
141         if (ec_strvec_len(strvec) != 1)
142                 return 0;
143
144         input = ec_strvec_val(strvec, 0);
145         if (split_path(input, &dname, &bname) < 0)
146                 return -1;
147
148         if (strcmp(dname, "") == 0)
149                 effective_dir = ".";
150         else
151                 effective_dir = dname;
152
153         if (node->lstat(effective_dir, &st) < 0)
154                 goto fail;
155         if (!S_ISDIR(st.st_mode))
156                 goto out;
157
158         dir = node->opendir(effective_dir);
159         if (dir == NULL)
160                 goto fail;
161
162         bname_len = strlen(bname);
163         while (1) {
164                 int save_errno = errno;
165
166                 errno = 0;
167                 de = node->readdir(dir);
168                 if (de == NULL) {
169                         if (errno == 0) {
170                                 errno = save_errno;
171                                 goto out;
172                         } else {
173                                 goto fail;
174                         }
175                 }
176
177                 if (!ec_str_startswith(de->d_name, bname))
178                         continue;
179                 if (bname[0] != '.' && de->d_name[0] == '.')
180                         continue;
181
182                 /* add '/' if it's a dir */
183                 if (de->d_type == DT_DIR) {
184                         is_dir = 1;
185                 } else if (de->d_type == DT_UNKNOWN) {
186                         int dir_fd = node->dirfd(dir);
187
188                         if (dir_fd < 0)
189                                 goto fail;
190                         if (node->fstatat(dir_fd, de->d_name, &st2, 0) < 0)
191                                 goto fail;
192                         if (S_ISDIR(st2.st_mode))
193                                 is_dir = 1;
194                         else
195                                 is_dir = 0;
196                 } else {
197                         is_dir = 0;
198                 }
199
200                 if (is_dir) {
201                         type = EC_COMP_PARTIAL;
202                         if (ec_asprintf(&comp_str, "%s%s/", input,
203                                         &de->d_name[bname_len]) < 0)
204                                 goto fail;
205                         if (ec_asprintf(&disp_str, "%s/", de->d_name) < 0)
206                                 goto fail;
207                 } else {
208                         type = EC_COMP_FULL;
209                         if (ec_asprintf(&comp_str, "%s%s", input,
210                                         &de->d_name[bname_len]) < 0)
211                                 goto fail;
212                         if (ec_asprintf(&disp_str, "%s", de->d_name) < 0)
213                                 goto fail;
214                 }
215                 if (ec_comp_add_item(comp, gen_node, &item,
216                                                 type, input, comp_str) < 0)
217                         goto out;
218
219                 /* fix the display string: we don't want to display the full
220                  * path. */
221                 if (ec_comp_item_set_display(item, disp_str) < 0)
222                         goto out;
223
224                 item = NULL;
225                 ec_free(comp_str);
226                 comp_str = NULL;
227                 ec_free(disp_str);
228                 disp_str = NULL;
229         }
230 out:
231         ec_free(comp_str);
232         ec_free(disp_str);
233         ec_free(dname);
234         ec_free(bname);
235         if (dir != NULL)
236                 node->closedir(dir);
237
238         return 0;
239
240 fail:
241         ec_free(comp_str);
242         ec_free(disp_str);
243         ec_free(dname);
244         ec_free(bname);
245         if (dir != NULL)
246                 node->closedir(dir);
247
248         return -1;
249 }
250
251 static int
252 ec_node_file_init_priv(struct ec_node *gen_node)
253 {
254         struct ec_node_file *node = (struct ec_node_file *)gen_node;
255
256         node->lstat = lstat;
257         node->opendir = opendir;
258         node->readdir = readdir;
259         node->dirfd = dirfd;
260         node->fstatat = fstatat;
261
262         return 0;
263 }
264
265 static struct ec_node_type ec_node_file_type = {
266         .name = "file",
267         .parse = ec_node_file_parse,
268         .complete = ec_node_file_complete,
269         .size = sizeof(struct ec_node_file),
270         .init_priv = ec_node_file_init_priv,
271 };
272
273 EC_NODE_TYPE_REGISTER(ec_node_file_type);
274
275 /* LCOV_EXCL_START */
276 static int
277 test_lstat(const char *pathname, struct stat *buf)
278 {
279         if (!strcmp(pathname, "/tmp/toto/")) {
280                 struct stat st = { .st_mode = S_IFDIR };
281                 memcpy(buf, &st, sizeof(*buf));
282                 return 0;
283         }
284
285         errno = ENOENT;
286         return -1;
287 }
288
289 static DIR *
290 test_opendir(const char *name)
291 {
292         int *p;
293
294         if (strcmp(name, "/tmp/toto/")) {
295                 errno = ENOENT;
296                 return NULL;
297         }
298
299         p = malloc(sizeof(int));
300         if (p)
301                 *p = 0;
302
303         return (DIR *)p;
304 }
305
306 static struct dirent *
307 test_readdir(DIR *dirp)
308 {
309         static struct dirent de[] = {
310                 { .d_type = DT_DIR, .d_name = ".." },
311                 { .d_type = DT_DIR, .d_name = "." },
312                 { .d_type = DT_REG, .d_name = "bar" },
313                 { .d_type = DT_UNKNOWN, .d_name = "bar2" },
314                 { .d_type = DT_REG, .d_name = "foo" },
315                 { .d_type = DT_DIR, .d_name = "titi" },
316                 { .d_type = DT_UNKNOWN, .d_name = "tutu" },
317                 { .d_name = "" },
318         };
319         int *p = (int *)dirp;
320         struct dirent *ret = &de[*p];
321
322         if (!strcmp(ret->d_name, ""))
323                 return NULL;
324
325         *p = *p + 1;
326
327         return ret;
328 }
329
330 static int
331 test_closedir(DIR *dirp)
332 {
333         free(dirp);
334         return 0;
335 }
336
337 static int
338 test_dirfd(DIR *dirp)
339 {
340         int *p = (int *)dirp;
341         return *p;
342 }
343
344 static int
345 test_fstatat(int dirfd, const char *pathname, struct stat *buf,
346         int flags)
347 {
348         (void)dirfd;
349         (void)flags;
350
351         if (!strcmp(pathname, "bar2")) {
352                 struct stat st = { .st_mode = S_IFREG };
353                 memcpy(buf, &st, sizeof(*buf));
354                 return 0;
355         } else if (!strcmp(pathname, "tutu")) {
356                 struct stat st = { .st_mode = S_IFDIR };
357                 memcpy(buf, &st, sizeof(*buf));
358                 return 0;
359         }
360
361         errno = ENOENT;
362         return -1;
363 }
364
365 static int
366 ec_node_file_override_functions(struct ec_node *gen_node)
367 {
368         struct ec_node_file *node = (struct ec_node_file *)gen_node;
369
370         node->lstat = test_lstat;
371         node->opendir = test_opendir;
372         node->readdir = test_readdir;
373         node->closedir = test_closedir;
374         node->dirfd = test_dirfd;
375         node->fstatat = test_fstatat;
376
377         return 0;
378 }
379
380 static int ec_node_file_testcase(void)
381 {
382         struct ec_node *node;
383         int testres = 0;
384
385         node = ec_node("file", EC_NO_ID);
386         if (node == NULL) {
387                 EC_LOG(EC_LOG_ERR, "cannot create node\n");
388                 return -1;
389         }
390         ec_node_file_override_functions(node);
391
392         /* any string matches */
393         testres |= EC_TEST_CHECK_PARSE(node, 1, "foo");
394         testres |= EC_TEST_CHECK_PARSE(node, 1, "/tmp/bar");
395         testres |= EC_TEST_CHECK_PARSE(node, -1);
396
397         /* test completion */
398         testres |= EC_TEST_CHECK_COMPLETE(node,
399                 EC_NODE_ENDLIST,
400                 EC_NODE_ENDLIST);
401         testres |= EC_TEST_CHECK_COMPLETE(node,
402                 "/tmp/toto/t", EC_NODE_ENDLIST,
403                 EC_NODE_ENDLIST);
404         testres |= EC_TEST_CHECK_COMPLETE_PARTIAL(node,
405                 "/tmp/toto/t", EC_NODE_ENDLIST,
406                 "/tmp/toto/titi/", "/tmp/toto/tutu/", EC_NODE_ENDLIST);
407         testres |= EC_TEST_CHECK_COMPLETE(node,
408                 "/tmp/toto/f", EC_NODE_ENDLIST,
409                 "/tmp/toto/foo", EC_NODE_ENDLIST);
410         testres |= EC_TEST_CHECK_COMPLETE(node,
411                 "/tmp/toto/b", EC_NODE_ENDLIST,
412                 "/tmp/toto/bar", "/tmp/toto/bar2", EC_NODE_ENDLIST);
413
414         ec_node_free(node);
415
416         return testres;
417 }
418 /* LCOV_EXCL_STOP */
419
420 static struct ec_test ec_node_file_test = {
421         .name = "node_file",
422         .test = ec_node_file_testcase,
423 };
424
425 EC_TEST_REGISTER(ec_node_file_test);