Skip to content

Commit 1e22761

Browse files
JSCU-CNISchamper
authored andcommitted
Add JFFS2 support to dissect.target (fox-it#417)
Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com>
1 parent dc4f539 commit 1e22761

File tree

4 files changed

+129
-2
lines changed

4 files changed

+129
-2
lines changed

dissect/target/containers/vhd.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ def __init__(self, fh: Union[BinaryIO, Path], *args, **kwargs):
2020

2121
@staticmethod
2222
def _detect_fh(fh: BinaryIO, original: Union[list, BinaryIO]) -> bool:
23-
fh.seek(-512, io.SEEK_END)
24-
return b"conectix" in fh.read(9)
23+
try:
24+
fh.seek(-512, io.SEEK_END)
25+
return b"conectix" in fh.read(9)
26+
except OSError:
27+
return False
2528

2629
@staticmethod
2730
def detect_path(path: Path, original: Union[list, BinaryIO]) -> bool:

dissect/target/filesystem.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,3 +1572,4 @@ def open_multi_volume(fhs: list[BinaryIO], *args, **kwargs) -> Filesystem:
15721572
register("squashfs", "SquashFSFilesystem")
15731573
register("zip", "ZipFilesystem")
15741574
register("ad1", "AD1Filesystem")
1575+
register("jffs", "JFFSFilesystem")

dissect/target/filesystems/jffs.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from typing import BinaryIO, Iterator, Optional
2+
3+
from dissect.jffs import jffs2
4+
from dissect.jffs.c_jffs2 import c_jffs2
5+
6+
from dissect.target.exceptions import (
7+
FileNotFoundError,
8+
FilesystemError,
9+
IsADirectoryError,
10+
NotADirectoryError,
11+
NotASymlinkError,
12+
)
13+
from dissect.target.filesystem import Filesystem, FilesystemEntry
14+
from dissect.target.helpers import fsutil
15+
16+
17+
class JFFSFilesystem(Filesystem):
18+
__type__ = "jffs"
19+
20+
def __init__(self, fh: BinaryIO, *args, **kwargs):
21+
super().__init__(fh, *args, **kwargs)
22+
self.jffs2 = jffs2.JFFS2(fh)
23+
24+
@staticmethod
25+
def _detect(fh: BinaryIO) -> bool:
26+
return int.from_bytes(fh.read(2), "little") in (
27+
c_jffs2.JFFS2_MAGIC_BITMASK,
28+
c_jffs2.JFFS2_OLD_MAGIC_BITMASK,
29+
)
30+
31+
def get(self, path: str) -> FilesystemEntry:
32+
return JFFSFilesystemEntry(self, path, self._get_node(path))
33+
34+
def _get_node(self, path: str, node: Optional[jffs2.INode] = None) -> jffs2.INode:
35+
try:
36+
return self.jffs2.get(path, node)
37+
except jffs2.FileNotFoundError as e:
38+
raise FileNotFoundError(path, cause=e)
39+
except jffs2.NotADirectoryError as e:
40+
raise NotADirectoryError(path, cause=e)
41+
except jffs2.NotASymlinkError as e:
42+
raise NotASymlinkError(path, cause=e)
43+
except jffs2.Error as e:
44+
raise FileNotFoundError(path, cause=e)
45+
46+
47+
class JFFSFilesystemEntry(FilesystemEntry):
48+
fs: JFFSFilesystem
49+
entry: jffs2.INode
50+
51+
def get(self, path: str) -> FilesystemEntry:
52+
entry_path = fsutil.join(self.path, path, alt_separator=self.fs.alt_separator)
53+
entry = self.fs._get_node(path, self.entry)
54+
return JFFSFilesystemEntry(self.fs, entry_path, entry)
55+
56+
def open(self) -> BinaryIO:
57+
if self.is_dir():
58+
raise IsADirectoryError(self.path)
59+
return self._resolve().entry.open()
60+
61+
def _iterdir(self) -> Iterator[tuple[str, jffs2.INode]]:
62+
if not self.is_dir():
63+
raise NotADirectoryError(self.path)
64+
65+
if self.is_symlink():
66+
yield from self.readlink_ext().iterdir()
67+
else:
68+
yield from self.entry.iterdir()
69+
70+
def iterdir(self) -> Iterator[str]:
71+
for name, _ in self._iterdir():
72+
yield name
73+
74+
def scandir(self) -> Iterator[FilesystemEntry]:
75+
for name, entry in self._iterdir():
76+
entry_path = fsutil.join(self.path, name, alt_separator=self.fs.alt_separator)
77+
yield JFFSFilesystemEntry(self.fs, entry_path, entry)
78+
79+
def is_dir(self, follow_symlinks: bool = False) -> bool:
80+
try:
81+
return self._resolve(follow_symlinks).entry.is_dir()
82+
except FilesystemError:
83+
return False
84+
85+
def is_file(self, follow_symlinks: bool = False) -> bool:
86+
try:
87+
return self._resolve(follow_symlinks).entry.is_file()
88+
except FilesystemError:
89+
return False
90+
91+
def is_symlink(self) -> bool:
92+
return self.entry.is_symlink()
93+
94+
def readlink(self) -> str:
95+
if not self.is_symlink():
96+
raise NotASymlinkError()
97+
98+
return self.entry.link
99+
100+
def stat(self, follow_symlinks: bool = False) -> fsutil.stat_result:
101+
return self._resolve(follow_symlinks).lstat()
102+
103+
def lstat(self) -> fsutil.stat_result:
104+
node = self.entry.inode
105+
106+
# mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime
107+
st_info = fsutil.stat_result(
108+
[
109+
self.entry.mode,
110+
self.entry.inum,
111+
id(self.fs),
112+
1, # TODO: properly calculate nlink in dissect.jffs
113+
node.uid,
114+
node.gid,
115+
node.isize,
116+
self.entry.atime.timestamp(),
117+
self.entry.mtime.timestamp(),
118+
self.entry.ctime.timestamp(),
119+
]
120+
)
121+
122+
return st_info

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ full = [
5656
"dissect.fat>=3.0.dev,<4.0.dev",
5757
"dissect.ffs>=3.0.dev,<4.0.dev",
5858
"dissect.ole>=3.0.dev,<4.0.dev",
59+
"dissect.jffs>=1.0.dev,<2.0.dev",
5960
"dissect.shellitem>=3.0.dev,<4.0.dev",
6061
"dissect.squashfs>=1.0.dev,<2.0.dev",
6162
"dissect.sql>=3.0.dev,<4.0.dev",

0 commit comments

Comments
 (0)