mirror of
https://github.com/ChiChou/entdb.git
synced 2026-06-11 07:17:47 +02:00
Add version history and side-by-side diff for binary entitlements
- Show all OS versions where the same binary path exists - Add side-by-side diff view comparing entitlements between versions - Normalize plist by sorting root-level keys before comparison - Add plist utilities for parsing and key extraction
This commit is contained in:
+31
-1
@@ -1,4 +1,4 @@
|
||||
import type { Engine } from "./types";
|
||||
import type { Engine, PathHistory } from "./types";
|
||||
import type { OS } from "@/lib/types";
|
||||
import { dataBaseURL } from "@/lib/env";
|
||||
import { fetchText, fetchLines } from "@/lib/client";
|
||||
@@ -42,6 +42,10 @@ class KVStore {
|
||||
});
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return this.#index.has(key);
|
||||
}
|
||||
|
||||
*keys(): IterableIterator<string> {
|
||||
yield* this.#index.keys();
|
||||
}
|
||||
@@ -93,6 +97,32 @@ export class KVEngine implements Engine {
|
||||
return lines.split("\n").filter(Boolean);
|
||||
}
|
||||
|
||||
async getPathHistory(path: string): Promise<PathHistory[]> {
|
||||
const osList = await this.listOS();
|
||||
const results: PathHistory[] = [];
|
||||
|
||||
const checks = osList.map(async (os) => {
|
||||
const tag = `${os.version}_${os.build}`;
|
||||
try {
|
||||
const reader = await this.openKV(`${this.#baseURL}/${tag}/blobs`);
|
||||
return { os, available: reader.has(path) };
|
||||
} catch {
|
||||
return { os, available: false };
|
||||
}
|
||||
});
|
||||
|
||||
const settled = await Promise.all(checks);
|
||||
return settled.sort((a, b) => {
|
||||
const vA = a.os.version.split(".").map(Number);
|
||||
const vB = b.os.version.split(".").map(Number);
|
||||
for (let i = 0; i < Math.max(vA.length, vB.length); i++) {
|
||||
const diff = (vB[i] || 0) - (vA[i] || 0);
|
||||
if (diff !== 0) return diff;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
#osCache: OS[] | null = null;
|
||||
|
||||
private async findOS(build: string): Promise<OS> {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import type { OS } from "@/lib/types";
|
||||
|
||||
export interface PathHistory {
|
||||
os: OS;
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
export interface Engine {
|
||||
listOS(): Promise<OS[]>;
|
||||
getPaths(build: string): Promise<string[]>;
|
||||
getBinaryXML(build: string, path: string): Promise<string>;
|
||||
getKeys(build: string): Promise<string[]>;
|
||||
getPathsForKey(build: string, key: string): Promise<string[]>;
|
||||
getPathHistory(path: string): Promise<PathHistory[]>;
|
||||
}
|
||||
|
||||
+20
-1
@@ -1,4 +1,4 @@
|
||||
import type { Engine } from "./types";
|
||||
import type { Engine, PathHistory } from "./types";
|
||||
import type { OS } from "@/lib/types";
|
||||
import { dataBaseURL } from "@/lib/env";
|
||||
|
||||
@@ -197,4 +197,23 @@ export class WASMEngine implements Engine {
|
||||
});
|
||||
return rows.map((row) => row[0] as string);
|
||||
}
|
||||
|
||||
async getPathHistory(path: string): Promise<PathHistory[]> {
|
||||
const db = await getDB();
|
||||
const osList = await this.listOS();
|
||||
|
||||
const rows = db.exec({
|
||||
sql: `SELECT DISTINCT os.build FROM bin JOIN os ON bin.osid=os.id WHERE bin.path=?`,
|
||||
bind: [path],
|
||||
rowMode: "array",
|
||||
returnValue: "resultRows",
|
||||
});
|
||||
|
||||
const availableBuilds = new Set(rows.map((row) => row[0] as string));
|
||||
|
||||
return osList.map((os) => ({
|
||||
os,
|
||||
available: availableBuilds.has(os.build),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
export function parsePlist(xml: string): Document {
|
||||
const parser = new DOMParser();
|
||||
return parser.parseFromString(xml, "application/xml");
|
||||
}
|
||||
|
||||
interface PlistEntry {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function normalizePlist(xml: string): string {
|
||||
const doc = parsePlist(xml);
|
||||
const rootDict = doc.querySelector("plist > dict");
|
||||
if (!rootDict) return xml;
|
||||
|
||||
const entries: PlistEntry[] = [];
|
||||
const children = Array.from(rootDict.children);
|
||||
|
||||
for (let i = 0; i < children.length; i += 2) {
|
||||
const keyEl = children[i];
|
||||
const valueEl = children[i + 1];
|
||||
if (keyEl?.tagName === "key" && valueEl) {
|
||||
entries.push({
|
||||
key: keyEl.textContent || "",
|
||||
value: new XMLSerializer().serializeToString(valueEl),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
entries.sort((a, b) => a.key.localeCompare(b.key));
|
||||
|
||||
const lines = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<plist version="1.0">',
|
||||
"<dict>",
|
||||
...entries.map((e) => `<key>${e.key}</key>\n${e.value}`),
|
||||
"</dict>",
|
||||
"</plist>",
|
||||
];
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export interface PlistDiff {
|
||||
added: string[];
|
||||
removed: string[];
|
||||
changed: string[];
|
||||
unchanged: string[];
|
||||
}
|
||||
|
||||
export function diffPlistKeys(oldXml: string, newXml: string): PlistDiff {
|
||||
const oldKeys = extractRootKeys(oldXml);
|
||||
const newKeys = extractRootKeys(newXml);
|
||||
|
||||
const oldDoc = parsePlist(oldXml);
|
||||
const newDoc = parsePlist(newXml);
|
||||
|
||||
const added: string[] = [];
|
||||
const removed: string[] = [];
|
||||
const changed: string[] = [];
|
||||
const unchanged: string[] = [];
|
||||
|
||||
const allKeys = new Set([...oldKeys, ...newKeys]);
|
||||
|
||||
for (const key of allKeys) {
|
||||
const inOld = oldKeys.has(key);
|
||||
const inNew = newKeys.has(key);
|
||||
|
||||
if (!inOld && inNew) {
|
||||
added.push(key);
|
||||
} else if (inOld && !inNew) {
|
||||
removed.push(key);
|
||||
} else {
|
||||
const oldValue = getKeyValue(oldDoc, key);
|
||||
const newValue = getKeyValue(newDoc, key);
|
||||
if (oldValue === newValue) {
|
||||
unchanged.push(key);
|
||||
} else {
|
||||
changed.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { added, removed, changed, unchanged };
|
||||
}
|
||||
|
||||
function extractRootKeys(xml: string): Set<string> {
|
||||
const doc = parsePlist(xml);
|
||||
const keys = new Set<string>();
|
||||
const rootDict = doc.querySelector("plist > dict");
|
||||
if (!rootDict) return keys;
|
||||
|
||||
const keyElements = rootDict.querySelectorAll(":scope > key");
|
||||
keyElements.forEach((el) => {
|
||||
if (el.textContent) keys.add(el.textContent);
|
||||
});
|
||||
return keys;
|
||||
}
|
||||
|
||||
function getKeyValue(doc: Document, keyName: string): string {
|
||||
const rootDict = doc.querySelector("plist > dict");
|
||||
if (!rootDict) return "";
|
||||
|
||||
const keys = rootDict.querySelectorAll(":scope > key");
|
||||
for (const key of keys) {
|
||||
if (key.textContent === keyName) {
|
||||
const value = key.nextElementSibling;
|
||||
if (value) {
|
||||
return value.outerHTML;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function prettifyXml(src: string): string {
|
||||
// Remove DOCTYPE to avoid DTD loading issues
|
||||
const cleanSrc = src.replace(/<!DOCTYPE[^>]*>/i, "");
|
||||
|
||||
const xmlDoc = new DOMParser().parseFromString(cleanSrc, "application/xml");
|
||||
if (xmlDoc.querySelector("parsererror")) {
|
||||
return src;
|
||||
}
|
||||
|
||||
const xsltDoc = new DOMParser().parseFromString(
|
||||
`<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:output omit-xml-declaration="yes" indent="yes"/>
|
||||
<xsl:template match="node()|@*">
|
||||
<xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>`,
|
||||
"application/xml",
|
||||
);
|
||||
|
||||
try {
|
||||
const xsltProcessor = new XSLTProcessor();
|
||||
xsltProcessor.importStylesheet(xsltDoc);
|
||||
const resultDoc = xsltProcessor.transformToDocument(xmlDoc);
|
||||
return new XMLSerializer().serializeToString(resultDoc);
|
||||
} catch {
|
||||
return src;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user