Veröffentlichte Quellen
Quellen als ZIP herunterladen
CH-J Tools
Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.
CH-J / Server Manager
1704 Dateien
0.1.2-beta
Hinzugefügt: · Erstellt:
1
"use strict";3
const api = window.chjPlugin.hashing;4
const i18n = window.chjHashI18n.createI18n();5
const t = (key, values) => i18n.t(key, values);6
const state = { algorithms: [], calculateSelection: null, verifySelection: null, compareA: null, compareB: null, manifestSource: null, manifestFile: null, manifestRoot: null, activeJobId: null, activeJob: null };7
const $ = (id) => document.getElementById(id);9
function setText(id, value) { $(id).textContent = value; }10
function describeSelection(selection) { return selection?.files?.map((file) => `${file.name} (${formatBytes(file.size)})`).join(", ") || selection?.name || t("Nothing selected"); }11
function formatBytes(value) { const bytes = Number(value || 0); if (bytes < 1024) return `${bytes} B`; const units = ["KiB", "MiB", "GiB", "TiB"]; let amount = bytes; let unit = -1; do { amount /= 1024; unit += 1; } while (amount >= 1024 && unit < units.length - 1); return `${new Intl.NumberFormat(i18n.getLanguage(), { minimumFractionDigits: amount >= 100 ? 0 : amount >= 10 ? 1 : 2, maximumFractionDigits: amount >= 100 ? 0 : amount >= 10 ? 1 : 2 }).format(amount)} ${units[unit]}`; }12
function formatDuration(ms) { const seconds = Math.max(0, Number(ms || 0) / 1000); if (seconds < 60) return `${seconds.toFixed(1)} s`; return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`; }13
function errorMessage(error) { return i18n.errorMessage(error); }14
async function action(callback) { try { await callback(); } catch (error) { setText("summary", errorMessage(error)); $("summary").className = "summary failed"; } }16
const FAVORITES_KEY = "chj.hash.favorites";17
const DEFAULT_FAVORITES = ["sha256", "sha512", "blake3"];18
function loadFavorites() {19
try { const saved = JSON.parse(localStorage.getItem(FAVORITES_KEY)); if (Array.isArray(saved)) return new Set(saved.map(String)); } catch { /* Storage may be unavailable. */ }20
return new Set(DEFAULT_FAVORITES);21
}22
function saveFavorites() { try { localStorage.setItem(FAVORITES_KEY, JSON.stringify([...state.favorites])); } catch { /* The choice lasts for this session. */ } }24
function algorithmRow(algorithm) {25
const row = document.createElement("div"); row.className = "algorithm"; row.dataset.row = algorithm.id; row.dataset.search = `${algorithm.id} ${algorithm.name}`.toLowerCase();26
const star = document.createElement("button"); star.type = "button"; star.className = "star"; star.dataset.star = algorithm.id;27
star.addEventListener("click", () => toggleFavorite(algorithm.id));28
const label = document.createElement("label"); label.className = "algorithm-name";29
const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.dataset.algorithm = algorithm.id; checkbox.checked = state.checked.has(algorithm.id);30
checkbox.addEventListener("change", () => { if (checkbox.checked) state.checked.add(algorithm.id); else state.checked.delete(algorithm.id); row.classList.toggle("selected", checkbox.checked); });31
const title = document.createElement("span"); title.textContent = algorithm.name; label.append(checkbox, title);32
row.append(star, label); row.classList.toggle("selected", checkbox.checked);33
if (algorithm.xof || algorithm.seeded || algorithm.keyed) {34
const advanced = document.createElement("span"); advanced.className = "advanced";35
const input = document.createElement("input"); input.dataset.optionFor = algorithm.id;36
if (algorithm.xof) { input.type = "number"; input.min = algorithm.xof.minBytes; input.max = algorithm.xof.maxBytes; input.value = state.options.get(algorithm.id) ?? algorithm.xof.defaultBytes; input.title = t("Output length in bytes"); input.placeholder = t("Output bytes"); }37
else if (algorithm.seeded) { input.value = state.options.get(algorithm.id) ?? "0"; input.title = t("{bits}-bit seed", { bits: algorithm.seedBits }); input.placeholder = t("Seed (default 0)"); }38
else { input.value = state.options.get(algorithm.id) ?? ""; input.title = t("{bytes}-byte hexadecimal key", { bytes: algorithm.keyBytes }); input.placeholder = t("HEX key ({bytes} bytes; zero key if blank)", { bytes: algorithm.keyBytes }); }39
input.addEventListener("input", () => state.options.set(algorithm.id, input.value));40
advanced.append(input); row.append(advanced);41
}42
return row;43
}45
function buildAlgorithmList() {46
const root = $("algorithmList"); root.replaceChildren();47
const group = (name, heading, algorithms, open) => {48
const details = document.createElement("details"); details.className = "algorithm-group"; details.open = open; details.dataset.group = name;49
const summary = document.createElement("summary"); summary.textContent = heading; if (name !== "favorites") summary.dataset.category = name;50
details.append(summary, ...algorithms.map(algorithmRow)); root.append(details);51
};52
const favorites = state.algorithms.filter((algorithm) => state.favorites.has(algorithm.id));53
if (favorites.length) group("favorites", t("Favorites"), favorites, true);54
const groups = new Map();55
for (const algorithm of state.algorithms) {56
if (state.favorites.has(algorithm.id)) continue;57
if (!groups.has(algorithm.category)) groups.set(algorithm.category, []);58
groups.get(algorithm.category).push(algorithm);59
}60
const order = ["recommended", "xof", "legacy", "broken", "fast", "checksum"];61
const rank = (name) => { const index = order.indexOf(name); return index < 0 ? order.length : index; };62
if (!$("favoritesOnly").checked) for (const [name, algorithms] of [...groups].sort((a, b) => rank(a[0]) - rank(b[0]))) group(name, t(`category.${name}`), algorithms, state.openGroups.has(name));63
root.querySelectorAll("details").forEach((details) => details.addEventListener("toggle", () => { if (details.open) state.openGroups.add(details.dataset.group); else state.openGroups.delete(details.dataset.group); }));64
renderStars(); filterAlgorithms();65
}66
function renderStars() {67
document.querySelectorAll("[data-star]").forEach((star) => {68
const favorite = state.favorites.has(star.dataset.star);69
star.textContent = favorite ? "★" : "☆"; star.classList.toggle("on", favorite);70
star.setAttribute("aria-pressed", String(favorite)); const label = t(favorite ? "Remove from favorites" : "Add to favorites"); star.title = label; star.setAttribute("aria-label", label);71
});72
}73
function toggleFavorite(id) {74
if (state.favorites.has(id)) state.favorites.delete(id); else state.favorites.add(id);75
saveFavorites(); buildAlgorithmList(); populateAlgorithmSelects();76
}77
function filterAlgorithms() {78
const query = $("algorithmSearch").value.trim().toLowerCase();79
document.querySelectorAll("[data-row]").forEach((row) => { row.hidden = Boolean(query) && !row.dataset.search.includes(query); });80
document.querySelectorAll("#algorithmList details").forEach((details) => {81
const visible = [...details.querySelectorAll("[data-row]")].some((row) => !row.hidden);82
details.hidden = !visible; if (query && visible) details.open = true;83
});84
}85
function populateAlgorithmSelects() {86
const fixed = (algorithm) => !algorithm.xof;87
for (const [id, filter] of [["verifyAlgorithm", fixed], ["compareAlgorithm", fixed], ["manifestAlgorithm", (algorithm) => fixed(algorithm) && !algorithm.seeded && !algorithm.keyed]]) {88
const previous = [...$(id).selectedOptions].map((option) => option.value);89
populateSelect(id, filter);90
for (const option of $(id).options) option.selected = previous.includes(option.value);91
}92
}94
function algorithmRequest(algorithm, optionRoot = document) {95
const request = { id: algorithm.id }; const input = optionRoot.querySelector(`[data-option-for="${algorithm.id}"]`);96
if (algorithm.xof) request.outputBytes = Number(input?.value || algorithm.xof.defaultBytes);97
if (algorithm.seeded) request.seed = input?.value || "0";98
if (algorithm.keyed && input?.value.trim()) request.keyHex = input.value.trim();99
return request;100
}101
function selectedAlgorithms() { return state.algorithms.filter((algorithm) => state.checked.has(algorithm.id)).map((algorithm) => algorithmRequest(algorithm)); }102
function simpleAlgorithm(id) { const algorithm = state.algorithms.find((item) => item.id === id); return algorithmRequest(algorithm); }104
function populateSelect(id, filter = () => true) {105
const select = $(id); const first = select.querySelector("option[value='']"); select.replaceChildren(); if (first) select.append(first);106
const favoritesFirst = [...state.algorithms.filter((algorithm) => state.favorites.has(algorithm.id)), ...state.algorithms.filter((algorithm) => !state.favorites.has(algorithm.id))];107
for (const algorithm of favoritesFirst.filter(filter)) { const option = document.createElement("option"); option.value = algorithm.id; option.textContent = algorithm.name; select.append(option); }108
}110
async function chooseFiles(multiple) { const result = await api.selectFiles(multiple); return result.canceled ? null : result; }111
async function chooseDirectory() { const result = await api.selectDirectory(); return result.canceled ? null : result; }112
function acceptJob(job) { state.activeJobId = job.jobId; state.activeJob = job; renderJob(job); }114
function renderJob(job) {115
if (!job || (state.activeJobId && job.jobId !== state.activeJobId)) return;116
state.activeJob = job; state.activeJobId = job.jobId;117
setText("jobState", `${t(job.state).toLocaleUpperCase(i18n.getLanguage())} · ${job.filesCompleted}/${job.filesTotal}${job.currentFile ? ` · ${job.currentFile.name}` : ""}`);118
$("progressBar").style.width = `${Math.max(0, Math.min(100, job.percent || 0))}%`;119
$("progressStats").replaceChildren(...[120
`${formatBytes(job.bytes)} / ${formatBytes(job.totalBytes)}`,121
`${Number(job.percent || 0).toFixed(1)}%`,122
`${formatBytes(job.throughputBytesPerSecond)}/s`,123
t("Elapsed {duration}", { duration: formatDuration(job.elapsedMs) }),124
t("ETA {duration}", { duration: job.etaMs == null ? "—" : formatDuration(job.etaMs) })125
].map((text) => { const span = document.createElement("span"); span.textContent = text; return span; }));126
$("cancel").classList.toggle("hidden", !["queued", "running"].includes(job.state));127
$("copyAll").classList.toggle("hidden", !job.results.some((result) => result.hashes?.length));128
$("exportResults").classList.toggle("hidden", job.state !== "completed" || !job.results.some((result) => result.hashes?.length));129
const summary = $("summary"); summary.className = "summary";130
if (job.error) { summary.textContent = `${job.error.code}: ${errorMessage(job.error)}`; summary.classList.add("failed"); }131
else if (job.mode === "compare" && job.state === "completed") { const failed = job.results.some((result) => result.status !== "COMPLETED"); summary.textContent = t(failed ? "Comparison failed" : job.comparison?.identical ? "IDENTICAL digests" : "DIFFERENT digests"); summary.classList.add(failed ? "failed" : job.comparison?.identical ? "match" : "mismatch"); }132
else if (job.mode === "verify" && job.results[0]) { summary.textContent = t(job.results[0].status); summary.classList.add(job.results[0].status === "MATCH" ? "match" : "mismatch"); }133
else if (job.state === "completed") { const matches = job.results.filter((result) => result.status === "MATCH").length; const failures = job.results.filter((result) => !["COMPLETED", "MATCH"].includes(result.status)).length; summary.textContent = job.mode === "verify-manifest" ? t("{matches} matched · {failures} failed", { matches, failures }) : failures ? t("{completed} completed · {failures} failed", { completed: job.results.length - failures, failures }) : t("{count} file(s) completed", { count: job.results.length }); if (failures) summary.classList.add("failed"); }134
else summary.textContent = "";135
renderResults(job);136
}138
function renderResults(job) {139
const body = $("results"); body.replaceChildren();140
for (const result of job.results) {141
const hashes = result.hashes?.length ? result.hashes : [null];142
for (const hash of hashes) {143
const row = document.createElement("tr");144
for (const value of [result.file.relativePath, formatBytes(result.file.size), hash?.id || "—", hash?.value || (result.error ? errorMessage(result.error) : "—"), t(result.status)]) { const cell = document.createElement("td"); cell.textContent = value; if (value === hash?.value) cell.className = "digest"; row.append(cell); }145
row.children[4].className = `status-${String(result.status).toLowerCase().replace(/\s+/g, "-")}`;146
const controls = document.createElement("td");147
if (hash) { const copy = document.createElement("button"); copy.className = "small ghost"; copy.textContent = t("Copy"); copy.addEventListener("click", () => action(async () => { await api.copyResult({ jobId: job.jobId, fileId: result.file.fileId, algorithmId: hash.id, compact: true }); copy.textContent = t("Copied"); setTimeout(() => { copy.textContent = t("Copy"); }, 1200); })); controls.append(copy); }148
row.append(controls); body.append(row);149
}150
}151
}153
document.querySelectorAll(".tab").forEach((button) => button.addEventListener("click", () => {154
document.querySelectorAll(".tab").forEach((item) => item.classList.toggle("active", item === button));155
document.querySelectorAll(".tab-panel").forEach((panel) => panel.classList.toggle("active", panel.id === button.dataset.tab));156
}));158
$("closeButton").addEventListener("click", () => window.chjPlugin.close());159
$("calculateMode").addEventListener("change", () => $("recursiveLabel").classList.toggle("hidden", $("calculateMode").value !== "directory"));160
$("calculatePick").addEventListener("click", () => action(async () => { const mode = $("calculateMode").value; state.calculateSelection = mode === "directory" ? await chooseDirectory() : await chooseFiles(mode === "multiple"); setText("calculateSelection", describeSelection(state.calculateSelection)); }));161
$("selectAll").addEventListener("click", () => { state.checked = new Set(state.algorithms.map((algorithm) => algorithm.id)); buildAlgorithmList(); });162
$("selectNone").addEventListener("click", () => { state.checked = new Set(); buildAlgorithmList(); });163
$("recommended").addEventListener("click", () => { state.checked = new Set(DEFAULT_FAVORITES); buildAlgorithmList(); });164
$("algorithmSearch").addEventListener("input", filterAlgorithms);165
$("favoritesOnly").addEventListener("change", buildAlgorithmList);166
$("calculateStart").addEventListener("click", () => action(async () => { if (!state.calculateSelection) throw new Error(t("Choose local input first.")); const algorithms = selectedAlgorithms(); if (!algorithms.length) throw new Error(t("Select at least one algorithm.")); acceptJob(await api.start({ selectionId: state.calculateSelection.selectionId, algorithms, recursive: $("recursive").checked, output: $("output").value })); }));168
$("verifyPick").addEventListener("click", () => action(async () => { state.verifySelection = await chooseFiles(false); setText("verifySelection", describeSelection(state.verifySelection)); }));169
$("expected").addEventListener("input", () => { const value = $("expected").value.trim(); if ($("expectedEncoding").value !== "hex" || !/^[0-9a-f]+$/i.test(value)) return setText("detection", ""); const candidates = state.algorithms.filter((algorithm) => algorithm.digestBits && algorithm.digestBits / 4 === value.length); setText("detection", candidates.length ? t("Length candidates: {names}. Length alone is not definitive.", { names: candidates.map((item) => item.name).join(", ") }) : t("No fixed-length algorithm matches this length.")); });170
$("expectedEncoding").addEventListener("change", () => $("expected").dispatchEvent(new Event("input")));171
$("verifyStart").addEventListener("click", () => action(async () => { if (!state.verifySelection) throw new Error(t("Choose a local file first.")); acceptJob(await api.verify({ selectionId: state.verifySelection.selectionId, algorithm: simpleAlgorithm($("verifyAlgorithm").value), expected: $("expected").value, expectedEncoding: $("expectedEncoding").value })); }));173
$("comparePickA").addEventListener("click", () => action(async () => { state.compareA = await chooseFiles(false); setText("compareA", describeSelection(state.compareA)); }));174
$("comparePickB").addEventListener("click", () => action(async () => { state.compareB = await chooseFiles(false); setText("compareB", describeSelection(state.compareB)); }));175
$("compareStart").addEventListener("click", () => action(async () => { if (!state.compareA || !state.compareB) throw new Error(t("Choose both local files first.")); const algorithms = [...$("compareAlgorithm").selectedOptions].map((option) => simpleAlgorithm(option.value)); if (!algorithms.length) throw new Error(t("Select at least one comparison algorithm.")); acceptJob(await api.compare({ leftSelectionId: state.compareA.selectionId, rightSelectionId: state.compareB.selectionId, algorithms })); }));177
$("manifestSourcePick").addEventListener("click", () => action(async () => { state.manifestSource = $("manifestSourceMode").value === "directory" ? await chooseDirectory() : await chooseFiles(true); setText("manifestSource", describeSelection(state.manifestSource)); }));178
$("manifestFormat").addEventListener("change", () => { if ($("manifestFormat").value === "sfv") $("manifestAlgorithm").value = "crc32"; });179
$("manifestGenerate").addEventListener("click", () => action(async () => { if (!state.manifestSource) throw new Error(t("Choose local source files first.")); const id = $("manifestAlgorithm").value; const extension = $("manifestFormat").value === "sfv" ? "sfv" : (id === "crc32" ? "checksums" : id.replace(/-/g, "")); const destination = await api.selectManifestDestination(`checksums.${extension}`); if (destination.canceled) return; acceptJob(await api.generateManifest({ selectionId: state.manifestSource.selectionId, destinationSelectionId: destination.selectionId, algorithm: simpleAlgorithm(id), format: $("manifestFormat").value, recursive: $("manifestRecursive").checked })); }));180
$("manifestPick").addEventListener("click", () => action(async () => { const result = await api.selectManifest(); state.manifestFile = result.canceled ? null : result; setText("manifestSelection", describeSelection(result.canceled ? null : { files: [result.file] })); }));181
$("manifestRootPick").addEventListener("click", () => action(async () => { state.manifestRoot = await chooseDirectory(); setText("manifestRoot", describeSelection(state.manifestRoot)); }));182
$("manifestVerify").addEventListener("click", () => action(async () => { if (!state.manifestFile || !state.manifestRoot) throw new Error(t("Choose a manifest and its local root directory.")); acceptJob(await api.verifyManifest({ manifestSelectionId: state.manifestFile.selectionId, rootSelectionId: state.manifestRoot.selectionId, algorithmId: $("manifestVerifyAlgorithm").value || undefined })); }));184
$("cancel").addEventListener("click", () => action(async () => { if (state.activeJobId) renderJob(await api.cancel(state.activeJobId)); }));185
$("copyAll").addEventListener("click", () => action(async () => { await api.copyResult({ jobId: state.activeJobId }); $("copyAll").textContent = t("Copied"); setTimeout(() => { $("copyAll").textContent = t("Copy all"); }, 1200); }));186
$("exportResults").addEventListener("click", () => action(async () => { const destination = await api.selectManifestDestination("hash-results.txt"); if (!destination.canceled) await api.exportResults({ jobId: state.activeJobId, destinationSelectionId: destination.selectionId, format: "text" }); }));188
function pgpError(error) { const box = $("pgpResult"); box.className = "summary failed"; box.textContent = errorMessage(error); }189
function renderPgpResult(result) {190
const box = $("pgpResult"); box.className = "summary match"; box.replaceChildren();191
const add = (text, className) => { const line = document.createElement("div"); line.textContent = text; if (className) line.className = className; box.append(line); };192
add(t("The signature is valid."), "pgp-verdict");193
if (result.userIds?.length) add(t("Signed by: {name}", { name: result.userIds.join(", ") }));194
add(t("Key fingerprint: {fingerprint}", { fingerprint: result.primaryFingerprint }), "mono");195
if (result.signerFingerprint !== result.primaryFingerprint) add(t("Signing subkey: {fingerprint}", { fingerprint: result.signerFingerprint }), "mono");196
if (result.createdAt) add(t("Signed on: {date}", { date: new Date(result.createdAt).toLocaleString(i18n.getLanguage()) }));197
add(t(result.fingerprintMatched ? "The key matches the fingerprint you entered." : "No fingerprint was entered. Compare the key fingerprint with one from the signer."));198
}199
async function refreshPgpKeys() {200
const { keys } = await api.pgpKeys(); const root = $("pgpKeys"); root.replaceChildren();201
if (!keys.length) { const empty = document.createElement("span"); empty.className = "selection"; empty.textContent = t("No saved keys"); root.append(empty); return; }202
for (const key of keys) {203
const row = document.createElement("div"); row.className = "pgp-key";204
const text = document.createElement("div"); const name = document.createElement("strong"); name.textContent = key.userIds?.[0] || "—"; const fingerprint = document.createElement("div"); fingerprint.className = "mono selection"; fingerprint.textContent = key.fingerprint; text.append(name, fingerprint);205
const use = document.createElement("button"); use.type = "button"; use.className = "small ghost"; use.textContent = t("Use fingerprint"); use.addEventListener("click", () => { $("pgpFingerprint").value = key.fingerprint; });206
const remove = document.createElement("button"); remove.type = "button"; remove.className = "small ghost"; remove.textContent = t("Remove"); remove.addEventListener("click", () => action(async () => { await api.pgpRemoveKey(key.fingerprint); await refreshPgpKeys(); }));207
row.append(text, use, remove); root.append(row);208
}209
}210
$("pgpFilePick").addEventListener("click", () => action(async () => { state.pgpFile = await chooseFiles(false); setText("pgpFile", describeSelection(state.pgpFile)); }));211
$("pgpSignaturePick").addEventListener("click", () => action(async () => { const result = await api.selectPgpFile("signature"); state.pgpSignature = result.canceled ? null : result; setText("pgpSignature", result.canceled ? t("Nothing selected") : result.file.name); }));212
$("pgpKeyPick").addEventListener("click", () => action(async () => { const result = await api.selectPgpFile("key"); state.pgpKey = result.canceled ? null : result; setText("pgpKey", result.canceled ? t("Saved keys are used if you do not add one") : result.file.name); }));213
$("pgpVerify").addEventListener("click", async () => {214
const button = $("pgpVerify"); button.disabled = true; $("pgpResult").className = "summary"; $("pgpResult").textContent = t("Verifying…");215
try {216
if (!state.pgpFile) throw new Error(t("Choose a local file first."));217
if (!state.pgpSignature) throw new Error(t("Choose the signature first."));218
const result = await api.pgpVerify({ fileSelectionId: state.pgpFile.selectionId, signatureSelectionId: state.pgpSignature.selectionId, keySelectionId: state.pgpKey?.selectionId ?? null, keyText: $("pgpKeyText").value, fingerprint: $("pgpFingerprint").value, saveKey: $("pgpSaveKey").checked });219
renderPgpResult(result); if (result.saved) await refreshPgpKeys();220
} catch (error) { pgpError(error); }221
finally { button.disabled = false; }222
});224
api.onProgress((job) => renderJob(job));226
function applyLanguage(language) {227
i18n.setLanguage(language);228
i18n.apply(document);229
$("algorithmSearch").placeholder = t("Search algorithms");230
for (const [id, selection] of Object.entries({ calculateSelection: state.calculateSelection, verifySelection: state.verifySelection, compareA: state.compareA, compareB: state.compareB, manifestSource: state.manifestSource, manifestSelection: state.manifestFile ? { files: [state.manifestFile.file] } : null, manifestRoot: state.manifestRoot })) setText(id, describeSelection(selection));231
if (state.algorithms.length) buildAlgorithmList();232
$("expected").dispatchEvent(new Event("input"));233
if (state.activeJob) renderJob(state.activeJob);234
}236
window.chjPlugin.ui.onLanguageChanged(applyLanguage);237
action(async () => {238
const [language, algorithms] = await Promise.all([window.chjPlugin.ui.getLanguage(), api.getAlgorithms()]);239
state.algorithms = algorithms;240
state.favorites = loadFavorites();241
state.checked = new Set(DEFAULT_FAVORITES.filter((id) => state.favorites.has(id)));242
state.options = new Map(); state.openGroups = new Set();243
applyLanguage(language);244
buildAlgorithmList();245
populateAlgorithmSelects();246
await refreshPgpKeys();247
$("verifyAlgorithm").value = "sha256"; $("compareAlgorithm").value = "sha256"; $("manifestAlgorithm").value = "sha256";248
});SHA-256
01503ba42edafa983407b94754802875e33b9c553724a3a6b612b155dc569202CH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.
SHA-256 des Archivs
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f