File: d3pkg.py

package info (click to toggle)
game-data-packager 87
  • links: PTS, VCS
  • area: contrib
  • in suites: forky, sid
  • size: 33,392 kB
  • sloc: python: 15,387; sh: 704; ansic: 95; makefile: 50
file content (132 lines) | stat: -rw-r--r-- 3,689 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/usr/bin/python3
# encoding=utf-8
#
# Copyright © 2024 Sébastien Noel <sebastien@twolife.be>
# SPDX-License-Identifier: GPL-2.0-or-later

from __future__ import annotations

import logging
import os
from collections.abc import Collection
from types import TracebackType

from . import (SimpleUnpackable)
from ..util import (mkdir_p)

PKG_MAGIC = 0x4f504b47

logger = logging.getLogger(__name__)


class D3Entry():
    """Data class for a file inside a Descent3 .pkg file."""

    def __init__(self, path: str) -> None:
        self.path = path
        self.offset = 0
        self.size = 0


class D3Pkg(SimpleUnpackable):
    """Object representing a Descent3 .pkg file."""

    def __init__(self, path: str) -> None:
        self.path = path
        self.n_files = 0
        self.files: list[D3Entry] = []
        self._parse_archive()

    def __enter__(self) -> D3Pkg:
        return self

    def __exit__(
        self,
        _et: type[BaseException] | None = None,
        _ev: BaseException | None = None,
        _tb: TracebackType | None = None
    ) -> None:
        pass

    def _parse_archive(self) -> None:
        reader = open(self.path, 'rb')
        archive_size = os.path.getsize(self.path)

        magic = int.from_bytes(reader.read(4), byteorder='little')
        if magic != PKG_MAGIC:
            raise ValueError(
                '"%s" is not a .pkg: magic number does not match' % self.path)

        self.n_files = int.from_bytes(reader.read(4), byteorder='little')
        logger.debug('%s contains %d files', self.path, self.n_files)

        nextFileStart = reader.tell()

        while nextFileStart < archive_size:
            reader.seek(nextFileStart)

            length = int.from_bytes(reader.read(4), byteorder='little')
            str_dirname = reader.read(length)[:-1].decode('windows-1252')
            str_dirname = str_dirname.replace("\\", "/")

            length = int.from_bytes(reader.read(4), byteorder='little')
            str_basename = reader.read(length)[:-1].decode('windows-1252')

            entry = D3Entry(os.path.join(str_dirname, str_basename))
            entry.size = int.from_bytes(reader.read(4), byteorder='little')
            logger.debug('Found %s - size: %d', entry.path, entry.size)

            # skip the next 8 bytes (checksum ?)
            reader.seek(8, 1)

            entry.offset = reader.tell()
            nextFileStart = entry.offset + entry.size

            self.files.append(entry)

        reader.close()

    def extractall(
        self,
        outdir: str,
        members: Collection[str] | None = None,
    ) -> None:
        # TODO: The members hint is ignored, we only support extracting
        # everything

        reader = open(self.path, 'rb')

        for entry in self.files:
            dest_file = os.path.join(outdir, entry.path)
            mkdir_p(os.path.dirname(dest_file))
            reader.seek(entry.offset)

            logger.debug('Extracting %s ...', dest_file)
            with open(dest_file, "wb") as f:
                f.write(reader.read(entry.size))
        reader.close()

    def printdir(self) -> None:
        for entry in self.files:
            print("%s @%d - size: %d" % (entry.path, entry.offset, entry.size))

    @property
    def format(self) -> str:
        return 'd3pkg'


if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument(
        '--output', '-o', help='extract to OUTPUT', default=None)
    parser.add_argument('pkg')
    args = parser.parse_args()

    archive = D3Pkg(args.pkg)

    if args.output:
        archive.extractall(args.output)
    else:
        archive.printdir()