Created
May 14, 2024 09:33
-
-
Save bluec0re/036cf07eed038178c9c70c7ee19c6308 to your computer and use it in GitHub Desktop.
Simple deb package browser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import argparse | |
import dataclasses | |
import datetime | |
import os | |
import subprocess | |
import tarfile | |
import tempfile | |
from collections.abc import Generator | |
from typing import IO, TypeVar | |
T = TypeVar("T", bytes, str) | |
class IOView(IO[T]): | |
def __init__(self, base: IO[T], start: int, end: int) -> None: | |
self._base: IO[T] = base | |
self._start = start | |
self._end = end | |
self._pos = start | |
def read(self, n: int = -1) -> T: | |
if n == -1 or self._end - n < self._pos: | |
n = self._end - self._pos | |
self._base.seek(self._pos) | |
out = self._base.read(n) | |
self._pos += len(out) | |
return out | |
def seek(self, n: int, whence: int = 0) -> int: | |
if whence == os.SEEK_SET: | |
self._pos = self._start + n | |
elif whence == os.SEEK_CUR: | |
self._pos += n | |
elif whence == os.SEEK_END: | |
self._pos = self._end - n | |
if self._pos < self._start: | |
self._pos = self._start | |
elif self._pos > self._end: | |
self._pos = self._end | |
return self._pos - self._start | |
def tell(self): | |
return self._pos - self._start | |
class ArFile: | |
@dataclasses.dataclass | |
class ArFileInfo: | |
name: str | |
offset: int | |
size: int | |
timestamp: datetime.datetime | |
owner: int | |
group: int | |
mode: int | |
def __init__(self, file_obj: IO[bytes]) -> None: | |
self._obj = file_obj | |
file_obj.seek(0, os.SEEK_END) | |
self._file_len = file_obj.tell() | |
file_obj.seek(0) | |
self._files: list[ArFile.ArFileInfo] = [] | |
assert self._obj.read(8) == b"!<arch>\n" | |
self._file_list_pos = self._obj.tell() | |
def iter_files(self) -> Generator[ArFileInfo, None, None]: | |
for info in self._files: | |
yield info | |
while self._file_list_pos < self._file_len: | |
self._obj.seek(self._file_list_pos) | |
filename = self._obj.read(16).decode().rstrip().removesuffix("/") | |
timestamp = datetime.datetime.fromtimestamp( | |
float(self._obj.read(12).decode().rstrip()) | |
) | |
owner_id = int(self._obj.read(6).decode().rstrip()) | |
group_id = int(self._obj.read(6).decode().rstrip()) | |
mode = int(self._obj.read(8).decode().rstrip(), 8) | |
size = int(self._obj.read(10).decode().rstrip()) | |
assert self._obj.read(2) == b"\x60\x0a" | |
info = ArFile.ArFileInfo( | |
name=filename, | |
offset=self._obj.tell(), | |
size=size, | |
timestamp=timestamp, | |
owner=owner_id, | |
group=group_id, | |
mode=mode, | |
) | |
self._file_list_pos = self._obj.tell() + size | |
if size % 2 != 0: | |
self._file_list_pos += 1 | |
self._files.append(info) | |
yield info | |
def getmembers(self) -> list[ArFileInfo]: | |
return list(self.iter_files()) | |
def extractfile(self, name_or_info: str | ArFileInfo) -> IO[bytes]: | |
if isinstance(name_or_info, str): | |
for info in self.iter_files(): | |
if info.name == name_or_info: | |
break | |
else: | |
raise IOError(f"File {name_or_info!r} not found in archive") | |
else: | |
info = name_or_info | |
return IOView(self._obj, info.offset, info.offset + info.size) | |
def browse(archive: ArFile | tarfile.TarFile): | |
files = archive.getmembers() | |
while True: | |
print() | |
for i, info in enumerate(files, 1): | |
print(f"{i}: {info.name}") | |
try: | |
res = input("> ") | |
if not res: | |
break | |
except EOFError: | |
break | |
info = files[int(res) - 1] | |
fp = archive.extractfile(info) | |
if fp: | |
ext = info.name.rsplit("/", 1)[-1].split(".", 1)[-1] | |
if ext.startswith("tar"): | |
tf = tarfile.open(fileobj=fp) | |
browse(tf) | |
elif ext.endswith(".deb"): | |
af = ArFile(fp) | |
browse(af) | |
else: | |
with tempfile.NamedTemporaryFile(suffix=ext) as tmp: | |
tmp.write(fp.read()) | |
tmp.flush() | |
subprocess.run(["vim", "-b", "-R", tmp.name]) | |
def main(): | |
parser = argparse.ArgumentParser() | |
parser.add_argument( | |
"input_file", metavar="INPUT_FILE", type=argparse.FileType("rb") | |
) | |
args = parser.parse_args() | |
arfile = ArFile(args.input_file) | |
browse(arfile) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment