Skip to content

Commit 57ef31c

Browse files
committed
Update project
1 parent f64f89e commit 57ef31c

File tree

21 files changed

+287
-122
lines changed

21 files changed

+287
-122
lines changed

.github/pull_request_template.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!--
2+
Thank you for submitting a Pull Request. Please:
3+
* Read our commit style guide:
4+
Commit messages should adhere to the following points:
5+
* Separate subject from body with a blank line
6+
* Limit the subject line to 50 characters as much as possible
7+
* Capitalize the subject line
8+
* Do not end the subject line with a period
9+
* Use the imperative mood in the subject line
10+
* The verb should represent what was accomplished (Create, Add, Fix etc)
11+
* Wrap the body at 72 characters
12+
* Use the body to explain the what and why vs. the how
13+
For an example, look at the following link:
14+
https://docs.dissect.tools/en/latest/contributing/style-guide.html#example-commit-message
15+
16+
* Include a description of the proposed changes and how to test them.
17+
18+
* After creation, associate the PR with an issue, under the development section.
19+
Or use closing keywords in the body during creation:
20+
E.G:
21+
* close(|s|d) #<nr>
22+
* fix(|es|ed) #<nr>
23+
* resolve(|s|d) #<nr>
24+
-->

.github/workflows/dissect-ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,6 @@ jobs:
3232
trigger-tests:
3333
needs: [publish]
3434
uses: fox-it/dissect-workflow-templates/.github/workflows/dissect-ci-demand-test-template.yml@main
35+
secrets: inherit
3536
with:
3637
on-demand-test: 'dissect.target'

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ dist/
66
*.pyc
77
__pycache__/
88
.pytest_cache/
9-
tests/docs/api
10-
tests/docs/build
9+
tests/_docs/api
10+
tests/_docs/build
1111
.tox/

dissect/qnxfs/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
)
99
from dissect.qnxfs.qnx4 import QNX4
1010
from dissect.qnxfs.qnx6 import QNX6
11-
from dissect.qnxfs.qnxfs import QNXFS
11+
from dissect.qnxfs.qnxfs import QNXFS, is_qnxfs
1212

1313
__all__ = [
1414
"c_qnx4",
@@ -19,6 +19,7 @@
1919
"FileNotFoundError",
2020
"NotADirectoryError",
2121
"NotASymlinkError",
22+
"is_qnxfs",
2223
"QNX4",
2324
"QNX6",
2425
"QNXFS",

dissect/qnxfs/c_qnx4.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,5 +102,4 @@
102102
};
103103
"""
104104

105-
c_qnx4 = cstruct()
106-
c_qnx4.load(qnx4_def)
105+
c_qnx4 = cstruct().load(qnx4_def)

dissect/qnxfs/c_qnx6.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,7 @@
131131
"""
132132

133133

134-
c_qnx6_le = cstruct()
135-
c_qnx6_le.load(qnx6_def)
136-
c_qnx6_be = cstruct(endian=">")
137-
c_qnx6_be.load(qnx6_def)
134+
c_qnx6_le = cstruct().load(qnx6_def)
135+
c_qnx6_be = cstruct(endian=">").load(qnx6_def)
138136

139137
c_qnx6 = c_qnx6_le

dissect/qnxfs/exceptions.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ class Error(Exception):
22
pass
33

44

5-
class FileNotFoundError(Error):
5+
class FileNotFoundError(Error, FileNotFoundError):
66
pass
77

88

9-
class NotADirectoryError(Error):
9+
class IsADirectoryError(Error, IsADirectoryError):
10+
pass
11+
12+
13+
class NotADirectoryError(Error, NotADirectoryError):
1014
pass
1115

1216

dissect/qnxfs/qnx4.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
from __future__ import annotations
66

77
import stat
8-
from datetime import datetime
98
from functools import cached_property, lru_cache
10-
from typing import BinaryIO, Iterator, Optional
9+
from typing import TYPE_CHECKING, BinaryIO
1110

1211
from dissect.util import ts
1312
from dissect.util.stream import RunlistStream
@@ -20,6 +19,10 @@
2019
NotASymlinkError,
2120
)
2221

22+
if TYPE_CHECKING:
23+
from collections.abc import Iterator
24+
from datetime import datetime
25+
2326

2427
class QNX4:
2528
"""QNX4 filesystem implementation.
@@ -29,20 +32,19 @@ class QNX4:
2932
"""
3033

3134
def __init__(self, fh: BinaryIO):
32-
self.fh = fh
33-
34-
fh.seek(c_qnx4.QNX4_BLOCK_SIZE)
35-
if fh.read(16) != b"/" + b"\x00" * 15:
35+
if not _is_qnx4(fh):
3636
raise ValueError("Invalid QNX4 filesystem")
3737

38+
self.fh = fh
39+
self.block_size = c_qnx4.QNX4_BLOCK_SIZE
3840
self.inode = lru_cache(1024)(self.inode)
3941

4042
self.root = INode(self, c_qnx4.QNX4_ROOT_INO * c_qnx4.QNX4_INODES_PER_BLOCK)
4143

4244
def inode(self, inum: int) -> INode:
4345
return INode(self, inum)
4446

45-
def get(self, path: str | int, node: Optional[INode] = None) -> INode:
47+
def get(self, path: str | int, node: INode | None = None) -> INode:
4648
if isinstance(path, int):
4749
return self.inode(path)
4850

@@ -79,7 +81,7 @@ def __repr__(self) -> str:
7981

8082
def _read_inode(self) -> c_qnx4.qnx4_inode_entry:
8183
block, index = divmod(self.inum, c_qnx4.QNX4_INODES_PER_BLOCK)
82-
self.fs.fh.seek((block * c_qnx4.QNX4_BLOCK_SIZE) + (index * c_qnx4.QNX4_DIR_ENTRY_SIZE))
84+
self.fs.fh.seek((block * self.fs.block_size) + (index * c_qnx4.QNX4_DIR_ENTRY_SIZE))
8385
return c_qnx4.qnx4_inode_entry(self.fs.fh)
8486

8587
@cached_property
@@ -123,8 +125,8 @@ def atime(self) -> datetime:
123125
return ts.from_unix(self.inode.di_atime)
124126

125127
@cached_property
126-
def ctime(self) -> int:
127-
"""Return the datetime creation time."""
128+
def ctime(self) -> datetime:
129+
"""Return the file change time."""
128130
return ts.from_unix(self.inode.di_ctime)
129131

130132
@cached_property
@@ -193,7 +195,7 @@ def is_ipc(self) -> bool:
193195

194196
def listdir(self) -> dict[str, INode]:
195197
"""Return a directory listing."""
196-
return {name: inode for name, inode in self.iterdir()}
198+
return dict(self.iterdir())
197199

198200
def iterdir(self) -> Iterator[tuple[str, INode]]:
199201
"""Iterate directory contents."""
@@ -203,7 +205,7 @@ def iterdir(self) -> Iterator[tuple[str, INode]]:
203205
fh = self.fs.fh
204206
for block, size in self._iter_chain():
205207
for i in range(c_qnx4.QNX4_INODES_PER_BLOCK * size):
206-
fh.seek((block * c_qnx4.QNX4_BLOCK_SIZE) + (i * c_qnx4.QNX4_DIR_ENTRY_SIZE))
208+
fh.seek((block * self.fs.block_size) + (i * c_qnx4.QNX4_DIR_ENTRY_SIZE))
207209

208210
buf = fh.read(c_qnx4.QNX4_DIR_ENTRY_SIZE)
209211
if len(buf) != c_qnx4.QNX4_DIR_ENTRY_SIZE:
@@ -222,7 +224,7 @@ def iterdir(self) -> Iterator[tuple[str, INode]]:
222224
inum = ((link_info.dl_inode_blk - 1) * c_qnx4.QNX4_INODES_PER_BLOCK) + link_info.dl_inode_ndx
223225

224226
if link_info.dl_lfn_blk:
225-
fh.seek((link_info.dl_lfn_blk - 1) * c_qnx4.QNX4_BLOCK_SIZE)
227+
fh.seek((link_info.dl_lfn_blk - 1) * self.fs.block_size)
226228
lfn_entry = c_qnx4.qnx4_longfilename_entry(fh)
227229
name = lfn_entry.lfn_name
228230
else:
@@ -245,7 +247,7 @@ def _iter_chain(self) -> Iterator[tuple[int, int]]:
245247

246248
xblk_num = self.inode.di_xblk
247249
while num_extents:
248-
self.fs.fh.seek((xblk_num - 1) * c_qnx4.QNX4_BLOCK_SIZE)
250+
self.fs.fh.seek((xblk_num - 1) * self.fs.block_size)
249251
xblk = c_qnx4.qnx4_xblk(self.fs.fh)
250252
if xblk.signature != b"IamXblk":
251253
raise Error("Invalid QNX4 xblk signature")
@@ -258,8 +260,13 @@ def _iter_chain(self) -> Iterator[tuple[int, int]]:
258260

259261
def dataruns(self) -> list[tuple[int, int]]:
260262
"""Return the data runlist."""
261-
return [(block, size) for block, size in self._iter_chain()]
263+
return list(self._iter_chain())
262264

263265
def open(self) -> BinaryIO:
264266
"""Return a file-like object for reading the file."""
265-
return RunlistStream(self.fs.fh, self.dataruns(), self.size, c_qnx4.QNX4_BLOCK_SIZE)
267+
return RunlistStream(self.fs.fh, self.dataruns(), self.size, self.fs.block_size)
268+
269+
270+
def _is_qnx4(fh: BinaryIO) -> bool:
271+
fh.seek(c_qnx4.QNX4_BLOCK_SIZE)
272+
return fh.read(16) == b"/" + b"\x00" * 15

dissect/qnxfs/qnx6.py

Lines changed: 33 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66

77
import stat
88
import struct
9-
from datetime import datetime
109
from functools import cached_property, lru_cache
11-
from typing import BinaryIO, Iterator, Optional
10+
from typing import TYPE_CHECKING, BinaryIO
1211
from uuid import UUID
1312

1413
from dissect.util import ts
@@ -22,6 +21,12 @@
2221
NotASymlinkError,
2322
)
2423

24+
if TYPE_CHECKING:
25+
from collections.abc import Iterator
26+
from datetime import datetime
27+
28+
from dissect.cstruct import cstruct
29+
2530

2631
class QNX6:
2732
"""QNX6 filesystem implementation.
@@ -34,29 +39,7 @@ def __init__(self, fh: BinaryIO):
3439
self.fh = fh
3540
self._c_qnx = None
3641

37-
sb_offset = None
38-
for sb_offset in [c_qnx6.QNX6_BOOTBLOCK_SIZE, 0]:
39-
fh.seek(sb_offset)
40-
try:
41-
sb = c_qnx6_le.qnx6_super_block(fh)
42-
except EOFError:
43-
continue
44-
45-
if sb.sb_magic != c_qnx6.QNX6_SUPER_MAGIC:
46-
fh.seek(sb_offset)
47-
sb = c_qnx6_be.qnx6_super_block(fh)
48-
if sb.sb_magic != c_qnx6.QNX6_SUPER_MAGIC:
49-
continue
50-
51-
self._c_qnx = c_qnx6_be
52-
else:
53-
self._c_qnx = c_qnx6_le
54-
55-
self.sb1 = sb
56-
break
57-
else:
58-
raise ValueError("Unable to find QNX6 superblock")
59-
42+
sb_offset, self.sb1, self._c_qnx = _find_sb(fh)
6043
second_sb_offset = self.sb1.sb_num_blocks * self.sb1.sb_blocksize + sb_offset + c_qnx6.QNX6_SUPERBLOCK_AREA
6144
fh.seek(second_sb_offset)
6245
self.sb2 = self._c_qnx.qnx6_super_block(fh)
@@ -94,7 +77,7 @@ def inode(self, inum: int) -> INode:
9477
"""Return an inode by number."""
9578
return INode(self, inum)
9679

97-
def get(self, path: str | int, node: Optional[INode] = None) -> INode:
80+
def get(self, path: str | int, node: INode | None = None) -> INode:
9881
"""Return an inode by path."""
9982
if isinstance(path, int):
10083
return self.inode(path)
@@ -171,8 +154,8 @@ def atime(self) -> datetime:
171154
return ts.from_unix(self.inode.di_atime)
172155

173156
@cached_property
174-
def ctime(self) -> int:
175-
"""Return the datetime creation time."""
157+
def ctime(self) -> datetime:
158+
"""Return the file change time."""
176159
return ts.from_unix(self.inode.di_ctime)
177160

178161
@cached_property
@@ -231,7 +214,7 @@ def is_ipc(self) -> bool:
231214

232215
def listdir(self) -> dict[str, INode]:
233216
"""Return a directory listing."""
234-
return {name: inode for name, inode in self.iterdir()}
217+
return dict(self.iterdir())
235218

236219
def iterdir(self) -> Iterator[tuple[str, INode]]:
237220
"""Iterate directory contents."""
@@ -267,6 +250,27 @@ def open(self) -> BinaryIO:
267250
return RunlistStream(self.fs.fh, self.dataruns(), self.size, self.fs.block_size)
268251

269252

253+
def _find_sb(fh: BinaryIO) -> tuple[int, c_qnx6.qnx6_super_block, cstruct]:
254+
sb_offset = None
255+
for sb_offset in [c_qnx6.QNX6_BOOTBLOCK_SIZE, 0]:
256+
fh.seek(sb_offset)
257+
try:
258+
sb = c_qnx6_le.qnx6_super_block(fh)
259+
except EOFError:
260+
continue
261+
262+
if sb.sb_magic == c_qnx6.QNX6_SUPER_MAGIC:
263+
return sb_offset, sb, c_qnx6_le
264+
265+
# Try big-endian
266+
fh.seek(sb_offset)
267+
sb = c_qnx6_be.qnx6_super_block(fh)
268+
if sb.sb_magic == c_qnx6.QNX6_SUPER_MAGIC:
269+
return sb_offset, sb, c_qnx6_be
270+
271+
raise ValueError("Unable to find QNX6 superblock")
272+
273+
270274
def _generate_dataruns(fs: QNX6, size: int, pointers: list[int], levels: int) -> Iterator[tuple[int, int]]:
271275
if levels == 0:
272276
for ptr in pointers:
@@ -283,21 +287,3 @@ def _generate_dataruns(fs: QNX6, size: int, pointers: list[int], levels: int) ->
283287
fs.fh.seek((fs._blocks_offset + ptr) * fs.block_size)
284288
blocks = struct.unpack(f"{fs._c_qnx.endian}{fs.block_size // 4}I", fs.fh.read(fs.block_size))
285289
yield from _generate_dataruns(fs, size, blocks, levels - 1)
286-
287-
288-
if __name__ == "__main__":
289-
import sys
290-
291-
from dissect.target import container, volume
292-
293-
vol = container.open(sys.argv[1])
294-
295-
if ".vmdk" in sys.argv[1]:
296-
vs = volume.open(vol)
297-
vol = vs.volumes[0]
298-
299-
fs = QNX6(vol)
300-
301-
from IPython import embed
302-
303-
embed(colors="Linux")

dissect/qnxfs/qnxfs.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
from typing import BinaryIO
44

5-
from dissect.qnxfs.qnx4 import QNX4
6-
from dissect.qnxfs.qnx6 import QNX6
5+
from dissect.qnxfs.qnx4 import QNX4, _is_qnx4
6+
from dissect.qnxfs.qnx6 import QNX6, _find_sb
77

88

99
def QNXFS(fh: BinaryIO) -> QNX4 | QNX6:
@@ -18,3 +18,17 @@ def QNXFS(fh: BinaryIO) -> QNX4 | QNX6:
1818
pass
1919

2020
raise ValueError("Unable to open QNX filesystem")
21+
22+
23+
def is_qnxfs(fh: BinaryIO) -> bool:
24+
if _is_qnx4(fh):
25+
return True
26+
27+
try:
28+
_find_sb(fh)
29+
except ValueError:
30+
pass
31+
else:
32+
return True
33+
34+
return False

0 commit comments

Comments
 (0)