Skip to content

Commit fca6e51

Browse files
authored
Merge pull request #12 from MAK-Relic-Tool/issue-#40-and-drive-name-hotifx
SGA Packing Patch
2 parents f887f00 + ad5d98d commit fca6e51

9 files changed

Lines changed: 189 additions & 64 deletions

File tree

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ relic.cli =
3838
relic.cli.sga =
3939
unpack = relic.sga.core.cli:RelicSgaUnpackCli
4040
pack = relic.sga.core.cli:RelicSgaPackCli
41+
repack = relic.sga.core.cli:RelicSgaRepackCli
4142

4243
[options.packages.find]
4344
where = src

src/relic/sga/core/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
"""
44
from relic.sga.core.definitions import Version, MagicWord, StorageType, VerificationType
55

6-
__version__ = "1.1.1"
6+
__version__ = "1.1.2"

src/relic/sga/core/cli.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from __future__ import annotations
22

3+
import argparse
4+
import os.path
35
from argparse import ArgumentParser, Namespace
4-
from typing import Optional
6+
from typing import Optional, Callable
57

68
import fs.copy
79
from fs.base import FS
@@ -20,6 +22,42 @@ def _create_parser(
2022
return command_group.add_parser("sga")
2123

2224

25+
def _arg_exists_err(value: str) -> argparse.ArgumentTypeError:
26+
return argparse.ArgumentTypeError(f"The given path '{value}' does not exist!")
27+
28+
29+
def _get_dir_type_validator(exists: bool) -> Callable[[str], str]:
30+
def _dir_type(path: str) -> str:
31+
if not os.path.exists(path):
32+
if exists:
33+
raise _arg_exists_err(path)
34+
else:
35+
return path
36+
37+
if os.path.isdir(path):
38+
return path
39+
40+
raise argparse.ArgumentTypeError(f"The given path '{path}' is not a directory!")
41+
42+
return _dir_type
43+
44+
45+
def _get_file_type_validator(exists: Optional[bool]) -> Callable[[str], str]:
46+
def _file_type(path: str) -> str:
47+
if not os.path.exists(path):
48+
if exists:
49+
raise _arg_exists_err(path)
50+
else:
51+
return path
52+
53+
if os.path.isfile(path):
54+
return path
55+
56+
raise argparse.ArgumentTypeError(f"The given path '{path}' is not a file!")
57+
58+
return _file_type
59+
60+
2361
class RelicSgaUnpackCli(CliPlugin):
2462
def _create_parser(
2563
self, command_group: Optional[_SubParsersAction] = None
@@ -30,8 +68,16 @@ def _create_parser(
3068
else:
3169
parser = command_group.add_parser("unpack")
3270

33-
parser.add_argument("src_sga", type=str, help="Source SGA File")
34-
parser.add_argument("out_dir", type=str, help="Output Directory")
71+
parser.add_argument(
72+
"src_sga",
73+
type=_get_file_type_validator(exists=True),
74+
help="Source SGA File",
75+
)
76+
parser.add_argument(
77+
"out_dir",
78+
type=_get_dir_type_validator(exists=False),
79+
help="Output Directory",
80+
)
3581

3682
return parser
3783

@@ -64,3 +110,23 @@ def _create_parser(
64110
# pack further delegates to version plugins
65111

66112
return parser
113+
114+
115+
class RelicSgaRepackCli(CliPluginGroup):
116+
"""An alternative to pack which 'repacks' an SGA. Intended for testing purposes."""
117+
118+
GROUP = "relic.cli.sga.repack"
119+
120+
def _create_parser(
121+
self, command_group: Optional[_SubParsersAction] = None
122+
) -> ArgumentParser:
123+
parser: ArgumentParser
124+
desc = "Debug Command; reads and repacks an SGA archive."
125+
if command_group is None:
126+
parser = ArgumentParser("repack", description=desc)
127+
else:
128+
parser = command_group.add_parser("repack", description=desc)
129+
130+
# pack further delegates to version plugins
131+
132+
return parser

src/relic/sga/core/filesystem.py

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,10 @@ def to_info(self, namespaces=None):
240240

241241

242242
class _EssenceDriveFS(MemoryFS):
243-
def __init__(self, alias: str) -> None:
243+
def __init__(self, alias: str, name: str) -> None:
244244
super().__init__()
245245
self.alias = alias
246+
self.name = name
246247

247248
def _make_dir_entry(
248249
self, resource_type: ResourceType, name: str
@@ -292,6 +293,22 @@ def setinfo(self, path: str, info: Mapping[str, Mapping[str, object]]) -> None:
292293
# if LAZY_NAMESPACE in info and not resource_entry.is_dir:
293294
# lazy
294295

296+
def getinfo(
297+
self, path, namespaces=None
298+
): # type: (Text, Optional[Collection[Text]]) -> Info
299+
info = super().getinfo(path, namespaces)
300+
301+
_path = self.validatepath(path)
302+
if _path == "/" and (
303+
namespaces is not None and ESSENCE_NAMESPACE in namespaces
304+
):
305+
raw_info = info.raw
306+
essence_ns = dict(raw_info[ESSENCE_NAMESPACE])
307+
essence_ns["alias"] = self.alias
308+
essence_ns["name"] = self.name
309+
info = Info(raw_info)
310+
return info
311+
295312
def getessence(self, path: str) -> Info:
296313
return self.getinfo(path, [ESSENCE_NAMESPACE])
297314

@@ -324,9 +341,12 @@ def setmeta(self, meta: Dict[str, Any], namespace: str = "standard") -> None:
324341
def getessence(self, path: str) -> Info:
325342
return self.getinfo(path, [ESSENCE_NAMESPACE])
326343

327-
def create_drive(self, name: str) -> _EssenceDriveFS:
328-
drive = _EssenceDriveFS(name)
329-
self.add_fs(name, drive)
344+
def create_drive(self, alias: str, name: str) -> _EssenceDriveFS:
345+
drive = _EssenceDriveFS(alias, name)
346+
first_drive = len([*self.iterate_fs()]) == 0
347+
self.add_fs(
348+
alias, drive, write=first_drive
349+
) # TODO see if name would work here, using alias because that is what it originally was
330350
return drive
331351

332352
def _delegate(self, path):
@@ -340,39 +360,6 @@ def _delegate(self, path):
340360
return super()._delegate(path)
341361

342362

343-
# if __name__ == "__main__":
344-
# test_file = File("test.txt", b"This is a Test!", StorageType.STORE, False, None)
345-
# test_folder = Folder("Test", [], [test_file])
346-
# data_folders = [test_folder]
347-
# data_files = []
348-
# data_drive = Drive("data", "", data_folders, data_files)
349-
# attr_drive = Drive("attr", "", [], [test_file])
350-
# archive = Archive("Test", None, [data_drive, attr_drive])
351-
#
352-
# with SGAFS() as fs:
353-
# data_fs = MemoryFS()
354-
# fs.add_fs("data", data_fs)
355-
# data_dir = data_fs.makedir("Test Data")
356-
# with data_dir.open("sample_data.txt", "wb") as data_sample_text:
357-
# data_sample_text.write(b"Sample Data Text!")
358-
#
359-
# attr_fs = MemoryFS()
360-
# fs.add_fs("attr", attr_fs)
361-
# attr_dir = attr_fs.makedir("Test Attr")
362-
# with attr_dir.open("sample_attr.txt", "wb") as attr_sample_text:
363-
# attr_sample_text.write(b"Sample Attr Text!")
364-
#
365-
# for root, folders, files in fs.walk():
366-
# print(root, "\n\t", folders, "\n\t", files)
367-
#
368-
# for name, sub_fs in fs.iterate_fs():
369-
# print(name)
370-
# for root, folders, files in sub_fs.walk():
371-
# print("\t", root, "\n\t\t", folders, "\n\t\t", files)
372-
#
373-
# print(fs.getinfo("/", ["basic", "access"]).raw)
374-
# pass
375-
376363
__all__ = [
377364
"ESSENCE_NAMESPACE",
378365
"EssenceFSHandler",

src/relic/sga/core/serialization.py

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
Iterable,
1717
TypeVar,
1818
Generic,
19+
Type,
1920
)
2021

2122
from fs.base import FS
@@ -220,6 +221,8 @@ def _write_data(data: bytes, stream: BinaryIO) -> int:
220221

221222

222223
def _get_or_write_name(name: str, stream: BinaryIO, lookup: Dict[str, int]) -> int:
224+
# Tools don't like "/" so coerce "/" to "\"
225+
name = name.replace("/", "\\")
223226
if name in lookup:
224227
return lookup[name]
225228

@@ -399,7 +402,7 @@ def assemble_drive(
399402
drive_folder_index = drive_def.root_folder - folder_offset
400403
drive_folder_def = local_folder_defs[drive_folder_index]
401404

402-
drive = essence_fs.create_drive(drive_def.alias)
405+
drive = essence_fs.create_drive(drive_def.alias, drive_def.name)
403406
self._assemble_container(
404407
drive,
405408
drive_folder_def.file_range,
@@ -508,41 +511,70 @@ def flatten_folder_collection(self, container_fs: FS, path: str) -> Tuple[int, i
508511
self.flat_folders[subfolder_start:subfolder_end] = subfolder_defs
509512
return subfolder_start, subfolder_end
510513

514+
def _flatten_folder_names(self, fs: FS, path: str) -> None:
515+
folders = [file_info.name for file_info in fs.scandir("/") if file_info.is_dir]
516+
files = [file_info.name for file_info in fs.scandir("/") if file_info.is_file]
517+
518+
if len(path) > 0 and path[0] == "/":
519+
path = path[1:] # strip leading '/'
520+
_get_or_write_name(path, self.name_stream, self.flat_names)
521+
522+
for fold_path in folders:
523+
full_fold_path = f"{path}/{fold_path}"
524+
full_fold_path = str(full_fold_path).split(":", 1)[
525+
-1
526+
] # Strip 'alias:' from path
527+
if full_fold_path[0] == "/":
528+
full_fold_path = full_fold_path[1:] # strip leading '/'
529+
_get_or_write_name(full_fold_path, self.name_stream, self.flat_names)
530+
531+
for file_path in files:
532+
_get_or_write_name(file_path, self.name_stream, self.flat_names)
533+
511534
def disassemble_folder(self, folder_fs: FS, path: str) -> FolderDef:
512535
folder_def = FolderDef(None, None, None) # type: ignore
513-
514-
# Subfiles
515-
subfile_range = self.flatten_file_collection(folder_fs)
516-
# Subfolders
517-
# # Since Relic typically uses the first folder as the root folder; I will try to preserve that parent folders come before their child folders
518-
subfolder_range = self.flatten_folder_collection(folder_fs, path)
536+
# Write Name
537+
self._flatten_folder_names(folder_fs, path)
519538

520539
folder_name = str(path).split(":", 1)[-1] # Strip 'alias:' from path
521-
522540
if folder_name[0] == "/":
523541
folder_name = folder_name[1:] # strip leading '/'
524-
525542
folder_def.name_pos = _get_or_write_name(
526543
folder_name, self.name_stream, self.flat_names
527544
)
545+
546+
# Subfolders
547+
# # Since Relic typically uses the first folder as the root folder; I will try to preserve that parent folders come before their child folders
548+
subfolder_range = self.flatten_folder_collection(folder_fs, path)
549+
550+
# Subfiles
551+
subfile_range = self.flatten_file_collection(folder_fs)
552+
528553
folder_def.file_range = subfile_range
529554
folder_def.folder_range = subfolder_range
530555

531556
return folder_def
532557

533-
def disassemble_drive(self, drive: _EssenceDriveFS, alias: str) -> DriveDef:
534-
name = ""
558+
def disassemble_drive(self, drive: _EssenceDriveFS) -> DriveDef:
559+
name = drive.name
560+
folder_name = ""
561+
alias = drive.alias
535562
drive_folder_def = FolderDef(None, None, None) # type: ignore
563+
self._flatten_folder_names(drive, folder_name)
564+
536565
root_folder = len(self.flat_folders)
537566
folder_start = len(self.flat_folders)
538567
file_start = len(self.flat_files)
539568
self.flat_folders.append(drive_folder_def)
540569

570+
# Name should be an empty string?
541571
drive_folder_def.name_pos = _get_or_write_name(
542-
name, self.name_stream, self.flat_names
572+
folder_name, self.name_stream, self.flat_names
543573
)
544574
drive_folder_def.file_range = self.flatten_file_collection(drive)
545-
drive_folder_def.folder_range = self.flatten_folder_collection(drive, name)
575+
drive_folder_def.folder_range = self.flatten_folder_collection(
576+
drive, folder_name
577+
)
546578

547579
folder_end = len(self.flat_folders)
548580
file_end = len(self.flat_files)
@@ -593,9 +625,9 @@ def write_toc(self) -> TocBlock:
593625
)
594626

595627
def disassemble(self) -> TocBlock:
596-
for name, drive_fs in self.fs.iterate_fs():
628+
for _, drive_fs in self.fs.iterate_fs():
597629
drive_fs = typing.cast(_EssenceDriveFS, drive_fs)
598-
drive_def = self.disassemble_drive(drive_fs, name)
630+
drive_def = self.disassemble_drive(drive_fs)
599631
self.flat_drives.append(drive_def)
600632

601633
return self.write_toc()
@@ -740,6 +772,8 @@ def __init__(
740772
gen_empty_meta: Callable[[], TMetaBlock],
741773
finalize_meta: Callable[[BinaryIO, TMetaBlock], None],
742774
meta2def: Callable[[Dict[str, object]], TFileDef],
775+
assembler: Optional[Type[FSAssembler[TFileDef]]] = None,
776+
disassembler: Optional[Type[FSDisassembler[TFileDef]]] = None,
743777
):
744778
self.version = version
745779
self.meta_serializer = meta_serializer
@@ -752,6 +786,8 @@ def __init__(
752786
self.gen_empty_meta = gen_empty_meta
753787
self.finalize_meta = finalize_meta
754788
self.meta2def = meta2def
789+
self.assembler_type = assembler or FSAssembler
790+
self.disassembler_type = disassembler or FSDisassembler
755791

756792
def read(self, stream: BinaryIO) -> EssenceFS:
757793
# Magic & Version; skippable so that we can check for a valid file and read the version elsewhere
@@ -773,7 +809,7 @@ def read(self, stream: BinaryIO) -> EssenceFS:
773809
name, metadata = meta_block.name, self.assemble_meta(
774810
stream, meta_block, toc_meta_block
775811
)
776-
assembler: FSAssembler[TFileDef] = FSAssembler(
812+
assembler: FSAssembler[TFileDef] = self.assembler_type(
777813
stream=stream,
778814
ptrs=meta_block.ptrs,
779815
toc=toc_block,
@@ -806,7 +842,7 @@ def write(self, stream: BinaryIO, essence_fs: EssenceFS) -> int:
806842
with BytesIO() as data_stream:
807843
with BytesIO() as toc_stream:
808844
with BytesIO() as name_stream:
809-
disassembler = FSDisassembler(
845+
disassembler: FSDisassembler[TFileDef] = self.disassembler_type(
810846
fs=essence_fs,
811847
toc_stream=toc_stream,
812848
data_stream=data_stream,

tests/issues/test_issue_39.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def _generate_fake_osfs() -> FS:
4444

4545

4646
def _pack_fake_osfs(osfs: FS, name: str) -> EssenceFS:
47-
# Create 'SGA'
47+
# Create 'SGA' V2
4848
sga = EssenceFS()
4949
sga.setmeta(
5050
{
@@ -57,13 +57,14 @@ def _pack_fake_osfs(osfs: FS, name: str) -> EssenceFS:
5757
"essence",
5858
)
5959

60-
alias = "test"
60+
alias = "data"
61+
name = "test data"
6162
sga_drive = None # sga.create_drive(alias)
6263
for path in osfs.walk.files():
6364
if (
6465
sga_drive is None
6566
): # Lazily create drive, to avoid empty drives from being created
66-
sga_drive = sga.create_drive(alias)
67+
sga_drive = sga.create_drive(alias, name)
6768

6869
if "stream" in path:
6970
storage = StorageType.STREAM_COMPRESS

0 commit comments

Comments
 (0)