file: use lstat for file list (instead of stat) to reveal links master
authorPaul Donald <newtwen+github@gmail.com>
Wed, 11 Jun 2025 21:52:01 +0000 (23:52 +0200)
committerRobert Marko <robimarko@gmail.com>
Thu, 30 Oct 2025 11:07:04 +0000 (12:07 +0100)
I don't think there was a conscious design choice for stat(), but it is
documented that the 'type' entry using the rpcd file mechanisms can be
'symlink' but this is never the case using stat().

lstat and stat are virtually identical, but that lstat reports that
a symlink entry is a symlink, whereas stat reports that a symlink entry
is a file (the file it points to) and info about the link target
instead.

Someone parsing a directory using stat() would never know about links.

Using lstat, we can see whether an entry is a symlink for no extra cost.
When the file list encounters a link entry, it now includes the element
"target_type" which contains the link target type: "file", "directory"
etc - or "broken" if the link is broken - via one additional stat call.

So when calling e.g.: ubus call file list '{"path":"/etc"}'

instead of:

...
"name": "os-release",
"type": "file",
"size": 610,
"mode": 33188,
"atime": 1749462226,
"mtime": 1749462226,
"ctime": 1749462226,
"inode": 894,
"uid": 0,
"gid": 0
...

We get:

...
"name": "os-release",
"type": "symlink",
"size": 21,
"mode": 41471,
"atime": 1749462226,
"mtime": 1749462226,
"ctime": 1749462226,
"inode": 234,
"uid": 0,
"gid": 0,
"target_type": "file"
...

Moved the type determination out of _rpc_file_add_stat to a new function
_get_stat_type for re-usability in the if (S_ISLNK(s.st_mode)) block.

Tested on: openwrt main

Signed-off-by: Paul Donald <newtwen+github@gmail.com>
Tested-by: Eric Fahlgren <ericfahlgren@gmail.com>
Link: https://github.com/openwrt/rpcd/pull/13
Signed-off-by: Robert Marko <robimarko@gmail.com>
file.c

diff --git a/file.c b/file.c
index f14b274f0adb3cc60f81ccf2fc169e3f40d749fc..dc9c88a41890ea3affe2bf7c1df7869030e4f2c8 100644 (file)
--- a/file.c
+++ b/file.c
@@ -470,8 +470,8 @@ rpc_file_md5(struct ubus_context *ctx, struct ubus_object *obj,
        return UBUS_STATUS_OK;
 }
 
-static void
-_rpc_file_add_stat(struct stat *s)
+static int
+_get_stat_type(struct stat *s)
 {
        int type;
 
@@ -483,8 +483,13 @@ _rpc_file_add_stat(struct stat *s)
                    S_ISLNK(s->st_mode) ? DT_LNK :
                     S_ISSOCK(s->st_mode) ? DT_SOCK :
                      DT_UNKNOWN;
+       return type;
+}
 
-       blobmsg_add_string(&buf, "type", d_types[type]);
+static void
+_rpc_file_add_stat(struct stat *s)
+{
+       blobmsg_add_string(&buf, "type", d_types[_get_stat_type(s)]);
        blobmsg_add_u32(&buf, "size",  s->st_size);
        blobmsg_add_u32(&buf, "mode",  s->st_mode);
        blobmsg_add_u32(&buf, "atime", s->st_atime);
@@ -523,11 +528,22 @@ rpc_file_list(struct ubus_context *ctx, struct ubus_object *obj,
                if (asprintf(&entrypath, "%s/%s", path, e->d_name) < 0)
                        continue;
 
-               if (!stat(entrypath, &s))
+               // Use lstat to detect symlinks
+               if (!lstat(entrypath, &s))
                {
                        d = blobmsg_open_table(&buf, NULL);
                        blobmsg_add_string(&buf, "name", e->d_name);
                        _rpc_file_add_stat(&s);
+
+                       // add target type only for symlinks
+                       if (S_ISLNK(s.st_mode)) {
+                               struct stat target;
+                               if (!stat(entrypath, &target)) {
+                                       blobmsg_add_string(&buf, "target_type", d_types[_get_stat_type(&target)]);
+                               } else {
+                                       blobmsg_add_string(&buf, "target_type", "broken");
+                               }
+                       }
                        blobmsg_close_table(&buf, d);
                }