@synopackageland/cli 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +160 -0
- package/README.md +1 -1
- package/dist/build-spk/gateway.d.ts +2 -2
- package/dist/build-spk/gateway.js +2 -2
- package/dist/build-spk/generator.d.ts +1 -0
- package/dist/build-spk/generator.js +2 -2
- package/dist/build-spk/ui-texts.d.ts +3 -0
- package/dist/build-spk/ui-texts.js +7 -3
- package/dist/cli.js +107 -43
- package/dist/contract/web-launch.js +1 -4
- package/dist/error-registry.js +6 -1
- package/dist/reproducible-config.d.ts +8 -0
- package/dist/reproducible-config.js +32 -0
- package/dist/reproducible.d.ts +38 -0
- package/dist/reproducible.js +121 -0
- package/dist/runtime/executor-build.d.ts +1 -1
- package/dist/runtime/executor-build.js +3 -3
- package/dist/spk/check-info.d.ts +5 -0
- package/dist/spk/check-info.js +123 -0
- package/dist/spk/check-metadata.d.ts +7 -0
- package/dist/spk/check-metadata.js +113 -0
- package/dist/spk/check-payload.d.ts +2 -0
- package/dist/spk/check-payload.js +156 -0
- package/dist/spk/dsm-spec.d.ts +42 -0
- package/dist/spk/dsm-spec.js +86 -0
- package/dist/spk/findings.d.ts +13 -0
- package/dist/spk/findings.js +8 -0
- package/dist/spk/inspect.d.ts +18 -0
- package/dist/spk/inspect.js +62 -0
- package/dist/spk/rules.d.ts +265 -0
- package/dist/spk/rules.js +53 -0
- package/dist/spk/validate-spk.d.ts +13 -0
- package/dist/spk/validate-spk.js +107 -0
- package/dist/standalone/arch-jobs.d.ts +11 -0
- package/dist/standalone/arch-jobs.js +54 -0
- package/dist/standalone/build.d.ts +11 -2
- package/dist/standalone/build.js +74 -31
- package/dist/standalone/daemon.d.ts +1 -1
- package/dist/standalone/daemon.js +2 -2
- package/dist/standalone/dsm-window.js +4 -5
- package/dist/standalone/inspect-spk.d.ts +1 -0
- package/dist/standalone/inspect-spk.js +1 -1
- package/dist/standalone/manifest.js +10 -1
- package/dist/standalone/native-layout.d.ts +3 -0
- package/dist/standalone/native-layout.js +94 -33
- package/package.json +1 -1
- package/schema/synology-app.schema.json +9 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import DSM_PLATFORMS from "../build-spk/dsm-platforms.json" with { type: "json" };
|
|
2
|
+
/**
|
|
3
|
+
* The parts of the Synology DSM 7 Developer Guide that `inspect` checks
|
|
4
|
+
* against, transcribed from the guide's INFO, Package Structure and
|
|
5
|
+
* Appendix A pages.
|
|
6
|
+
*/
|
|
7
|
+
/** INFO Necessary Fields. */
|
|
8
|
+
export const REQUIRED_INFO_FIELDS = ["package", "version", "os_min_ver", "description", "arch", "maintainer"];
|
|
9
|
+
/** INFO Optional Fields, without the `displayname_<lang>` and `description_<lang>` families. */
|
|
10
|
+
export const OPTIONAL_INFO_FIELDS = [
|
|
11
|
+
"displayname", "maintainer_url", "distributor", "distributor_url", "support_url", "support_center", "model",
|
|
12
|
+
"exclude_arch", "checksum", "adminport", "adminurl", "adminprotocol", "dsmuidir", "dsmappname", "dsmapppage",
|
|
13
|
+
"dsmapplaunchname", "checkport", "startable", "ctl_stop", "ctl_uninstall", "precheckstartstop", "helpurl", "beta",
|
|
14
|
+
"report_url", "install_reboot", "install_dep_packages", "install_conflict_packages", "install_break_packages",
|
|
15
|
+
"install_replace_packages", "install_dep_services", "start_dep_services", "extractsize", "support_conf_folder",
|
|
16
|
+
"install_type", "silent_install", "silent_upgrade", "silent_uninstall", "auto_upgrade_from", "offline_install",
|
|
17
|
+
"thirdparty", "os_max_ver", "support_move", "exclude_model", "use_deprecated_replace_mechanism",
|
|
18
|
+
"install_on_cold_storage",
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Fields outside the guide's list that are still expected: `changelog` is
|
|
22
|
+
* shown by Package Center and used by first-party packages; the others are
|
|
23
|
+
* written by this SDK and ignored by DSM.
|
|
24
|
+
*/
|
|
25
|
+
export const EXTRA_INFO_FIELDS = ["changelog", "synopkgland_executor_sha256", "spk_land_metadata"];
|
|
26
|
+
/** Optional fields whose value is "yes" or "no". */
|
|
27
|
+
export const BOOLEAN_INFO_FIELDS = [
|
|
28
|
+
"support_center", "checkport", "startable", "ctl_stop", "ctl_uninstall", "precheckstartstop", "beta",
|
|
29
|
+
"install_reboot", "support_conf_folder", "silent_install", "silent_upgrade", "silent_uninstall", "offline_install",
|
|
30
|
+
"thirdparty", "support_move", "use_deprecated_replace_mechanism", "install_on_cold_storage",
|
|
31
|
+
];
|
|
32
|
+
/** Language codes of `displayname_<lang>` and `description_<lang>`. */
|
|
33
|
+
export const INFO_LANGUAGES = [
|
|
34
|
+
"enu", "cht", "chs", "krn", "ger", "fre", "ita", "spn", "jpn", "dan", "nor", "sve", "nld", "rus", "plk", "ptb",
|
|
35
|
+
"ptg", "hun", "trk", "csy",
|
|
36
|
+
];
|
|
37
|
+
/** After DSM 7.0, os_min_ver "should be at least 7.0-40000". */
|
|
38
|
+
export const DSM7_MIN_VER = "7.0-40000";
|
|
39
|
+
/**
|
|
40
|
+
* Appendix A arch families and platforms, with the ELF e_machine their
|
|
41
|
+
* binaries use, plus DSM 7 platforms newer than the appendix (from
|
|
42
|
+
* dsm-platforms.json, shared with package-source).
|
|
43
|
+
*/
|
|
44
|
+
const EM_386 = 3, EM_ARM = 40, EM_X86_64 = 62, EM_AARCH64 = 183;
|
|
45
|
+
const APPENDIX_ARCHES = {
|
|
46
|
+
noarch: undefined, x86_64: EM_X86_64, i686: EM_386, armv7: EM_ARM, armv5: EM_ARM, armv8: EM_AARCH64,
|
|
47
|
+
evansport: EM_386, "628x": EM_ARM,
|
|
48
|
+
alpine: EM_ARM, alpine4k: EM_ARM, armada370: EM_ARM, armada375: EM_ARM, armada38x: EM_ARM, armadaxp: EM_ARM,
|
|
49
|
+
comcerto2k: EM_ARM, monaco: EM_ARM,
|
|
50
|
+
armada37xx: EM_AARCH64, rtd1296: EM_AARCH64, rtd1619: EM_AARCH64, rtd1619b: EM_AARCH64,
|
|
51
|
+
apollolake: EM_X86_64, avoton: EM_X86_64, braswell: EM_X86_64, broadwell: EM_X86_64, broadwellnk: EM_X86_64,
|
|
52
|
+
broadwellntb: EM_X86_64, broadwellntbap: EM_X86_64, bromolow: EM_X86_64, cedarview: EM_X86_64,
|
|
53
|
+
coffeelake: EM_X86_64, denverton: EM_X86_64, geminilake: EM_X86_64, grantley: EM_X86_64, kvmx64: EM_X86_64,
|
|
54
|
+
purley: EM_X86_64, skylaked: EM_X86_64, v1000: EM_X86_64,
|
|
55
|
+
};
|
|
56
|
+
const FAMILY_MACHINE = { x86_64: EM_X86_64, aarch64: EM_AARCH64, armv7: EM_ARM };
|
|
57
|
+
export const ARCH_ELF_MACHINE = new Map([
|
|
58
|
+
...Object.entries(APPENDIX_ARCHES),
|
|
59
|
+
...Object.entries(DSM_PLATFORMS).flatMap(([family, { members, platformOnly }]) => [...members, ...platformOnly].map((platform) => [platform, FAMILY_MACHINE[family]])),
|
|
60
|
+
]);
|
|
61
|
+
/**
|
|
62
|
+
* Top-level SPK entries of the Package Structure page, plus metadata this SDK
|
|
63
|
+
* adds; container SPKs also carry a top-level copy of conf/privilege.
|
|
64
|
+
*/
|
|
65
|
+
export const KNOWN_TOP_LEVEL = [
|
|
66
|
+
"INFO", "package.tgz", "scripts", "conf", "WIZARD_UIFILES", "LICENSE", "PACKAGE_ICON.PNG", "PACKAGE_ICON_256.PNG",
|
|
67
|
+
"syno_signature.asc", "sdk-package.json", "native-layout.json", "privilege",
|
|
68
|
+
];
|
|
69
|
+
/** scripts/ of the Package Structure page; DSM needs start-stop-status to start, stop and poll a package. */
|
|
70
|
+
export const LIFECYCLE_SCRIPTS = ["preinst", "postinst", "preuninst", "postuninst", "preupgrade", "postupgrade", "start-stop-status"];
|
|
71
|
+
/** Optional scripts, run only with install_replace_packages. */
|
|
72
|
+
export const REPLACE_SCRIPTS = ["prereplace", "postreplace"];
|
|
73
|
+
/** conf/ entries: the conf page's files, and the systemd unit folder of the systemd-user-unit worker. */
|
|
74
|
+
export const CONF_ENTRIES = ["privilege", "resource", "PKG_DEPS", "PKG_CONX", "systemd"];
|
|
75
|
+
/**
|
|
76
|
+
* Resource ids of the guide's Available Workers pages; `systemd-unit` is not
|
|
77
|
+
* documented there but is used by packages that install on DSM 7.
|
|
78
|
+
*/
|
|
79
|
+
export const RESOURCE_WORKERS = [
|
|
80
|
+
"port-config", "usr-local-linker", "data-share", "systemd-user-unit", "systemd-unit", "web-config", "webservice",
|
|
81
|
+
"docker-project", "docker", "apache22", "indexdb", "mariadb10", "php", "syslog-config", "sysnotify",
|
|
82
|
+
];
|
|
83
|
+
/** PACKAGE_ICON.PNG is 64×64 on DSM 7; PACKAGE_ICON_256.PNG is 256×256. */
|
|
84
|
+
export const PACKAGE_ICONS = [["PACKAGE_ICON.PNG", 64], ["PACKAGE_ICON_256.PNG", 256]];
|
|
85
|
+
/** LICENSE "must be less than 1 MB". */
|
|
86
|
+
export const MAX_LICENSE_BYTES = 1024 * 1024;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type SpkRuleCode, type SpkRuleSeverity } from "./rules.js";
|
|
2
|
+
export interface SpkFinding {
|
|
3
|
+
code: SpkRuleCode;
|
|
4
|
+
severity: SpkRuleSeverity;
|
|
5
|
+
/** The SPK entry the finding is about, when there is one. */
|
|
6
|
+
path?: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
/** Collects findings; each code's severity comes from its rule. */
|
|
10
|
+
export declare class Findings {
|
|
11
|
+
readonly list: SpkFinding[];
|
|
12
|
+
add(code: SpkRuleCode, message: string, path?: string): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SPK_RULES } from "./rules.js";
|
|
2
|
+
/** Collects findings; each code's severity comes from its rule. */
|
|
3
|
+
export class Findings {
|
|
4
|
+
list = [];
|
|
5
|
+
add(code, message, path) {
|
|
6
|
+
this.list.push({ code, severity: SPK_RULES[code].severity, ...(path ? { path } : {}), message });
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type SpkInspection } from "../standalone/inspect-spk.js";
|
|
2
|
+
import { type SpkFinding } from "./validate-spk.js";
|
|
3
|
+
export interface SpkReport extends SpkInspection {
|
|
4
|
+
/** Built by synopkgland, so its build contract (`findings`) was checked too. */
|
|
5
|
+
synopkgland: boolean;
|
|
6
|
+
/** The DSM 7 package format rules, checked on every SPK. */
|
|
7
|
+
dsm: {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
errors: SpkFinding[];
|
|
10
|
+
warnings: SpkFinding[];
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Checks any SPK against the DSM 7 package format. An SPK that carries a
|
|
15
|
+
* synopkgland build marker is also checked against the build contract;
|
|
16
|
+
* others are never held to it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function inspectAnySpk(spkPath: string): Promise<SpkReport>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createWriteStream, mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { finished } from "node:stream/promises";
|
|
5
|
+
import * as tar from "tar";
|
|
6
|
+
import { inspectSpk, listPayload } from "../standalone/inspect-spk.js";
|
|
7
|
+
import { validateSpk } from "./validate-spk.js";
|
|
8
|
+
/** Top-level files the native-go/binary and native layout builds write; no DSM file has these names. */
|
|
9
|
+
const BUILD_MARKERS = ["sdk-package.json", "native-layout.json"];
|
|
10
|
+
/** A container build ships its manifest in the payload. */
|
|
11
|
+
const CONTAINER_MARKER = "synology-app.yaml";
|
|
12
|
+
/**
|
|
13
|
+
* Checks any SPK against the DSM 7 package format. An SPK that carries a
|
|
14
|
+
* synopkgland build marker is also checked against the build contract;
|
|
15
|
+
* others are never held to it.
|
|
16
|
+
*/
|
|
17
|
+
export async function inspectAnySpk(spkPath) {
|
|
18
|
+
const validation = await validateSpk(spkPath);
|
|
19
|
+
const dsm = { ok: validation.ok, errors: validation.errors, warnings: validation.warnings };
|
|
20
|
+
const facts = await readFacts(spkPath);
|
|
21
|
+
if (BUILD_MARKERS.some((name) => facts.top.has(name)) || facts.payload.some((entry) => entry.path === CONTAINER_MARKER)) {
|
|
22
|
+
return { ...(await inspectSpk(spkPath)), synopkgland: true, dsm };
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
...(validation.package ? { package: validation.package } : {}),
|
|
26
|
+
...(validation.version ? { version: validation.version } : {}),
|
|
27
|
+
...(validation.arch ? { arch: validation.arch } : {}),
|
|
28
|
+
signed: facts.top.has("syno_signature.asc"),
|
|
29
|
+
payload: facts.payload,
|
|
30
|
+
findings: [],
|
|
31
|
+
capabilities: [],
|
|
32
|
+
synopkgland: false,
|
|
33
|
+
dsm,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Top-level names and the payload listing; an unreadable archive yields none, which the DSM rules already report. */
|
|
37
|
+
async function readFacts(spkPath) {
|
|
38
|
+
const facts = { top: new Set(), payload: [] };
|
|
39
|
+
const dir = mkdtempSync(join(tmpdir(), "syno-inspect-facts-"));
|
|
40
|
+
try {
|
|
41
|
+
const writes = [];
|
|
42
|
+
await tar.t({
|
|
43
|
+
file: spkPath,
|
|
44
|
+
onReadEntry: (entry) => {
|
|
45
|
+
const path = entry.path.replace(/^\.\//, "");
|
|
46
|
+
facts.top.add(path.split("/")[0]);
|
|
47
|
+
if (path === "package.tgz" && entry.type === "File")
|
|
48
|
+
writes.push(finished(entry.pipe(createWriteStream(join(dir, "package.tgz")))));
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
await Promise.all(writes);
|
|
52
|
+
if (writes.length)
|
|
53
|
+
facts.payload = await listPayload(join(dir, "package.tgz"), []);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Not a readable tar or tar.gz.
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
rmSync(dir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
return facts;
|
|
62
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules `synopkgland inspect` checks on any SPK, whoever built it. Each code
|
|
3
|
+
* is stable and keeps its severity: an error is something the Synology DSM 7
|
|
4
|
+
* Developer Guide requires; a warning is a likely mistake, or a rule the guide
|
|
5
|
+
* does not state clearly. `source` names the guide page the rule comes from.
|
|
6
|
+
*/
|
|
7
|
+
export type SpkRuleSeverity = "error" | "warning";
|
|
8
|
+
export interface SpkRule {
|
|
9
|
+
severity: SpkRuleSeverity;
|
|
10
|
+
description: string;
|
|
11
|
+
source: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const SPK_RULES: {
|
|
14
|
+
readonly SPK_NOT_TAR: {
|
|
15
|
+
readonly severity: "error";
|
|
16
|
+
readonly description: "檔案不是可讀的 tar 封存;SPK 是未壓縮的 tar。";
|
|
17
|
+
readonly source: "DSM 7 Developer Guide: Package Structure";
|
|
18
|
+
};
|
|
19
|
+
readonly SPK_ENTRY_UNSAFE: {
|
|
20
|
+
readonly severity: "error";
|
|
21
|
+
readonly description: "SPK 內的路徑跳出封存根目錄(絕對路徑或 ..),或是連結。";
|
|
22
|
+
readonly source: "DSM 7 Developer Guide: Package Structure";
|
|
23
|
+
};
|
|
24
|
+
readonly SPK_ENTRY_UNKNOWN: {
|
|
25
|
+
readonly severity: "warning";
|
|
26
|
+
readonly description: "SPK 最上層有指南沒有列出的檔案或目錄(檔名區分大小寫)。";
|
|
27
|
+
readonly source: "DSM 7 Developer Guide: Package Structure";
|
|
28
|
+
};
|
|
29
|
+
readonly SPK_INFO_MISSING: {
|
|
30
|
+
readonly severity: "error";
|
|
31
|
+
readonly description: "缺少 INFO。";
|
|
32
|
+
readonly source: "DSM 7 Developer Guide: Package Structure";
|
|
33
|
+
};
|
|
34
|
+
readonly SPK_PAYLOAD_MISSING: {
|
|
35
|
+
readonly severity: "error";
|
|
36
|
+
readonly description: "缺少 package.tgz。";
|
|
37
|
+
readonly source: "DSM 7 Developer Guide: Package Structure";
|
|
38
|
+
};
|
|
39
|
+
readonly SPK_INFO_FIELD_REQUIRED: {
|
|
40
|
+
readonly severity: "error";
|
|
41
|
+
readonly description: "INFO 缺少必填欄位(package、version、os_min_ver、description、arch、maintainer)。";
|
|
42
|
+
readonly source: "DSM 7 Developer Guide: INFO Necessary Fields";
|
|
43
|
+
};
|
|
44
|
+
readonly SPK_INFO_DISPLAYNAME_MISSING: {
|
|
45
|
+
readonly severity: "warning";
|
|
46
|
+
readonly description: "INFO 沒有 displayname;Package Center 會改顯示 package 值。";
|
|
47
|
+
readonly source: "DSM 7 Developer Guide: INFO Optional Fields";
|
|
48
|
+
};
|
|
49
|
+
readonly SPK_INFO_LINE_INVALID: {
|
|
50
|
+
readonly severity: "warning";
|
|
51
|
+
readonly description: "INFO 有一行不是 key=\"value\"(空行與 # 註解除外)。";
|
|
52
|
+
readonly source: "DSM 7 Developer Guide: INFO";
|
|
53
|
+
};
|
|
54
|
+
readonly SPK_INFO_VALUE_UNTERMINATED: {
|
|
55
|
+
readonly severity: "error";
|
|
56
|
+
readonly description: "INFO 欄位值的引號沒有結束。";
|
|
57
|
+
readonly source: "DSM 7 Developer Guide: INFO";
|
|
58
|
+
};
|
|
59
|
+
readonly SPK_INFO_FIELD_DUPLICATE: {
|
|
60
|
+
readonly severity: "warning";
|
|
61
|
+
readonly description: "INFO 同一個欄位設定了兩次。";
|
|
62
|
+
readonly source: "DSM 7 Developer Guide: INFO";
|
|
63
|
+
};
|
|
64
|
+
readonly SPK_INFO_PACKAGE_INVALID: {
|
|
65
|
+
readonly severity: "error";
|
|
66
|
+
readonly description: "INFO package 含有 : / > < | = 其中之一。";
|
|
67
|
+
readonly source: "DSM 7 Developer Guide: INFO Necessary Fields (package)";
|
|
68
|
+
};
|
|
69
|
+
readonly SPK_INFO_VERSION_INVALID: {
|
|
70
|
+
readonly severity: "error";
|
|
71
|
+
readonly description: "INFO version 不是以 .、-、_ 分隔的數字,或某段超過 2147483647。";
|
|
72
|
+
readonly source: "DSM 7 Developer Guide: INFO Necessary Fields (version)";
|
|
73
|
+
};
|
|
74
|
+
readonly SPK_INFO_VERSION_NO_BUILD: {
|
|
75
|
+
readonly severity: "warning";
|
|
76
|
+
readonly description: "INFO version 沒有 -<build> 編號(指南格式為 [feature number]-[build number])。";
|
|
77
|
+
readonly source: "DSM 7 Developer Guide: INFO Necessary Fields (version)";
|
|
78
|
+
};
|
|
79
|
+
readonly SPK_INFO_OS_VER_INVALID: {
|
|
80
|
+
readonly severity: "error";
|
|
81
|
+
readonly description: "INFO os_min_ver 或 os_max_ver 不是 <major>.<minor>-<build>(也接受 <major>.<minor>.<micro>-<build>)。";
|
|
82
|
+
readonly source: "DSM 7 Developer Guide: INFO Necessary Fields (os_min_ver), Optional Fields (os_max_ver)";
|
|
83
|
+
};
|
|
84
|
+
readonly SPK_INFO_OS_MIN_VER_LOW: {
|
|
85
|
+
readonly severity: "warning";
|
|
86
|
+
readonly description: "INFO os_min_ver 低於 7.0-40000。";
|
|
87
|
+
readonly source: "DSM 7 Developer Guide: INFO Necessary Fields (os_min_ver)";
|
|
88
|
+
};
|
|
89
|
+
readonly SPK_INFO_ARCH_UNKNOWN: {
|
|
90
|
+
readonly severity: "warning";
|
|
91
|
+
readonly description: "INFO arch 或 exclude_arch 含有 Appendix A 與已知 DSM 7 平台都沒有的值。";
|
|
92
|
+
readonly source: "DSM 7 Developer Guide: Appendix A Platform and Arch Value Mapping Table";
|
|
93
|
+
};
|
|
94
|
+
readonly SPK_INFO_ARCH_NOARCH_MIXED: {
|
|
95
|
+
readonly severity: "warning";
|
|
96
|
+
readonly description: "INFO arch 同時有 noarch 與其他值。";
|
|
97
|
+
readonly source: "DSM 7 Developer Guide: INFO Necessary Fields (arch)";
|
|
98
|
+
};
|
|
99
|
+
readonly SPK_INFO_BOOLEAN_INVALID: {
|
|
100
|
+
readonly severity: "error";
|
|
101
|
+
readonly description: "INFO 的 yes/no 欄位(例如 startable、silent_install)不是 \"yes\" 或 \"no\"。";
|
|
102
|
+
readonly source: "DSM 7 Developer Guide: INFO Optional Fields";
|
|
103
|
+
};
|
|
104
|
+
readonly SPK_INFO_LANG_UNKNOWN: {
|
|
105
|
+
readonly severity: "warning";
|
|
106
|
+
readonly description: "displayname_<lang> 或 description_<lang> 的語言碼不在指南清單(enu、cht、chs、jpn…)。";
|
|
107
|
+
readonly source: "DSM 7 Developer Guide: INFO Optional Fields (description_[language])";
|
|
108
|
+
};
|
|
109
|
+
readonly SPK_INFO_FIELD_UNKNOWN: {
|
|
110
|
+
readonly severity: "warning";
|
|
111
|
+
readonly description: "INFO 欄位不在指南清單;DSM 不會使用它(拼錯的欄位名稱也會出現在這裡)。";
|
|
112
|
+
readonly source: "DSM 7 Developer Guide: INFO Necessary Fields, Optional Fields";
|
|
113
|
+
};
|
|
114
|
+
readonly SPK_INFO_ADMINPORT_INVALID: {
|
|
115
|
+
readonly severity: "error";
|
|
116
|
+
readonly description: "INFO adminport 不是 0–65536 的整數。";
|
|
117
|
+
readonly source: "DSM 7 Developer Guide: INFO Optional Fields (adminport)";
|
|
118
|
+
};
|
|
119
|
+
readonly SPK_INFO_CHECKSUM_MISMATCH: {
|
|
120
|
+
readonly severity: "error";
|
|
121
|
+
readonly description: "INFO checksum 不是 package.tgz 的 MD5。";
|
|
122
|
+
readonly source: "DSM 7 Developer Guide: INFO Optional Fields (checksum)";
|
|
123
|
+
};
|
|
124
|
+
readonly SPK_PAYLOAD_UNREADABLE: {
|
|
125
|
+
readonly severity: "error";
|
|
126
|
+
readonly description: "package.tgz 不是可讀的 tar 或 tar.gz 封存,或是空的。";
|
|
127
|
+
readonly source: "DSM 7 Developer Guide: Package Structure (package.tgz)";
|
|
128
|
+
};
|
|
129
|
+
readonly SPK_PAYLOAD_XZ: {
|
|
130
|
+
readonly severity: "warning";
|
|
131
|
+
readonly description: "package.tgz 是 xz 壓縮(Synology toolkit 的格式);inspect 讀不了 xz,沒有檢查 payload 內容。";
|
|
132
|
+
readonly source: "DSM 7 Developer Guide: Pack Stage";
|
|
133
|
+
};
|
|
134
|
+
readonly SPK_PAYLOAD_PATH_UNSAFE: {
|
|
135
|
+
readonly severity: "error";
|
|
136
|
+
readonly description: "package.tgz 的路徑是絕對路徑或含 ..,會解開到套件目錄之外。";
|
|
137
|
+
readonly source: "DSM 7 Developer Guide: Package Structure (package.tgz)";
|
|
138
|
+
};
|
|
139
|
+
readonly SPK_PAYLOAD_LINK_ESCAPE: {
|
|
140
|
+
readonly severity: "error";
|
|
141
|
+
readonly description: "package.tgz 的 symlink 或 hard link 指到套件目錄之外。";
|
|
142
|
+
readonly source: "DSM 7 Developer Guide: Package Structure (package.tgz)";
|
|
143
|
+
};
|
|
144
|
+
readonly SPK_PAYLOAD_LINK_ABSOLUTE: {
|
|
145
|
+
readonly severity: "warning";
|
|
146
|
+
readonly description: "package.tgz 的 symlink 指向絕對路徑;目標由 NAS 決定,不在套件內。";
|
|
147
|
+
readonly source: "DSM 7 Developer Guide: Package Structure (package.tgz)";
|
|
148
|
+
};
|
|
149
|
+
readonly SPK_UI_CONFIG_INVALID: {
|
|
150
|
+
readonly severity: "error";
|
|
151
|
+
readonly description: "dsmuidir 底下的 config 不是 JSON 物件。";
|
|
152
|
+
readonly source: "DSM 7 Developer Guide: Integrate DSM, Application Config";
|
|
153
|
+
};
|
|
154
|
+
readonly SPK_UI_TEXTS_MISSING: {
|
|
155
|
+
readonly severity: "error";
|
|
156
|
+
readonly description: "config 的 title/desc 參照套件自己的字串(例如 synopkgland:app_title),但該 entry 沒有宣告 \"texts\";DSM 會直接顯示 key。";
|
|
157
|
+
readonly source: "DSM 7.1.1/7.4 實測(DSM 只為宣告 texts 的 entry 載入 texts/<lang>/strings)";
|
|
158
|
+
};
|
|
159
|
+
readonly SPK_UI_TEXTS_UNDECLARED: {
|
|
160
|
+
readonly severity: "warning";
|
|
161
|
+
readonly description: "config 的 title/desc 是 section:key 字串參照,但沒有宣告 \"texts\";除非是 DSM 內建 section,否則 DSM 會直接顯示 key。";
|
|
162
|
+
readonly source: "DSM 7.1.1/7.4 實測";
|
|
163
|
+
};
|
|
164
|
+
readonly SPK_NOARCH_ELF: {
|
|
165
|
+
readonly severity: "error";
|
|
166
|
+
readonly description: "INFO arch 是 noarch,但 package.tgz 有 ELF 執行檔。";
|
|
167
|
+
readonly source: "DSM 7 Developer Guide: INFO Necessary Fields (arch)、Pack Stage";
|
|
168
|
+
};
|
|
169
|
+
readonly SPK_ELF_ARCH_MISMATCH: {
|
|
170
|
+
readonly severity: "error";
|
|
171
|
+
readonly description: "package.tgz 的 ELF 執行檔與 INFO arch 的 CPU 不符。";
|
|
172
|
+
readonly source: "DSM 7 Developer Guide: INFO Necessary Fields (arch)、Appendix A";
|
|
173
|
+
};
|
|
174
|
+
readonly SPK_SCRIPT_MISSING: {
|
|
175
|
+
readonly severity: "error";
|
|
176
|
+
readonly description: "缺少 scripts/start-stop-status;DSM 用它啟動、停止與查詢套件狀態。";
|
|
177
|
+
readonly source: "DSM 7 Developer Guide: Package Structure、scripts";
|
|
178
|
+
};
|
|
179
|
+
readonly SPK_SCRIPT_HOOK_MISSING: {
|
|
180
|
+
readonly severity: "warning";
|
|
181
|
+
readonly description: "缺少 preinst、postinst、preuninst、postuninst、preupgrade 或 postupgrade;指南列為必備,DSM 缺檔時的行為未驗證。";
|
|
182
|
+
readonly source: "DSM 7 Developer Guide: scripts";
|
|
183
|
+
};
|
|
184
|
+
readonly SPK_SCRIPT_NOT_EXECUTABLE: {
|
|
185
|
+
readonly severity: "error";
|
|
186
|
+
readonly description: "scripts/ 的生命週期腳本沒有執行權限。";
|
|
187
|
+
readonly source: "DSM 7 Developer Guide: scripts";
|
|
188
|
+
};
|
|
189
|
+
readonly SPK_SCRIPT_NO_INTERPRETER: {
|
|
190
|
+
readonly severity: "warning";
|
|
191
|
+
readonly description: "生命週期腳本開頭沒有 #!(例如 #!/bin/sh),也不是 ELF。";
|
|
192
|
+
readonly source: "DSM 7 Developer Guide: scripts";
|
|
193
|
+
};
|
|
194
|
+
readonly SPK_PRIVILEGE_MISSING: {
|
|
195
|
+
readonly severity: "error";
|
|
196
|
+
readonly description: "缺少 conf/privilege;DSM 7 起必須提供。";
|
|
197
|
+
readonly source: "DSM 7 Developer Guide: conf、Privilege Config";
|
|
198
|
+
};
|
|
199
|
+
readonly SPK_PRIVILEGE_INVALID: {
|
|
200
|
+
readonly severity: "error";
|
|
201
|
+
readonly description: "conf/privilege 不是 JSON 物件、沒有 defaults.run-as,或 run-as 不是 package/root。";
|
|
202
|
+
readonly source: "DSM 7 Developer Guide: Privilege Config";
|
|
203
|
+
};
|
|
204
|
+
readonly SPK_PRIVILEGE_ROOT: {
|
|
205
|
+
readonly severity: "warning";
|
|
206
|
+
readonly description: "conf/privilege 要求以 root 執行(defaults、ctrl-script 或 executable)。";
|
|
207
|
+
readonly source: "DSM 7 Developer Guide: Privilege Config";
|
|
208
|
+
};
|
|
209
|
+
readonly SPK_RESOURCE_INVALID: {
|
|
210
|
+
readonly severity: "error";
|
|
211
|
+
readonly description: "conf/resource 不是 JSON 物件。";
|
|
212
|
+
readonly source: "DSM 7 Developer Guide: Resource Config";
|
|
213
|
+
};
|
|
214
|
+
readonly SPK_RESOURCE_UNKNOWN: {
|
|
215
|
+
readonly severity: "warning";
|
|
216
|
+
readonly description: "conf/resource 有指南沒有列出的 resource worker id。";
|
|
217
|
+
readonly source: "DSM 7 Developer Guide: Available Workers";
|
|
218
|
+
};
|
|
219
|
+
readonly SPK_CONF_UNKNOWN: {
|
|
220
|
+
readonly severity: "warning";
|
|
221
|
+
readonly description: "conf/ 有 DSM 不讀取的檔案(已知:privilege、resource、PKG_DEPS、PKG_CONX、systemd/)。";
|
|
222
|
+
readonly source: "DSM 7 Developer Guide: conf、Systemd User Unit";
|
|
223
|
+
};
|
|
224
|
+
readonly SPK_ICON_MISSING: {
|
|
225
|
+
readonly severity: "error";
|
|
226
|
+
readonly description: "缺少 PACKAGE_ICON.PNG 或 PACKAGE_ICON_256.PNG。";
|
|
227
|
+
readonly source: "DSM 7 Developer Guide: Package Structure";
|
|
228
|
+
};
|
|
229
|
+
readonly SPK_ICON_INVALID: {
|
|
230
|
+
readonly severity: "error";
|
|
231
|
+
readonly description: "PACKAGE_ICON.PNG 或 PACKAGE_ICON_256.PNG 不是 PNG。";
|
|
232
|
+
readonly source: "DSM 7 Developer Guide: Package Structure";
|
|
233
|
+
};
|
|
234
|
+
readonly SPK_ICON_SIZE: {
|
|
235
|
+
readonly severity: "warning";
|
|
236
|
+
readonly description: "PACKAGE_ICON.PNG 不是 64×64,或 PACKAGE_ICON_256.PNG 不是 256×256。";
|
|
237
|
+
readonly source: "DSM 7 Developer Guide: Package Structure";
|
|
238
|
+
};
|
|
239
|
+
readonly SPK_LICENSE_TOO_LARGE: {
|
|
240
|
+
readonly severity: "error";
|
|
241
|
+
readonly description: "LICENSE 大於或等於 1 MB。";
|
|
242
|
+
readonly source: "DSM 7 Developer Guide: Package Structure";
|
|
243
|
+
};
|
|
244
|
+
readonly SPK_WIZARD_INVALID: {
|
|
245
|
+
readonly severity: "warning";
|
|
246
|
+
readonly description: "WIZARD_UIFILES 的精靈檔不是 JSON 陣列。";
|
|
247
|
+
readonly source: "DSM 7 Developer Guide: Package Structure (WIZARD_UIFILES)";
|
|
248
|
+
};
|
|
249
|
+
readonly SPK_WIZARD_SCRIPT_NOT_EXECUTABLE: {
|
|
250
|
+
readonly severity: "warning";
|
|
251
|
+
readonly description: "WIZARD_UIFILES 的 .sh 精靈產生器沒有執行權限。";
|
|
252
|
+
readonly source: "DSM 7 Developer Guide: Package Structure (WIZARD_UIFILES)";
|
|
253
|
+
};
|
|
254
|
+
readonly SPK_WIZARD_UNKNOWN: {
|
|
255
|
+
readonly severity: "warning";
|
|
256
|
+
readonly description: "WIZARD_UIFILES 的檔名不是 install/upgrade/uninstall_uifile[_<lang>][.sh]。";
|
|
257
|
+
readonly source: "DSM 7 Developer Guide: Package Structure (WIZARD_UIFILES)";
|
|
258
|
+
};
|
|
259
|
+
readonly SPK_SIGNATURE_INVALID: {
|
|
260
|
+
readonly severity: "warning";
|
|
261
|
+
readonly description: "syno_signature.asc 不是 ASCII-armored PGP 簽章(指南的套件簽章只適用 DSM 6.x,可省略此檔)。";
|
|
262
|
+
readonly source: "DSM 7 Developer Guide: Sign Package (only for DSM6.X)";
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
export type SpkRuleCode = keyof typeof SPK_RULES;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const GUIDE = "DSM 7 Developer Guide";
|
|
2
|
+
export const SPK_RULES = {
|
|
3
|
+
SPK_NOT_TAR: { severity: "error", description: "檔案不是可讀的 tar 封存;SPK 是未壓縮的 tar。", source: `${GUIDE}: Package Structure` },
|
|
4
|
+
SPK_ENTRY_UNSAFE: { severity: "error", description: "SPK 內的路徑跳出封存根目錄(絕對路徑或 ..),或是連結。", source: `${GUIDE}: Package Structure` },
|
|
5
|
+
SPK_ENTRY_UNKNOWN: { severity: "warning", description: "SPK 最上層有指南沒有列出的檔案或目錄(檔名區分大小寫)。", source: `${GUIDE}: Package Structure` },
|
|
6
|
+
SPK_INFO_MISSING: { severity: "error", description: "缺少 INFO。", source: `${GUIDE}: Package Structure` },
|
|
7
|
+
SPK_PAYLOAD_MISSING: { severity: "error", description: "缺少 package.tgz。", source: `${GUIDE}: Package Structure` },
|
|
8
|
+
SPK_INFO_FIELD_REQUIRED: { severity: "error", description: "INFO 缺少必填欄位(package、version、os_min_ver、description、arch、maintainer)。", source: `${GUIDE}: INFO Necessary Fields` },
|
|
9
|
+
SPK_INFO_DISPLAYNAME_MISSING: { severity: "warning", description: "INFO 沒有 displayname;Package Center 會改顯示 package 值。", source: `${GUIDE}: INFO Optional Fields` },
|
|
10
|
+
SPK_INFO_LINE_INVALID: { severity: "warning", description: "INFO 有一行不是 key=\"value\"(空行與 # 註解除外)。", source: `${GUIDE}: INFO` },
|
|
11
|
+
SPK_INFO_VALUE_UNTERMINATED: { severity: "error", description: "INFO 欄位值的引號沒有結束。", source: `${GUIDE}: INFO` },
|
|
12
|
+
SPK_INFO_FIELD_DUPLICATE: { severity: "warning", description: "INFO 同一個欄位設定了兩次。", source: `${GUIDE}: INFO` },
|
|
13
|
+
SPK_INFO_PACKAGE_INVALID: { severity: "error", description: "INFO package 含有 : / > < | = 其中之一。", source: `${GUIDE}: INFO Necessary Fields (package)` },
|
|
14
|
+
SPK_INFO_VERSION_INVALID: { severity: "error", description: "INFO version 不是以 .、-、_ 分隔的數字,或某段超過 2147483647。", source: `${GUIDE}: INFO Necessary Fields (version)` },
|
|
15
|
+
SPK_INFO_VERSION_NO_BUILD: { severity: "warning", description: "INFO version 沒有 -<build> 編號(指南格式為 [feature number]-[build number])。", source: `${GUIDE}: INFO Necessary Fields (version)` },
|
|
16
|
+
SPK_INFO_OS_VER_INVALID: { severity: "error", description: "INFO os_min_ver 或 os_max_ver 不是 <major>.<minor>-<build>(也接受 <major>.<minor>.<micro>-<build>)。", source: `${GUIDE}: INFO Necessary Fields (os_min_ver), Optional Fields (os_max_ver)` },
|
|
17
|
+
SPK_INFO_OS_MIN_VER_LOW: { severity: "warning", description: "INFO os_min_ver 低於 7.0-40000。", source: `${GUIDE}: INFO Necessary Fields (os_min_ver)` },
|
|
18
|
+
SPK_INFO_ARCH_UNKNOWN: { severity: "warning", description: "INFO arch 或 exclude_arch 含有 Appendix A 與已知 DSM 7 平台都沒有的值。", source: `${GUIDE}: Appendix A Platform and Arch Value Mapping Table` },
|
|
19
|
+
SPK_INFO_ARCH_NOARCH_MIXED: { severity: "warning", description: "INFO arch 同時有 noarch 與其他值。", source: `${GUIDE}: INFO Necessary Fields (arch)` },
|
|
20
|
+
SPK_INFO_BOOLEAN_INVALID: { severity: "error", description: "INFO 的 yes/no 欄位(例如 startable、silent_install)不是 \"yes\" 或 \"no\"。", source: `${GUIDE}: INFO Optional Fields` },
|
|
21
|
+
SPK_INFO_LANG_UNKNOWN: { severity: "warning", description: "displayname_<lang> 或 description_<lang> 的語言碼不在指南清單(enu、cht、chs、jpn…)。", source: `${GUIDE}: INFO Optional Fields (description_[language])` },
|
|
22
|
+
SPK_INFO_FIELD_UNKNOWN: { severity: "warning", description: "INFO 欄位不在指南清單;DSM 不會使用它(拼錯的欄位名稱也會出現在這裡)。", source: `${GUIDE}: INFO Necessary Fields, Optional Fields` },
|
|
23
|
+
SPK_INFO_ADMINPORT_INVALID: { severity: "error", description: "INFO adminport 不是 0–65536 的整數。", source: `${GUIDE}: INFO Optional Fields (adminport)` },
|
|
24
|
+
SPK_INFO_CHECKSUM_MISMATCH: { severity: "error", description: "INFO checksum 不是 package.tgz 的 MD5。", source: `${GUIDE}: INFO Optional Fields (checksum)` },
|
|
25
|
+
SPK_PAYLOAD_UNREADABLE: { severity: "error", description: "package.tgz 不是可讀的 tar 或 tar.gz 封存,或是空的。", source: `${GUIDE}: Package Structure (package.tgz)` },
|
|
26
|
+
SPK_PAYLOAD_XZ: { severity: "warning", description: "package.tgz 是 xz 壓縮(Synology toolkit 的格式);inspect 讀不了 xz,沒有檢查 payload 內容。", source: `${GUIDE}: Pack Stage` },
|
|
27
|
+
SPK_PAYLOAD_PATH_UNSAFE: { severity: "error", description: "package.tgz 的路徑是絕對路徑或含 ..,會解開到套件目錄之外。", source: `${GUIDE}: Package Structure (package.tgz)` },
|
|
28
|
+
SPK_PAYLOAD_LINK_ESCAPE: { severity: "error", description: "package.tgz 的 symlink 或 hard link 指到套件目錄之外。", source: `${GUIDE}: Package Structure (package.tgz)` },
|
|
29
|
+
SPK_PAYLOAD_LINK_ABSOLUTE: { severity: "warning", description: "package.tgz 的 symlink 指向絕對路徑;目標由 NAS 決定,不在套件內。", source: `${GUIDE}: Package Structure (package.tgz)` },
|
|
30
|
+
SPK_UI_CONFIG_INVALID: { severity: "error", description: "dsmuidir 底下的 config 不是 JSON 物件。", source: `${GUIDE}: Integrate DSM, Application Config` },
|
|
31
|
+
SPK_UI_TEXTS_MISSING: { severity: "error", description: "config 的 title/desc 參照套件自己的字串(例如 synopkgland:app_title),但該 entry 沒有宣告 \"texts\";DSM 會直接顯示 key。", source: "DSM 7.1.1/7.4 實測(DSM 只為宣告 texts 的 entry 載入 texts/<lang>/strings)" },
|
|
32
|
+
SPK_UI_TEXTS_UNDECLARED: { severity: "warning", description: "config 的 title/desc 是 section:key 字串參照,但沒有宣告 \"texts\";除非是 DSM 內建 section,否則 DSM 會直接顯示 key。", source: "DSM 7.1.1/7.4 實測" },
|
|
33
|
+
SPK_NOARCH_ELF: { severity: "error", description: "INFO arch 是 noarch,但 package.tgz 有 ELF 執行檔。", source: `${GUIDE}: INFO Necessary Fields (arch)、Pack Stage` },
|
|
34
|
+
SPK_ELF_ARCH_MISMATCH: { severity: "error", description: "package.tgz 的 ELF 執行檔與 INFO arch 的 CPU 不符。", source: `${GUIDE}: INFO Necessary Fields (arch)、Appendix A` },
|
|
35
|
+
SPK_SCRIPT_MISSING: { severity: "error", description: "缺少 scripts/start-stop-status;DSM 用它啟動、停止與查詢套件狀態。", source: `${GUIDE}: Package Structure、scripts` },
|
|
36
|
+
SPK_SCRIPT_HOOK_MISSING: { severity: "warning", description: "缺少 preinst、postinst、preuninst、postuninst、preupgrade 或 postupgrade;指南列為必備,DSM 缺檔時的行為未驗證。", source: `${GUIDE}: scripts` },
|
|
37
|
+
SPK_SCRIPT_NOT_EXECUTABLE: { severity: "error", description: "scripts/ 的生命週期腳本沒有執行權限。", source: `${GUIDE}: scripts` },
|
|
38
|
+
SPK_SCRIPT_NO_INTERPRETER: { severity: "warning", description: "生命週期腳本開頭沒有 #!(例如 #!/bin/sh),也不是 ELF。", source: `${GUIDE}: scripts` },
|
|
39
|
+
SPK_PRIVILEGE_MISSING: { severity: "error", description: "缺少 conf/privilege;DSM 7 起必須提供。", source: `${GUIDE}: conf、Privilege Config` },
|
|
40
|
+
SPK_PRIVILEGE_INVALID: { severity: "error", description: "conf/privilege 不是 JSON 物件、沒有 defaults.run-as,或 run-as 不是 package/root。", source: `${GUIDE}: Privilege Config` },
|
|
41
|
+
SPK_PRIVILEGE_ROOT: { severity: "warning", description: "conf/privilege 要求以 root 執行(defaults、ctrl-script 或 executable)。", source: `${GUIDE}: Privilege Config` },
|
|
42
|
+
SPK_RESOURCE_INVALID: { severity: "error", description: "conf/resource 不是 JSON 物件。", source: `${GUIDE}: Resource Config` },
|
|
43
|
+
SPK_RESOURCE_UNKNOWN: { severity: "warning", description: "conf/resource 有指南沒有列出的 resource worker id。", source: `${GUIDE}: Available Workers` },
|
|
44
|
+
SPK_CONF_UNKNOWN: { severity: "warning", description: "conf/ 有 DSM 不讀取的檔案(已知:privilege、resource、PKG_DEPS、PKG_CONX、systemd/)。", source: `${GUIDE}: conf、Systemd User Unit` },
|
|
45
|
+
SPK_ICON_MISSING: { severity: "error", description: "缺少 PACKAGE_ICON.PNG 或 PACKAGE_ICON_256.PNG。", source: `${GUIDE}: Package Structure` },
|
|
46
|
+
SPK_ICON_INVALID: { severity: "error", description: "PACKAGE_ICON.PNG 或 PACKAGE_ICON_256.PNG 不是 PNG。", source: `${GUIDE}: Package Structure` },
|
|
47
|
+
SPK_ICON_SIZE: { severity: "warning", description: "PACKAGE_ICON.PNG 不是 64×64,或 PACKAGE_ICON_256.PNG 不是 256×256。", source: `${GUIDE}: Package Structure` },
|
|
48
|
+
SPK_LICENSE_TOO_LARGE: { severity: "error", description: "LICENSE 大於或等於 1 MB。", source: `${GUIDE}: Package Structure` },
|
|
49
|
+
SPK_WIZARD_INVALID: { severity: "warning", description: "WIZARD_UIFILES 的精靈檔不是 JSON 陣列。", source: `${GUIDE}: Package Structure (WIZARD_UIFILES)` },
|
|
50
|
+
SPK_WIZARD_SCRIPT_NOT_EXECUTABLE: { severity: "warning", description: "WIZARD_UIFILES 的 .sh 精靈產生器沒有執行權限。", source: `${GUIDE}: Package Structure (WIZARD_UIFILES)` },
|
|
51
|
+
SPK_WIZARD_UNKNOWN: { severity: "warning", description: "WIZARD_UIFILES 的檔名不是 install/upgrade/uninstall_uifile[_<lang>][.sh]。", source: `${GUIDE}: Package Structure (WIZARD_UIFILES)` },
|
|
52
|
+
SPK_SIGNATURE_INVALID: { severity: "warning", description: "syno_signature.asc 不是 ASCII-armored PGP 簽章(指南的套件簽章只適用 DSM 6.x,可省略此檔)。", source: `${GUIDE}: Sign Package (only for DSM6.X)` },
|
|
53
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type SpkFinding } from "./findings.js";
|
|
2
|
+
export type { SpkFinding } from "./findings.js";
|
|
3
|
+
export interface SpkValidation {
|
|
4
|
+
/** No errors; warnings do not fail validation. */
|
|
5
|
+
ok: boolean;
|
|
6
|
+
package?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
arch?: string;
|
|
9
|
+
errors: SpkFinding[];
|
|
10
|
+
warnings: SpkFinding[];
|
|
11
|
+
}
|
|
12
|
+
/** Checks an SPK against the DSM 7 package format without installing or running anything. */
|
|
13
|
+
export declare function validateSpk(spkPath: string): Promise<SpkValidation>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createWriteStream, mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { finished } from "node:stream/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import * as tar from "tar";
|
|
6
|
+
import { checkInfoFields, parseInfo } from "./check-info.js";
|
|
7
|
+
import { checkMetadata } from "./check-metadata.js";
|
|
8
|
+
import { checkPayload } from "./check-payload.js";
|
|
9
|
+
import { KNOWN_TOP_LEVEL } from "./dsm-spec.js";
|
|
10
|
+
import { Findings } from "./findings.js";
|
|
11
|
+
/** Outer files other than package.tgz are metadata; anything larger is not read. */
|
|
12
|
+
const MAX_OUTER_FILE = 16 * 1024 * 1024;
|
|
13
|
+
const TOP_LEVEL = new Set(KNOWN_TOP_LEVEL);
|
|
14
|
+
/** Checks an SPK against the DSM 7 package format without installing or running anything. */
|
|
15
|
+
export async function validateSpk(spkPath) {
|
|
16
|
+
const findings = new Findings();
|
|
17
|
+
const dir = mkdtempSync(join(tmpdir(), "syno-validate-spk-"));
|
|
18
|
+
let info = {};
|
|
19
|
+
try {
|
|
20
|
+
const outer = await readOuter(spkPath, dir, findings);
|
|
21
|
+
if (outer) {
|
|
22
|
+
checkOuterEntries(outer.entries, findings);
|
|
23
|
+
info = checkInfo(outer, findings);
|
|
24
|
+
if (!outer.payloadPath)
|
|
25
|
+
findings.add("SPK_PAYLOAD_MISSING", "缺少 package.tgz。", "package.tgz");
|
|
26
|
+
else
|
|
27
|
+
await checkPayload(outer.payloadPath, info, findings);
|
|
28
|
+
checkMetadata({ files: outer.files, modes: new Map(outer.entries.map((entry) => [normalize(entry.path), entry.mode])) }, findings);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
rmSync(dir, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
const errors = findings.list.filter((finding) => finding.severity === "error");
|
|
35
|
+
return {
|
|
36
|
+
ok: errors.length === 0,
|
|
37
|
+
...(info.package ? { package: info.package } : {}),
|
|
38
|
+
...(info.version ? { version: info.version } : {}),
|
|
39
|
+
...(info.arch ? { arch: info.arch } : {}),
|
|
40
|
+
errors,
|
|
41
|
+
warnings: findings.list.filter((finding) => finding.severity === "warning"),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** One pass over the outer tar; nothing is extracted by name, so case-only differences stay distinct. */
|
|
45
|
+
async function readOuter(spkPath, dir, findings) {
|
|
46
|
+
const outer = { entries: [], files: new Map() };
|
|
47
|
+
const writes = [];
|
|
48
|
+
try {
|
|
49
|
+
await tar.t({
|
|
50
|
+
file: spkPath,
|
|
51
|
+
strict: true,
|
|
52
|
+
onReadEntry: (entry) => {
|
|
53
|
+
outer.entries.push({ path: entry.path, type: entry.type, mode: (entry.mode ?? 0) & 0o7777 });
|
|
54
|
+
const path = normalize(entry.path);
|
|
55
|
+
if (entry.type !== "File" || !isSafeOuter(entry.path, entry.type))
|
|
56
|
+
return;
|
|
57
|
+
if (path === "package.tgz") {
|
|
58
|
+
outer.payloadPath = join(dir, "package.tgz");
|
|
59
|
+
writes.push(finished(entry.pipe(createWriteStream(outer.payloadPath))));
|
|
60
|
+
}
|
|
61
|
+
else if ((entry.size ?? 0) <= MAX_OUTER_FILE) {
|
|
62
|
+
const chunks = [];
|
|
63
|
+
entry.on("data", (chunk) => chunks.push(chunk));
|
|
64
|
+
entry.on("end", () => outer.files.set(path, Buffer.concat(chunks)));
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
await Promise.all(writes);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
outer.entries.length = 0;
|
|
72
|
+
}
|
|
73
|
+
if (outer.entries.length === 0) {
|
|
74
|
+
findings.add("SPK_NOT_TAR", "檔案不是可讀的 tar 封存。");
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return outer;
|
|
78
|
+
}
|
|
79
|
+
function normalize(path) {
|
|
80
|
+
return path.replace(/^\.\//, "").replace(/\/$/, "");
|
|
81
|
+
}
|
|
82
|
+
function isSafeOuter(path, type) {
|
|
83
|
+
return !path.startsWith("/") && !normalize(path).split("/").includes("..") && type !== "SymbolicLink" && type !== "Link";
|
|
84
|
+
}
|
|
85
|
+
function checkOuterEntries(entries, findings) {
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
const path = normalize(entry.path);
|
|
88
|
+
if (!isSafeOuter(entry.path, entry.type)) {
|
|
89
|
+
findings.add("SPK_ENTRY_UNSAFE", `${entry.path} 跳出 SPK 根目錄或是連結;DSM 只解開一般檔案與目錄。`, entry.path);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const top = path.split("/")[0];
|
|
93
|
+
if (top && !TOP_LEVEL.has(top)) {
|
|
94
|
+
findings.add("SPK_ENTRY_UNKNOWN", `${top} 不是 SPK 最上層的已知檔案(檔名區分大小寫)。`, top);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function checkInfo(outer, findings) {
|
|
99
|
+
const text = outer.files.get("INFO");
|
|
100
|
+
if (text === undefined) {
|
|
101
|
+
findings.add("SPK_INFO_MISSING", "缺少 INFO。", "INFO");
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
const fields = parseInfo(text.toString("utf8"), findings);
|
|
105
|
+
checkInfoFields(fields, outer.payloadPath, findings);
|
|
106
|
+
return Object.fromEntries(fields);
|
|
107
|
+
}
|