Skip to content

Commit d06a077

Browse files
committed
feature: calculate nlinks
1 parent a37a8d2 commit d06a077

2 files changed

Lines changed: 39 additions & 0 deletions

File tree

dissect/jffs/jffs2.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,12 @@ class JFFS2:
4343
def __init__(self, fh: BinaryIO):
4444
self.fh = fh
4545

46+
# Dict of ino to list of tuples of (inode, offset)
4647
self._inodes: dict[int, list[tuple[c_jffs2.jffs2_raw_inode, int]]] = {}
48+
# Dict of pino to dict of dirent names to list of dirents
4749
self._dirents: dict[int, dict[bytes, list[DirEntry]]] = {}
4850
self._lost_found: list[list[DirEntry]] = []
51+
self._nlinksByInum: dict[int, int] = {} # Map from nlinks to inum
4952

5053
if (node := c_jffs2.jffs2_unknown_node(fh)).magic not in JFFS2_MAGIC_NUMBERS:
5154
raise Error(f"Unknown JFFS2 magic: {node.magic:#x}")
@@ -55,6 +58,7 @@ def __init__(self, fh: BinaryIO):
5558
self.root = self.inode(inum=0x1, type=c_jffs2.DT_DIR, parent=None)
5659

5760
self._scan()
61+
self._count_nlinks()
5862
self._garbage_collect()
5963

6064
def inode(self, inum: int, type: int | None = None, parent: INode | None = None) -> INode:
@@ -147,6 +151,34 @@ def _scan(self) -> None:
147151

148152
pos += (totlen + 3) & ~3
149153

154+
def _count_nlinks(self) -> None:
155+
"""Count the number of hardlinks for each inode.
156+
157+
JFFS does not store nlink information in the inode itself, so we have to calculate it.
158+
"""
159+
for direntries in self._dirents.values():
160+
for versions in direntries.values():
161+
if not versions:
162+
continue
163+
164+
last_version = versions[-1]
165+
if last_version.type == c_jffs2.DT_DIR:
166+
# Root dir (inum == 1) gets three nlinks
167+
# (see https://github.com/torvalds/linux/blob/6485cf5ea253d40d507cd71253c9568c5470cd27/fs/jffs2/fs.c#L311)
168+
self._nlinksByInum[last_version.inum] = 3 if last_version.inum == 1 else 2
169+
170+
# Now update the parent entry, which might not have an associated nlink yet.
171+
# The parent directory gets one nlink for each child directory.
172+
base_nlinks = 3 if last_version.parent_inum == 1 else 2
173+
self._nlinksByInum[last_version.parent_inum] = (
174+
self._nlinksByInum.get(last_version.parent_inum, base_nlinks) + 1
175+
)
176+
177+
elif last_version.type == c_jffs2.DT_REG:
178+
self._nlinksByInum[last_version.inum] = self._nlinksByInum.get(last_version.inum, 0) + 1
179+
elif last_version.type == c_jffs2.DT_LNK:
180+
self._nlinksByInum[last_version.inum] = 1
181+
150182
def _garbage_collect(self) -> None:
151183
"""Collect all found orphaned files and put them in the lost+found folder."""
152184
if not self._lost_found:
@@ -267,6 +299,10 @@ def uid(self) -> int:
267299
def gid(self) -> int:
268300
return self.inode.gid
269301

302+
@cached_property
303+
def nlink(self) -> int:
304+
return self.fs._nlinksByInum.get(self.inum, 0)
305+
270306
def is_dir(self) -> bool:
271307
return self.type == stat.S_IFDIR
272308

tests/test_jffs2.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,20 @@ def test_jffs2_uncompressed(jffs2_bin: BinaryIO) -> None:
1616

1717
root = fs.root
1818
assert root.is_dir()
19+
assert root.nlink == 4 # 3 from root and 1 from subdirectory
1920
assert list(root.listdir().keys()) == ["foo", "test.txt"]
2021

2122
test_file = fs.get("/test.txt")
2223
assert test_file.is_file()
24+
assert test_file.nlink == 1
2325
assert test_file.atime == datetime(2023, 6, 23, 20, 27, 20, tzinfo=timezone.utc)
2426
assert test_file.ctime == datetime(2023, 6, 23, 20, 27, 20, tzinfo=timezone.utc)
2527
assert test_file.mtime == datetime(2023, 6, 23, 20, 27, 20, tzinfo=timezone.utc)
2628
assert test_file.open().read() == b"contents\n"
2729

2830
link_file = fs.get("/foo/bar/link.txt")
2931
assert link_file.is_symlink()
32+
assert link_file.nlink == 1
3033
assert link_file.link == "/test.txt"
3134

3235

0 commit comments

Comments
 (0)