1 | // SPDX-License-Identifier: GPL-3.0-or-later |
2 | |
3 | #include "mos/filesystem/dentry.h" |
4 | |
5 | #include "mos/assert.h" |
6 | #include "mos/filesystem/inode.h" |
7 | #include "mos/filesystem/mount.h" |
8 | #include "mos/filesystem/vfs.h" |
9 | #include "mos/filesystem/vfs_types.h" |
10 | #include "mos/filesystem/vfs_utils.h" |
11 | #include "mos/io/io.h" |
12 | #include "mos/lib/sync/spinlock.h" |
13 | #include "mos/syslog/printk.h" |
14 | #include "mos/tasks/process.h" |
15 | #include "mos/tasks/task_types.h" |
16 | |
17 | #include <mos/filesystem/fs_types.h> |
18 | #include <mos/lib/structures/hashmap_common.h> |
19 | #include <mos_stdio.h> |
20 | #include <mos_stdlib.h> |
21 | #include <mos_string.h> |
22 | |
23 | // A path in its string form is composed of "segments" separated |
24 | // by a slash "/", a path may: |
25 | // |
26 | // - begin with a slash, indicating that's an absolute path |
27 | // - begin without a slash, indicating that's a relative path |
28 | // (relative to the current working directory (AT_FDCWD)) |
29 | // |
30 | // A path may end with a slash, indicating that the caller expects |
31 | // the path to be a directory |
32 | |
33 | // The two functions below have circular dependencies, so we need to forward declare them |
34 | // Both of them return a referenced dentry, no need to refcount them again |
35 | static dentry_t *dentry_resolve_lastseg(dentry_t *parent, char *leaf, lastseg_resolve_flags_t flags, bool *symlink_resolved); |
36 | static dentry_t *dentry_resolve_follow_symlink(dentry_t *dentry, lastseg_resolve_flags_t flags); |
37 | |
38 | /** |
39 | * @brief Lookup the parent directory of a given path, and return the last segment of the path in last_seg_out |
40 | * |
41 | * @param base_dir A directory to start the lookup from |
42 | * @param root_dir The root directory of the filesystem, the lookup will not go above this directory |
43 | * @param original_path The path to lookup |
44 | * @param last_seg_out The last segment of the path will be returned in this parameter, the caller is responsible for freeing it |
45 | * @return dentry_t* The parent directory of the path, or NULL if the path is invalid, the dentry will be referenced |
46 | */ |
47 | static dentry_t *dentry_resolve_to_parent(dentry_t *base_dir, dentry_t *root_dir, const char *original_path, char **last_seg_out) |
48 | { |
49 | pr_dinfo2(dcache, "lookup parent of '%s'" , original_path); |
50 | MOS_ASSERT_X(base_dir && root_dir && original_path, "Invalid VFS lookup parameters" ); |
51 | if (last_seg_out != NULL) |
52 | *last_seg_out = NULL; |
53 | |
54 | dentry_t *parent_ref = statement_expr(dentry_t *, { |
55 | dentry_t *tmp = path_is_absolute(original_path) ? root_dir : base_dir; |
56 | if (tmp->is_mountpoint) |
57 | tmp = dentry_get_mount(tmp)->root; // if it's a mountpoint, jump to mounted filesystem |
58 | tmp = dentry_ref_up_to(tmp, root_dir); |
59 | retval = tmp; |
60 | }); |
61 | |
62 | char *saveptr = NULL; |
63 | char *path = strdup(src: original_path); |
64 | const char *current_seg = strtok_r(str: path, PATH_DELIM_STR, saveptr: &saveptr); |
65 | if (unlikely(current_seg == NULL)) |
66 | { |
67 | // this only happens if the path is empty, or contains only slashes |
68 | // in which case we return the base directory |
69 | kfree(ptr: path); |
70 | if (last_seg_out != NULL) |
71 | *last_seg_out = NULL; |
72 | return parent_ref; |
73 | } |
74 | |
75 | while (true) |
76 | { |
77 | pr_dinfo2(dcache, "lookup parent: current segment '%s'" , current_seg); |
78 | const char *const next = strtok_r(NULL, PATH_DELIM_STR, saveptr: &saveptr); |
79 | if (parent_ref->inode->type == FILE_TYPE_SYMLINK) |
80 | { |
81 | // this is the real interesting dir |
82 | dentry_t *const parent_real_ref = dentry_resolve_follow_symlink(dentry: parent_ref, flags: RESOLVE_EXPECT_EXIST | RESOLVE_EXPECT_DIR); |
83 | dentry_unref(dentry: parent_ref); |
84 | if (IS_ERR(ptr: parent_real_ref)) |
85 | return ERR_PTR(error: -ENOENT); // the symlink target does not exist |
86 | parent_ref = parent_real_ref; |
87 | } |
88 | |
89 | if (next == NULL) |
90 | { |
91 | // "current_seg" is the last segment of the path |
92 | if (last_seg_out != NULL) |
93 | { |
94 | const bool ends_with_slash = original_path[strlen(str: original_path) - 1] == PATH_DELIM; |
95 | char *tmp = kmalloc(strlen(current_seg) + 2); // +2 for the null terminator and the slash |
96 | strcpy(dest: tmp, src: current_seg); |
97 | if (ends_with_slash) |
98 | strcat(dest: tmp, PATH_DELIM_STR); |
99 | *last_seg_out = tmp; |
100 | } |
101 | |
102 | kfree(ptr: path); |
103 | return parent_ref; |
104 | } |
105 | |
106 | if (strncmp(str1: current_seg, str2: "." , n: 2) == 0 || strcmp(str1: current_seg, str2: "./" ) == 0) |
107 | { |
108 | current_seg = next; |
109 | continue; |
110 | } |
111 | |
112 | if (strncmp(str1: current_seg, str2: ".." , n: 3) == 0 || strcmp(str1: current_seg, str2: "../" ) == 0) |
113 | { |
114 | if (parent_ref == root_dir) |
115 | { |
116 | // we can't go above the root directory |
117 | current_seg = next; |
118 | continue; |
119 | } |
120 | |
121 | dentry_t *parent = dentry_parent(dentry: parent_ref); |
122 | |
123 | // don't recurse up to the root |
124 | MOS_ASSERT(dentry_unref_one_norelease(parent_ref)); |
125 | parent_ref = parent; |
126 | |
127 | // if the parent is a mountpoint, we need to jump to the mountpoint's parent |
128 | // and then jump to the mountpoint's parent's parent |
129 | // already referenced when we jumped to the mountpoint |
130 | if (parent_ref->is_mountpoint) |
131 | parent_ref = dentry_root_get_mountpoint(dentry: parent); |
132 | |
133 | current_seg = next; |
134 | continue; |
135 | } |
136 | |
137 | dentry_t *const child_ref = dentry_lookup_child(parent: parent_ref, name: current_seg); |
138 | if (child_ref->inode == NULL) |
139 | { |
140 | *last_seg_out = NULL; |
141 | kfree(ptr: path); |
142 | dentry_try_release(dentry: child_ref); |
143 | dentry_unref(dentry: parent_ref); |
144 | return ERR_PTR(error: -ENOENT); |
145 | } |
146 | |
147 | if (child_ref->is_mountpoint) |
148 | { |
149 | pr_dinfo2(dcache, "jumping to mountpoint %s" , child_ref->name); |
150 | parent_ref = dentry_get_mount(dentry: child_ref)->root; // if it's a mountpoint, jump to the tree of mounted filesystem instead |
151 | |
152 | // refcount the mounted filesystem root |
153 | dentry_ref(dentry: parent_ref); |
154 | } |
155 | else |
156 | { |
157 | parent_ref = child_ref; |
158 | } |
159 | |
160 | current_seg = next; |
161 | } |
162 | |
163 | MOS_UNREACHABLE(); |
164 | } |
165 | |
166 | static dentry_t *dentry_resolve_follow_symlink(dentry_t *d, lastseg_resolve_flags_t flags) |
167 | { |
168 | MOS_ASSERT_X(d != NULL && d->inode != NULL, "check before calling this function!" ); |
169 | MOS_ASSERT_X(d->inode->type == FILE_TYPE_SYMLINK, "check before calling this function!" ); |
170 | |
171 | if (!d->inode->ops || !d->inode->ops->readlink) |
172 | mos_panic("inode does not support readlink (symlink) operation, but it's a symlink!" ); |
173 | |
174 | char *const target = kmalloc(MOS_PATH_MAX_LENGTH); |
175 | const size_t read = d->inode->ops->readlink(d, target, MOS_PATH_MAX_LENGTH); |
176 | if (read == 0) |
177 | { |
178 | mos_warn("symlink is empty" ); |
179 | return ERR_PTR(error: -ENOENT); // symlink is empty |
180 | } |
181 | |
182 | if (read == MOS_PATH_MAX_LENGTH) |
183 | { |
184 | mos_warn("symlink is too long" ); |
185 | return ERR_PTR(error: -ENAMETOOLONG); // symlink is too long |
186 | } |
187 | |
188 | target[read] = '\0'; // ensure null termination |
189 | |
190 | pr_dinfo2(dcache, "symlink target: %s" , target); |
191 | |
192 | char *last_segment = NULL; |
193 | dentry_t *const parent_ref = dentry_resolve_to_parent(base_dir: dentry_parent(dentry: d), root_dir: root_dentry, original_path: target, last_seg_out: &last_segment); |
194 | kfree(ptr: target); |
195 | if (IS_ERR(ptr: parent_ref)) |
196 | return parent_ref; // the symlink target does not exist |
197 | |
198 | // it's possibly that the symlink target is also a symlink, this will be handled recursively |
199 | bool is_symlink = false; |
200 | dentry_t *const child_ref = dentry_resolve_lastseg(parent: parent_ref, leaf: last_segment, flags, symlink_resolved: &is_symlink); |
201 | kfree(ptr: last_segment); |
202 | |
203 | // if symlink is true, we need to unref the parent_ref dentry as it's irrelevant now |
204 | if (IS_ERR(ptr: child_ref) || is_symlink) |
205 | dentry_unref(dentry: parent_ref); |
206 | |
207 | return child_ref; // the real dentry, or an error code |
208 | } |
209 | |
210 | static dentry_t *dentry_resolve_lastseg(dentry_t *parent, char *leaf, lastseg_resolve_flags_t flags, bool *is_symlink) |
211 | { |
212 | MOS_ASSERT(parent != NULL && leaf != NULL); |
213 | *is_symlink = false; |
214 | |
215 | pr_dinfo2(dcache, "resolving last segment: '%s'" , leaf); |
216 | const bool ends_with_slash = leaf[strlen(str: leaf) - 1] == PATH_DELIM; |
217 | if (ends_with_slash) |
218 | leaf[strlen(str: leaf) - 1] = '\0'; // remove the trailing slash |
219 | |
220 | if (unlikely(ends_with_slash && !(flags & RESOLVE_EXPECT_DIR))) |
221 | { |
222 | mos_warn("RESOLVE_EXPECT_DIR isn't set, but the provided path ends with a slash" ); |
223 | return ERR_PTR(error: -EINVAL); |
224 | } |
225 | |
226 | if (strncmp(str1: leaf, str2: "." , n: 2) == 0 || strcmp(str1: leaf, str2: "./" ) == 0) |
227 | return parent; |
228 | else if (strncmp(str1: leaf, str2: ".." , n: 3) == 0 || strcmp(str1: leaf, str2: "../" ) == 0) |
229 | { |
230 | if (parent == root_dentry) |
231 | return parent; |
232 | |
233 | dentry_t *const parent_parent = dentry_parent(dentry: parent); |
234 | MOS_ASSERT(dentry_unref_one_norelease(parent)); // don't recursively unref all the way to the root |
235 | |
236 | // if the parent is a mountpoint, we need to jump to the mountpoint's parent |
237 | if (parent_parent->is_mountpoint) |
238 | return dentry_root_get_mountpoint(dentry: parent_parent); |
239 | |
240 | return parent_parent; |
241 | } |
242 | |
243 | dentry_t *const child_ref = dentry_lookup_child(parent, name: leaf); // now we have a reference to the child |
244 | |
245 | if (unlikely(child_ref->inode == NULL)) |
246 | { |
247 | if (flags & RESOLVE_EXPECT_NONEXIST) |
248 | { |
249 | // do not use dentry_ref, because it checks for an inode |
250 | child_ref->refcount++; |
251 | return child_ref; |
252 | } |
253 | |
254 | pr_dinfo2(dcache, "file does not exist" ); |
255 | dentry_try_release(dentry: child_ref); // child has no ref, we should release it directly |
256 | return ERR_PTR(error: -ENOENT); |
257 | } |
258 | |
259 | MOS_ASSERT(child_ref->refcount > 0); // dentry_get_child may return a negative dentry, which is handled above, otherwise we should have a reference on it |
260 | |
261 | if (flags & RESOLVE_EXPECT_NONEXIST && !(flags & RESOLVE_EXPECT_EXIST)) |
262 | { |
263 | dentry_unref(dentry: child_ref); |
264 | return ERR_PTR(error: -EEXIST); |
265 | } |
266 | |
267 | if (child_ref->inode->type == FILE_TYPE_SYMLINK) |
268 | { |
269 | if (!(flags & RESOLVE_SYMLINK_NOFOLLOW)) |
270 | { |
271 | pr_dinfo2(dcache, "resolving symlink for '%s'" , leaf); |
272 | dentry_t *const symlink_target_ref = dentry_resolve_follow_symlink(d: child_ref, flags); |
273 | // we don't need the symlink node anymore |
274 | MOS_ASSERT(dentry_unref_one_norelease(child_ref)); |
275 | *is_symlink = symlink_target_ref != NULL; |
276 | return symlink_target_ref; |
277 | } |
278 | |
279 | pr_dinfo2(dcache, "not following symlink" ); |
280 | } |
281 | else if (child_ref->inode->type == FILE_TYPE_DIRECTORY) |
282 | { |
283 | if (!(flags & RESOLVE_EXPECT_DIR)) |
284 | { |
285 | MOS_ASSERT(dentry_unref_one_norelease(child_ref)); // it's the caller's responsibility to unref the parent and grandparents |
286 | return ERR_PTR(error: -EISDIR); |
287 | } |
288 | |
289 | // if the child is a mountpoint, we need to jump to the mounted filesystem's root |
290 | if (child_ref->is_mountpoint) |
291 | return dentry_ref(dentry: dentry_get_mount(dentry: child_ref)->root); |
292 | } |
293 | else |
294 | { |
295 | if (!(flags & RESOLVE_EXPECT_FILE)) |
296 | { |
297 | MOS_ASSERT(dentry_unref_one_norelease(child_ref)); // it's the caller's responsibility to unref the parent and grandparents |
298 | return ERR_PTR(error: -ENOTDIR); |
299 | } |
300 | } |
301 | |
302 | return child_ref; |
303 | } |
304 | |
305 | void dentry_attach(dentry_t *d, inode_t *inode) |
306 | { |
307 | MOS_ASSERT_X(d->inode == NULL, "reattaching an inode to a dentry" ); |
308 | MOS_ASSERT(inode != NULL); |
309 | // MOS_ASSERT_X(d->refcount == 1, "dentry %p refcount %zu is not 1", (void *) d, d->refcount); |
310 | |
311 | for (atomic_t i = 0; i < d->refcount; i++) |
312 | inode_ref(inode); // refcount the inode for each reference to the dentry |
313 | |
314 | inode_ref(inode); // refcount the inode for each reference to the dentry |
315 | d->inode = inode; |
316 | } |
317 | |
318 | void dentry_detach(dentry_t *d) |
319 | { |
320 | if (d->inode == NULL) |
321 | return; |
322 | |
323 | // the caller should have the only reference to the dentry |
324 | // MOS_ASSERT(d->refcount == 1); // !! TODO: this assertion fails in vfs_unlinkat |
325 | |
326 | (void) inode_unref(inode: d->inode); // we don't care if the inode is freed or not |
327 | d->inode = NULL; |
328 | } |
329 | |
330 | dentry_t *dentry_from_fd(fd_t fd) |
331 | { |
332 | if (fd == AT_FDCWD) |
333 | { |
334 | if (current_thread) |
335 | return current_process->working_directory; |
336 | else |
337 | return root_dentry; // no current process, so cwd is always root |
338 | } |
339 | |
340 | // sanity check: fd != AT_FDCWD, no current process |
341 | MOS_ASSERT(current_thread); |
342 | |
343 | io_t *io = process_get_fd(current_process, fd); |
344 | if (io == NULL) |
345 | return ERR_PTR(error: -EBADF); |
346 | |
347 | if (io->type != IO_FILE && io->type != IO_DIR) |
348 | return ERR_PTR(error: -EBADF); |
349 | |
350 | file_t *file = container_of(io, file_t, io); |
351 | return file->dentry; |
352 | } |
353 | |
354 | dentry_t *dentry_lookup_child(dentry_t *parent, const char *name) |
355 | { |
356 | if (unlikely(parent == NULL)) |
357 | return NULL; |
358 | |
359 | pr_dinfo2(dcache, "looking for dentry '%s' in '%s'" , name, dentry_name(parent)); |
360 | |
361 | // firstly check if it's in the cache |
362 | dentry_t *dentry = dentry_get_from_parent(sb: parent->superblock, parent, name); |
363 | MOS_ASSERT(dentry); |
364 | |
365 | spinlock_acquire(&dentry->lock); |
366 | |
367 | if (dentry->inode) |
368 | { |
369 | pr_dinfo2(dcache, "dentry '%s' found in the cache" , name); |
370 | spinlock_release(&dentry->lock); |
371 | return dentry_ref(dentry); |
372 | } |
373 | |
374 | // not in the cache, try to find it in the filesystem |
375 | if (parent->inode == NULL || parent->inode->ops == NULL || parent->inode->ops->lookup == NULL) |
376 | { |
377 | pr_dinfo2(dcache, "filesystem doesn't support lookup" ); |
378 | spinlock_release(&dentry->lock); |
379 | return dentry; |
380 | } |
381 | |
382 | const bool lookup_result = parent->inode->ops->lookup(parent->inode, dentry); |
383 | spinlock_release(&dentry->lock); |
384 | |
385 | if (lookup_result) |
386 | { |
387 | pr_dinfo2(dcache, "dentry '%s' found in the filesystem" , name); |
388 | return dentry_ref(dentry); |
389 | } |
390 | else |
391 | { |
392 | pr_dinfo2(dcache, "dentry '%s' not found in the filesystem" , name); |
393 | return dentry; // do not reference a negative dentry |
394 | } |
395 | } |
396 | |
397 | dentry_t *dentry_resolve(dentry_t *starting_dir, dentry_t *root_dir, const char *path, lastseg_resolve_flags_t flags) |
398 | { |
399 | if (!root_dir) |
400 | return ERR_PTR(error: -ENOENT); // no root directory |
401 | |
402 | char *last_segment; |
403 | pr_dinfo2(dcache, "resolving path '%s'" , path); |
404 | dentry_t *const parent_ref = dentry_resolve_to_parent(base_dir: starting_dir, root_dir, original_path: path, last_seg_out: &last_segment); |
405 | if (IS_ERR(ptr: parent_ref)) |
406 | { |
407 | pr_dinfo2(dcache, "failed to resolve parent of '%s', file not found" , path); |
408 | return parent_ref; |
409 | } |
410 | |
411 | if (last_segment == NULL) |
412 | { |
413 | // path is a single "/" |
414 | pr_dinfo2(dcache, "path '%s' is a single '/' or is empty" , path); |
415 | MOS_ASSERT(parent_ref == starting_dir); |
416 | if (!(flags & RESOLVE_EXPECT_DIR)) |
417 | { |
418 | dentry_unref(dentry: parent_ref); |
419 | return ERR_PTR(error: -EISDIR); |
420 | } |
421 | |
422 | return parent_ref; |
423 | } |
424 | |
425 | bool symlink = false; |
426 | dentry_t *child_ref = dentry_resolve_lastseg(parent: parent_ref, leaf: last_segment, flags, is_symlink: &symlink); |
427 | kfree(ptr: last_segment); |
428 | if (IS_ERR(ptr: child_ref) || symlink) |
429 | dentry_unref(dentry: parent_ref); // the lookup failed, or child_ref is irrelevant with the parent_ref |
430 | |
431 | return child_ref; |
432 | } |
433 | |
434 | static void dirter_add(vfs_listdir_state_t *state, u64 ino, const char *name, size_t name_len, file_type_t type) |
435 | { |
436 | vfs_listdir_entry_t *entry = kmalloc(sizeof(vfs_listdir_entry_t)); |
437 | linked_list_init(list_node(entry)); |
438 | entry->ino = ino; |
439 | entry->name = strndup(src: name, n: name_len); |
440 | entry->name_len = name_len; |
441 | entry->type = type; |
442 | list_node_append(head: &state->entries, list_node(entry)); |
443 | state->n_count++; |
444 | } |
445 | |
446 | void vfs_populate_listdir_buf(dentry_t *dir, vfs_listdir_state_t *state) |
447 | { |
448 | // this call may not write all the entries, because the buffer may not be big enough |
449 | if (dir->inode->ops && dir->inode->ops->iterate_dir) |
450 | dir->inode->ops->iterate_dir(dir, state, dirter_add); |
451 | else |
452 | vfs_generic_iterate_dir(dir, state, op: dirter_add); |
453 | } |
454 | |