@@ -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
0 commit comments