Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions polyfile/magic.py
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,45 @@ def __str__(self):
return f"({self.offset!s}{['.', ','][self.signed]}{num_bytes}{self.endianness.value})"


def rebase_in_named_test(offset: Offset, named_test: "NamedTest") -> Offset:
"""Resolves the positions in `offset` against the offset a named test was invoked at.

A test nested under a ``name`` block counts its offsets from wherever the ``use`` that
dispatched it matched, not from the start of the file. `NamedAbsoluteOffset` does that for a
test whose own offset is absolute, and this reaches the absolute offsets underneath one that
is not.

An indirect offset reads its pointer at a position, so that position is rebased too. A
relative (``&``) offset holds a *distance* from the previous match rather than a position, so
what it wraps is left alone: rebasing a distance would add the ``use`` site to it twice.

Args:
offset: The offset as parsed, counted from the start of the file.
named_test: The ``name`` block the test carrying `offset` belongs to.

Returns:
The offset with every position it reads resolved against the ``use`` site. The argument is
returned unchanged when it holds no absolute position.
"""
if isinstance(offset, NamedAbsoluteOffset):
return offset
if isinstance(offset, AbsoluteOffset):
return NamedAbsoluteOffset(named_test, offset.offset)
if isinstance(offset, IndirectOffset):
rebased = rebase_in_named_test(offset.offset, named_test)
if rebased is offset.offset:
return offset
return IndirectOffset(
offset=rebased,
num_bytes=offset.num_bytes,
endianness=offset.endianness,
signed=offset.signed,
post_process=offset.post_process,
is_id3=offset.is_id3,
)
return offset


INDIRECT_OFFSET_TYPES: Dict[Tuple[int, Endianness], str] = {
(1, Endianness.LITTLE): "byte", (1, Endianness.BIG): "byte",
(2, Endianness.LITTLE): "leshort", (2, Endianness.BIG): "beshort",
Expand Down Expand Up @@ -1110,8 +1149,8 @@ def __init__(
self.level: int = self.parent.level + 1
parent.children.append(self)
self.named_test: Optional[NamedTest] = parent.named_test
if self.named_test is not None and isinstance(offset, AbsoluteOffset):
self.offset = NamedAbsoluteOffset(self.named_test, offset.offset)
if self.named_test is not None:
self.offset = rebase_in_named_test(offset, self.named_test)
if mime is not None:
parent.can_match_mime = True
else:
Expand Down
51 changes: 51 additions & 0 deletions tests/test_magic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2051,6 +2051,57 @@ def test_an_env_python_script_is_described(self):
self.assertNotIn("Python script text executable", messages)


class NamedTestOffsetTest(TestCase):
"""An offset inside a `name` block counts from wherever the `use` dispatched it.

`NamedAbsoluteOffset` does that for a test whose own offset is absolute. An indirect offset
reads its pointer at a position too, and that position was left counting from the start of the
file, so every `(N.x)` inside a named list read the wrong bytes. On a Mach-O universal binary
that meant reading the CPU type where the architecture's file offset should be, and reporting
`[x86_64:]` with nothing inside the brackets.
"""

POINTER_IN_NAMED_LIST: str = "\n".join((
"0\tname\tblk\t\\b [",
">(4.L)\tindirect\tx\t\\b:",
"",
"0\tstring\tMAGI\tbase",
">8\tuse\tblk\t\\b",
"",
"0\tstring\tNESTED\tnested",
"",
))
"""`blk` is dispatched at offset 8, so its `(4.L)` reads the pointer at offset 12."""

@staticmethod
def sample() -> bytes:
"""A file whose pointer at offset 12 leads to `NESTED`, with a decoy at offset 4.

The decoy is what an offset counted from the start of the file would read instead.
"""
data = bytearray(b"\x00" * 48)
data[0:4] = b"MAGI"
data[4:8] = struct.pack(">I", 44)
data[12:16] = struct.pack(">I", 32)
data[32:38] = b"NESTED"
return bytes(data)

def messages(self, definitions: str, data: bytes) -> Set[str]:
"""Matches `data` against ad-hoc definitions written to a temporary file."""
with TemporaryDirectory() as directory:
path = Path(directory) / "definitions"
path.write_text(definitions)
return {str(match) for match in MagicMatcher.parse(path).match(data)}

def test_an_indirect_offset_reads_from_the_use_site(self):
"""`file` 5.48 reports `base [:nested` for this input."""
messages = self.messages(self.POINTER_IN_NAMED_LIST, self.sample())
self.assertTrue(
any("nested" in message for message in messages),
f"the named list's indirect offset did not reach the pointer it declares: {messages!r}",
)


class UseTestSemanticsTest(TestCase):
"""Regression tests for the `use` test truth value reported in issue #3484."""

Expand Down
Loading