Skip to content

Instantly share code, notes, and snippets.

@bluec0re
Created May 14, 2024 09:33
Show Gist options
  • Save bluec0re/036cf07eed038178c9c70c7ee19c6308 to your computer and use it in GitHub Desktop.
Save bluec0re/036cf07eed038178c9c70c7ee19c6308 to your computer and use it in GitHub Desktop.
Simple deb package browser
#!/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