- Rename all user-facing and technical identifiers from OpenPets/Pet to FamiliarOS/Familiar. - Rename packages from @open-pets/* to @familiaros/*; rename install-pet/pet-format packages. - Rename plugin IDs and directories from openpets.* to familiaros.*. - Rename IPC namespace from openpets:* to familiaros:* and state filenames from openpets-* to familiaros-* with legacy migration. - Rename source files (pet-window, built-in-pet, default-pet-controller, etc.) to familiar equivalents. - Update locales (en, es-419, ja, ko, pt-BR, zh-Hans, zh-Hant) and tray/pet context menu strings. - Add Familiar naming feature: preference, settings input, tray menu display. - Update assets and packaging config; all desktop tests pass.
66 lines
6.6 KiB
TypeScript
66 lines
6.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdtempSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { validatePluginCatalog } from "../src/plugin-catalog-validation.js";
|
|
import { installCatalogPluginPackage, safeDeletePluginInstallDir, validatePluginZipUrl } from "../src/plugin-package.js";
|
|
import type { FamiliarOSPluginManifest } from "../src/plugin-manifest.js";
|
|
|
|
const manifest: FamiliarOSPluginManifest = { manifestVersion: 1, id: "zip-plug", name: "Zip Plug", version: "1.0.0", runtime: "declarative", permissions: ["timer", "familiar:speak"], triggers: [{ on: "timer", everyMinutes: 5, actions: [{ type: "familiar.speak", message: "hi" }] }] };
|
|
const text = JSON.stringify(manifest);
|
|
const zip = makeZip("familiaros.plugin.json", Buffer.from(text));
|
|
const entry = { id: manifest.id, name: manifest.name, version: manifest.version, description: "desc", runtime: "declarative" as const, permissions: manifest.permissions, downloadUrl: "https://zip.familiaros.dev/plugins/zip-plug.zip", sha256: createHash("sha256").update(zip).digest("hex") };
|
|
|
|
validatePluginZipUrl(entry.downloadUrl);
|
|
assert.throws(() => validatePluginZipUrl("http://zip.familiaros.dev/plugins/x.zip"), /not allowed/);
|
|
assert.throws(() => validatePluginZipUrl("https://zip.familiaros.dev:444/plugins/x.zip"), /not allowed/);
|
|
|
|
const userData = mkdtempSync(join(tmpdir(), "familiaros-plugin-package-"));
|
|
const installed = await installCatalogPluginPackage({ userDataPath: userData, catalogEntry: entry, zip });
|
|
assert.equal(installed.manifest.id, manifest.id);
|
|
assert.equal(readFileSync(join(userData, "plugins", manifest.id, "familiaros.plugin.json"), "utf8"), text);
|
|
assert.rejects(() => installCatalogPluginPackage({ userDataPath: userData, catalogEntry: { ...entry, name: "Other" }, zip }), /does not match/);
|
|
assert.rejects(() => installCatalogPluginPackage({ userDataPath: userData, catalogEntry: entry, zip: makeZip("nested/familiaros.plugin.json", Buffer.from(text)) }), /invalid|exactly one root manifest|must contain familiaros/);
|
|
|
|
const jsManifest: FamiliarOSPluginManifest = { manifestVersion: 2, id: "js-plug", name: "JS Plug", version: "1.0.0", runtime: "javascript", sdkVersion: "1.0.0", entry: "index.mjs", permissions: ["familiar:speak"] };
|
|
const jsText = JSON.stringify(jsManifest);
|
|
const jsEntryText = "export default {};\n";
|
|
const jsZip = makeZipFiles([{ name: "familiaros.plugin.json", data: Buffer.from(jsText) }, { name: "index.mjs", data: Buffer.from(jsEntryText) }]);
|
|
const jsCatalogEntry = { id: jsManifest.id, name: jsManifest.name, version: jsManifest.version, description: "desc", runtime: "javascript" as const, sdkVersion: "1.0.0", permissions: jsManifest.permissions, downloadUrl: "https://zip.familiaros.dev/plugins/js-plug.zip", sha256: createHash("sha256").update(jsZip).digest("hex") };
|
|
const jsInstalled = await installCatalogPluginPackage({ userDataPath: userData, catalogEntry: jsCatalogEntry, zip: jsZip });
|
|
assert.equal(readFileSync(join(jsInstalled.installPath, "index.mjs"), "utf8"), jsEntryText);
|
|
await assert.rejects(() => installCatalogPluginPackage({ userDataPath: userData, catalogEntry: jsCatalogEntry, zip: makeZipFiles([{ name: "familiaros.plugin.json", data: Buffer.from(jsText) }]) }), /manifest and entry/);
|
|
await assert.rejects(() => installCatalogPluginPackage({ userDataPath: userData, catalogEntry: jsCatalogEntry, zip: makeZipFiles([{ name: "familiaros.plugin.json", data: Buffer.from(jsText) }, { name: "index.mjs", data: Buffer.from(jsEntryText) }, { name: "extra.js", data: Buffer.from("") }]) }), /manifest and entry|too many entries/);
|
|
|
|
const canonicalEntry = validatePluginCatalog({ version: 1, generatedAt: new Date().toISOString(), plugins: [{ ...entry, permissions: ["timer", "familiar:speak"] }] }).plugins[0];
|
|
await installCatalogPluginPackage({ userDataPath: userData, catalogEntry: canonicalEntry, zip });
|
|
|
|
const oldText = JSON.stringify({ ...manifest, version: "0.9.0" });
|
|
writeFileSync(join(userData, "plugins", manifest.id, "familiaros.plugin.json"), oldText, "utf8");
|
|
await installCatalogPluginPackage({ userDataPath: userData, catalogEntry: entry, zip });
|
|
assert.equal(readFileSync(join(userData, "plugins", manifest.id, "familiaros.plugin.json"), "utf8"), text);
|
|
|
|
const deleteRoot = mkdtempSync(join(tmpdir(), "familiaros-plugin-delete-"));
|
|
const outside = mkdtempSync(join(tmpdir(), "familiaros-plugin-outside-"));
|
|
symlinkSync(outside, join(deleteRoot, "plugins"), "dir");
|
|
await assert.rejects(() => safeDeletePluginInstallDir(deleteRoot, manifest.id, join(deleteRoot, "plugins", manifest.id), "catalog"), /invalid|unexpected/);
|
|
|
|
console.error("Plugin package validation passed.");
|
|
|
|
function makeZip(name: string, data: Buffer): Buffer {
|
|
return makeZipFiles([{ name, data }]);
|
|
}
|
|
|
|
function makeZipFiles(files: Array<{ name: string; data: Buffer }>): Buffer {
|
|
const locals: Buffer[] = []; const centrals: Buffer[] = []; let offset = 0;
|
|
for (const file of files) {
|
|
const nameBuffer = Buffer.from(file.name); const crc = crc32(file.data); const now = 0;
|
|
const local = Buffer.alloc(30); local.writeUInt32LE(0x04034b50, 0); local.writeUInt16LE(20, 4); local.writeUInt16LE(0, 6); local.writeUInt16LE(0, 8); local.writeUInt32LE(now, 10); local.writeUInt32LE(crc, 14); local.writeUInt32LE(file.data.length, 18); local.writeUInt32LE(file.data.length, 22); local.writeUInt16LE(nameBuffer.length, 26);
|
|
const central = Buffer.alloc(46); central.writeUInt32LE(0x02014b50, 0); central.writeUInt16LE(20, 4); central.writeUInt16LE(20, 6); central.writeUInt16LE(0, 8); central.writeUInt16LE(0, 10); central.writeUInt32LE(now, 12); central.writeUInt32LE(crc, 16); central.writeUInt32LE(file.data.length, 20); central.writeUInt32LE(file.data.length, 24); central.writeUInt16LE(nameBuffer.length, 28); central.writeUInt32LE((0o100644 << 16) >>> 0, 38); central.writeUInt32LE(offset, 42);
|
|
const localPart = Buffer.concat([local, nameBuffer, file.data]); locals.push(localPart); centrals.push(Buffer.concat([central, nameBuffer])); offset += localPart.length;
|
|
}
|
|
const localPart = Buffer.concat(locals); const centralPart = Buffer.concat(centrals); const end = Buffer.alloc(22); end.writeUInt32LE(0x06054b50, 0); end.writeUInt16LE(files.length, 8); end.writeUInt16LE(files.length, 10); end.writeUInt32LE(centralPart.length, 12); end.writeUInt32LE(localPart.length, 16); return Buffer.concat([localPart, centralPart, end]);
|
|
}
|
|
|
|
function crc32(buffer: Buffer): number { let crc = -1; for (const byte of buffer) { crc ^= byte; for (let i = 0; i < 8; i++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); } return (crc ^ -1) >>> 0; }
|