completed -> comp
[protos/libecoli.git] / lib / 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_parsed.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_parsed *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_PARSED_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                 errno = 0;
165                 de = node->readdir(dir);
166                 if (de == NULL) {
167                         if (errno == 0)
168                                 goto out;
169                         else
170                                 goto fail;
171                 }
172
173                 if (!ec_str_startswith(de->d_name, bname))
174                         continue;
175                 if (bname[0] != '.' && de->d_name[0] == '.')
176                         continue;
177
178                 /* add '/' if it's a dir */
179                 if (de->d_type == DT_DIR) {
180                         is_dir = 1;
181                 } else if (de->d_type == DT_UNKNOWN) {
182                         int dir_fd = node->dirfd(dir);
183
184                         if (dir_fd < 0)
185                                 goto fail;
186                         if (node->fstatat(dir_fd, de->d_name, &st2, 0) < 0)
187                                 goto fail;
188                         if (S_ISDIR(st2.st_mode))
189                                 is_dir = 1;
190                         else
191                                 is_dir = 0;
192                 } else {
193                         is_dir = 0;
194                 }
195
196                 if (is_dir) {
197                         type = EC_COMP_PARTIAL;
198                         if (ec_asprintf(&comp_str, "%s%s/", input,
199                                         &de->d_name[bname_len]) < 0)
200                                 goto fail;
201                         if (ec_asprintf(&disp_str, "%s/", de->d_name) < 0)
202                                 goto fail;
203                 } else {
204                         type = EC_COMP_FULL;
205                         if (ec_asprintf(&comp_str, "%s%s", input,
206                                         &de->d_name[bname_len]) < 0)
207                                 goto fail;
208                         if (ec_asprintf(&disp_str, "%s", de->d_name) < 0)
209                                 goto fail;
210                 }
211                 if (ec_comp_add_item(comp, gen_node, &item,
212                                                 type, input, comp_str) < 0)
213                         goto out;
214
215                 /* fix the display string: we don't want to display the full
216                  * path. */
217                 if (ec_comp_item_set_display(item, disp_str) < 0)
218                         goto out;
219
220                 item = NULL;
221                 ec_free(comp_str);
222                 comp_str = NULL;
223                 ec_free(disp_str);
224                 disp_str = NULL;
225         }
226 out:
227         ec_free(comp_str);
228         ec_free(disp_str);
229         ec_free(dname);
230         ec_free(bname);
231         if (dir != NULL)
232                 node->closedir(dir);
233
234         return 0;
235
236 fail:
237         ec_free(comp_str);
238         ec_free(disp_str);
239         ec_free(dname);
240         ec_free(bname);
241         if (dir != NULL)
242                 node->closedir(dir);
243
244         return -1;
245 }
246
247 static int
248 ec_node_file_init_priv(struct ec_node *gen_node)
249 {
250         struct ec_node_file *node = (struct ec_node_file *)gen_node;
251
252         node->lstat = lstat;
253         node->opendir = opendir;
254         node->readdir = readdir;
255         node->dirfd = dirfd;
256         node->fstatat = fstatat;
257
258         return 0;
259 }
260
261 static struct ec_node_type ec_node_file_type = {
262         .name = "file",
263         .parse = ec_node_file_parse,
264         .complete = ec_node_file_complete,
265         .size = sizeof(struct ec_node_file),
266         .init_priv = ec_node_file_init_priv,
267 };
268
269 EC_NODE_TYPE_REGISTER(ec_node_file_type);
270
271 /* LCOV_EXCL_START */
272 static int
273 test_lstat(const char *pathname, struct stat *buf)
274 {
275         if (!strcmp(pathname, "/tmp/toto/")) {
276                 struct stat st = { .st_mode = S_IFDIR };
277                 memcpy(buf, &st, sizeof(*buf));
278                 return 0;
279         }
280
281         errno = ENOENT;
282         return -1;
283 }
284
285 static DIR *
286 test_opendir(const char *name)
287 {
288         int *p;
289
290         if (strcmp(name, "/tmp/toto/")) {
291                 errno = ENOENT;
292                 return NULL;
293         }
294
295         p = malloc(sizeof(int));
296         if (p)
297                 *p = 0;
298
299         return (DIR *)p;
300 }
301
302 static struct dirent *
303 test_readdir(DIR *dirp)
304 {
305         static struct dirent de[] = {
306                 { .d_type = DT_DIR, .d_name = ".." },
307                 { .d_type = DT_DIR, .d_name = "." },
308                 { .d_type = DT_REG, .d_name = "bar" },
309                 { .d_type = DT_UNKNOWN, .d_name = "bar2" },
310                 { .d_type = DT_REG, .d_name = "foo" },
311                 { .d_type = DT_DIR, .d_name = "titi" },
312                 { .d_type = DT_UNKNOWN, .d_name = "tutu" },
313                 { .d_name = "" },
314         };
315         int *p = (int *)dirp;
316         struct dirent *ret = &de[*p];
317
318         if (!strcmp(ret->d_name, ""))
319                 return NULL;
320
321         *p = *p + 1;
322
323         return ret;
324 }
325
326 static int
327 test_closedir(DIR *dirp)
328 {
329         free(dirp);
330         return 0;
331 }
332
333 static int
334 test_dirfd(DIR *dirp)
335 {
336         int *p = (int *)dirp;
337         return *p;
338 }
339
340 static int
341 test_fstatat(int dirfd, const char *pathname, struct stat *buf,
342         int flags)
343 {
344         (void)dirfd;
345         (void)flags;
346
347         if (!strcmp(pathname, "bar2")) {
348                 struct stat st = { .st_mode = S_IFREG };
349                 memcpy(buf, &st, sizeof(*buf));
350                 return 0;
351         } else if (!strcmp(pathname, "tutu")) {
352                 struct stat st = { .st_mode = S_IFDIR };
353                 memcpy(buf, &st, sizeof(*buf));
354                 return 0;
355         }
356
357         errno = ENOENT;
358         return -1;
359 }
360
361 static int
362 ec_node_file_override_functions(struct ec_node *gen_node)
363 {
364         struct ec_node_file *node = (struct ec_node_file *)gen_node;
365
366         node->lstat = test_lstat;
367         node->opendir = test_opendir;
368         node->readdir = test_readdir;
369         node->closedir = test_closedir;
370         node->dirfd = test_dirfd;
371         node->fstatat = test_fstatat;
372
373         return 0;
374 }
375
376 static int ec_node_file_testcase(void)
377 {
378         struct ec_node *node;
379         int testres = 0;
380
381         node = ec_node("file", EC_NO_ID);
382         if (node == NULL) {
383                 EC_LOG(EC_LOG_ERR, "cannot create node\n");
384                 return -1;
385         }
386         ec_node_file_override_functions(node);
387
388         /* any string matches */
389         testres |= EC_TEST_CHECK_PARSE(node, 1, "foo");
390         testres |= EC_TEST_CHECK_PARSE(node, 1, "/tmp/bar");
391         testres |= EC_TEST_CHECK_PARSE(node, -1);
392
393         /* test completion */
394         testres |= EC_TEST_CHECK_COMPLETE(node,
395                 EC_NODE_ENDLIST,
396                 EC_NODE_ENDLIST);
397         testres |= EC_TEST_CHECK_COMPLETE(node,
398                 "/tmp/toto/t", EC_NODE_ENDLIST,
399                 EC_NODE_ENDLIST);
400         testres |= EC_TEST_CHECK_COMPLETE_PARTIAL(node,
401                 "/tmp/toto/t", EC_NODE_ENDLIST,
402                 "/tmp/toto/titi/", "/tmp/toto/tutu/", EC_NODE_ENDLIST);
403         testres |= EC_TEST_CHECK_COMPLETE(node,
404                 "/tmp/toto/f", EC_NODE_ENDLIST,
405                 "/tmp/toto/foo", EC_NODE_ENDLIST);
406         testres |= EC_TEST_CHECK_COMPLETE(node,
407                 "/tmp/toto/b", EC_NODE_ENDLIST,
408                 "/tmp/toto/bar", "/tmp/toto/bar2", EC_NODE_ENDLIST);
409
410         ec_node_free(node);
411
412         return testres;
413 }
414 /* LCOV_EXCL_STOP */
415
416 static struct ec_test ec_node_file_test = {
417         .name = "node_file",
418         .test = ec_node_file_testcase,
419 };
420
421 EC_TEST_REGISTER(ec_node_file_test);