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
8 changes: 5 additions & 3 deletions docs/assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ nnnotes export -o OUT [--layout original[,cas]] [--select group:G|key:PREFIX|bun
nnnotes plan [the selection and parameters of export] [-o OUT] [--json] [--since RUN] [--check] [--census]
[--emit-tasks DIR] [--why TASK]
nnnotes run-stage TASK.json|DIR [...] [--store DIR] [--fetch] [--force]
nnnotes catalogs list [--json] | import FILE [--label L] [--apk-catalog FILE] | fetch [--label L]
nnnotes catalogs list [--json] | import FILE [--label L] [--apk-catalog FILE] [--resource-version V] | fetch [--label L]
| diff OLD NEW [--json]
nnnotes store verify [--quick]
```
Expand Down Expand Up @@ -109,8 +109,10 @@ The catalog versions of a store: `import` a catalog file (with the APK's catalog
them (labels, version ids or sha256 prefixes): keys added, removed and changed, bundles and raw files by stable name,
the keys that reach a changed file, and, when a run over each version has stored its address table, the objects by
stable address (an object keeps its stable address while its object id changes with its bundle's content). A version
is labelled by `--label`, else by the resource version (`fetch`, when the region has an API root), else by the
first 12 hex digits of its sha256. `export` imports the current catalog when it is not imported yet.
is labelled by `--label`, else by its resource version, else by the first 12 hex digits of its sha256. `fetch`
records the resource version it downloads (none for `main`); `import` takes `--resource-version`, else the JP
catalog's source version, else the version in a file name `catalog_<version>_<language>.bin` as `fetch` and the
cache name them (none for `main`). `export` imports the current catalog when it is not imported yet.

### store verify

Expand Down
34 changes: 30 additions & 4 deletions src/nnnotes/cli_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
nnnotes plan [the selection and parameters of export] [-o OUT] [--json] [--since RUN] [--check] [--census]
[--emit-tasks DIR] [--why TASK]
nnnotes run-stage TASK.json|DIR [...] [--store DIR] [--fetch] [--force]
nnnotes catalogs list [--json] | import FILE [--label L] | fetch [--label L] | diff A B [--json]
nnnotes catalogs list [--json] | import FILE [--label L] [--resource-version V] | fetch [--label L] | diff A B [--json]
nnnotes store verify [--quick]

The pipeline is the registry STAGE_SOURCES in order: each entry names the module and attribute of a stage (a Stage
Expand Down Expand Up @@ -47,6 +47,7 @@
import argparse
import fnmatch
import json
import re
import sys
import threading
import zipfile
Expand All @@ -60,7 +61,7 @@

from .apkset import ApkSet
from . import contract
from .config import ConfigError, describe as describe_setting, usable_cpus, use
from .config import ConfigError, check_catalog_version, describe as describe_setting, usable_cpus, use
from .contract import Cost, IncompatibleTask, Input, Task
from .stages import Env, Pending, Stage, describe, execute

Expand Down Expand Up @@ -1335,6 +1336,28 @@ def _apk_catalog(args, cfg) -> bytes | None:
return apk_catalog(apk)


CATALOG_FILE = re.compile(r"catalog_([A-Za-z0-9][A-Za-z0-9.-]*)_[A-Za-z0-9-]+\.bin")


def _import_resource_version(given: str | None, file: Path, source: dict | None) -> str | None:
"""The resource version of an imported catalog: `given` (--resource-version), else the JP source's version, else
the version in an international file name `catalog_<version>_<language>.bin` (none for `main`). A JP source is
authoritative: a different `given` is an error."""
if given is not None:
try:
check_catalog_version(given)
except ValueError as e:
raise ValueError(f"--resource-version {given!r}: {e}") from None
if source is not None:
if given is not None and given != source["version"]:
raise ValueError(f"--resource-version {given} differs from the catalog's source version {source['version']}")
return source["version"]
if given is not None:
return given
m = CATALOG_FILE.fullmatch(file.name)
return m[1] if m and m[1] != "main" and ".." not in m[1] else None


def cmd_catalogs_import(args, cfg, common):
try:
remote = Path(args.file).read_bytes()
Expand All @@ -1345,9 +1368,10 @@ def cmd_catalogs_import(args, cfg, common):
if cfg.provider() == "jp":
from .jp import read_source
source = read_source(args.file, remote).to_dict()
resource = _import_resource_version(args.resource_version, Path(args.file), source)
v = _db(args, cfg).add(remote, _apk_catalog(args, cfg), label=args.label,
region=cfg.get("catalog", "region"), language=cfg.get("catalog", "language"),
resource_version=args.resource_version,
resource_version=resource,
apk_version_name=_apk_version(cfg.path("paths", "apk")), source=source)
except ValueError as e:
args.usage(str(e))
Expand Down Expand Up @@ -1525,7 +1549,9 @@ def bind(fn):
m.add_argument("file", help="catalog_<version>_<language>.bin")
m.add_argument("--label", help="label of the version (default: its resource version, else the sha256 prefix)")
m.add_argument("--apk-catalog", help="the APK's catalog.bin (default: read from [paths] apk)")
m.add_argument("--resource-version", help="the game's resource version of this catalog")
m.add_argument("--resource-version",
help="the game's resource version of this catalog (default: the JP source's version, else the "
"version in the file name catalog_<version>_<language>.bin)")
_store_arg(m)
m.set_defaults(func=bind(cmd_catalogs_import), usage=m.error)
m = csub.add_parser("fetch", help="fetch the region's current catalog and import it")
Expand Down
24 changes: 24 additions & 0 deletions tests/test_cli_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,30 @@ def test_catalog_versions_import_list_and_diff(tmp_path, capsys):
assert nn(capsys, "catalogs", "diff", "v1", "v9", *s)[0] == 2


def test_catalog_import_takes_the_resource_version_from_the_file_name(tmp_path, capsys):
s = ["--store", tmp_path / "s"]
named = tmp_path / "catalog_1.0.0.201_en.bin"
named.write_bytes(catalog_bytes())
main = tmp_path / "catalog_main_en.bin"
main.write_bytes(synth.CatalogWriter().build([(NAMES["shared"], synth.remote(NAMES["shared"]), [])]))
assert nn(capsys, "catalogs", "import", named, *s)[0] == 0
assert nn(capsys, "catalogs", "import", main, *s)[0] == 0
vs = json.loads(nn(capsys, "catalogs", "list", "--json", *s)[1])["versions"]
assert [(v["labels"], v["resourceVersion"]) for v in vs] == [
(["1.0.0.201"], "1.0.0.201"), ([contract.sha256(main.read_bytes())[:12]], None)]


def test_catalog_import_resource_version_option():
file = Path("catalog_1.0.0.201_en.bin")
assert cli_assets._import_resource_version("1.0.0.300", file, None) == "1.0.0.300"
assert cli_assets._import_resource_version(None, Path("renamed.bin"), None) is None
assert cli_assets._import_resource_version(None, file, {"version": "1.0.0.300"}) == "1.0.0.300"
with pytest.raises(ValueError, match="differs from the catalog's source version"):
cli_assets._import_resource_version("1.0.0.201", file, {"version": "1.0.0.300"})
with pytest.raises(ValueError, match="--resource-version"):
cli_assets._import_resource_version("../x", file, None)


def test_store_verify(tmp_path, capsys, fake):
d = setup_data(tmp_path)
export(capsys, tmp_path, d, "s", "o")
Expand Down
Loading