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