From 7369a6d9ed7113476f7c35ab135ad90f82c5a9fa Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon, 12 Oct 2026 00:10:46 +0000
Subject: [PATCH] card #12: update ventures/docscope/index.html
---
ventures/docscope/index.html | 281 +++++++++--------------------------
1 file changed, 70 insertions(+), 211 deletions(-)
diff --git a/ventures/docscope/index.html b/ventures/docscope/index.html
index d6bb4c3..845d272 100644
--- a/ventures/docscope/index.html
+++ b/ventures/docscope/index.html
@@ -148,7 +148,6 @@
letter-spacing: .16em;
text-transform: uppercase;
}
- h2 { margin: 0; font: 700 22px/1.15 var(--sans); }
.lede { margin: 0 0 14px; color: var(--ink-soft); max-width: 68ch; }
.statusline {
display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap;
@@ -191,15 +190,8 @@
.tree ul { margin: 0; padding: 0 0 0 14px; list-style: none; border-left: 1px dotted #b7c0ca; }
.part {
display: flex; align-items: baseline; gap: 8px; width: 100%;
- text-align: left;
- background: transparent;
- border: 0;
- border-left: 3px solid transparent;
- padding: 4px 6px;
- font: inherit;
- color: var(--ink);
- cursor: pointer;
- touch-action: manipulation;
+ text-align: left; background: transparent; border: 0; border-left: 3px solid transparent;
+ padding: 4px 6px; font: inherit; color: var(--ink); cursor: pointer; touch-action: manipulation;
}
.part:hover, .part.on { background: #fff6d8; border-left-color: var(--amber); }
.part .nm { overflow-wrap: anywhere; }
@@ -212,12 +204,8 @@
.findings { display: grid; gap: 8px; }
.finding { border: 1px solid var(--rule); background: #fff; padding: 8px 10px; font-size: 14px; }
.finding .tag {
- display: inline-block;
- margin-right: 6px;
- padding: 0 6px;
- font: 700 11px/1.6 var(--mono);
- letter-spacing: .04em;
- text-transform: uppercase;
+ display: inline-block; margin-right: 6px; padding: 0 6px;
+ font: 700 11px/1.6 var(--mono); letter-spacing: .04em; text-transform: uppercase;
}
.sev-leak { background: var(--danger-bg); }
.sev-leak .tag { background: var(--danger); color: #fff; }
@@ -228,11 +216,7 @@
.sev-ok { background: var(--ok-bg); }
.sev-ok .tag { background: var(--ok); color: #fff; }
.finding p { margin: 4px 0 0; }
- .finding code, .anno code, .limits code, .ref code {
- font: 12px/1.4 var(--mono);
- background: #eef2f6;
- padding: 0 4px;
- }
+ .finding code, .anno code, .limits code, .ref code { font: 12px/1.4 var(--mono); background: #eef2f6; padding: 0 4px; }
.raw-wrap { display: grid; gap: 8px; }
.anno { margin: 0; font-size: 14px; color: var(--ink-soft); }
.verdict { display: inline-block; margin-top: 4px; padding: 1px 7px; font: 700 12px/1.6 var(--mono); }
@@ -240,27 +224,18 @@
.verdict.careful { background: var(--warn); color: #fff; }
.verdict.yes { background: var(--ok); color: #fff; }
pre.xml {
- margin: 0;
- max-height: 340px;
- overflow: auto;
- padding: 10px;
- background: #11161c;
- color: #e7eef6;
- font: 12px/1.45 var(--mono);
- white-space: pre-wrap;
- overflow-wrap: anywhere;
+ margin: 0; max-height: 340px; overflow: auto; padding: 10px;
+ background: #11161c; color: #e7eef6; font: 12px/1.45 var(--mono);
+ white-space: pre-wrap; overflow-wrap: anywhere;
}
.err { color: var(--danger); font-weight: 700; }
.ref { width: 100%; border-collapse: collapse; font-size: 14px; }
.ref th, .ref td { text-align: left; vertical-align: top; padding: 8px; border-bottom: 1px solid var(--rule); }
.ref th { font: 700 12px/1.3 var(--mono); letter-spacing: .04em; text-transform: uppercase; color: var(--mute); }
- .repos { display: grid; gap: 0; }
+ .repos { display: grid; }
.repo {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- gap: 4px 16px;
- padding: 12px 0;
- border-top: 1px solid var(--rule);
+ display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 16px;
+ padding: 12px 0; border-top: 1px solid var(--rule);
}
.repo a { font: 600 15px/1.3 var(--mono); }
.stars { font: 13px/1 var(--mono); color: var(--mute); white-space: nowrap; }
@@ -269,22 +244,11 @@
.tiers { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.tier {
background: var(--paper);
- border: 2px solid #fff;
- border-right-color: #8b95a3;
- border-bottom-color: #8b95a3;
- padding: 14px 14px 12px;
- display: flex;
- flex-direction: column;
- min-width: 0;
+ border: 2px solid #fff; border-right-color: #8b95a3; border-bottom-color: #8b95a3;
+ padding: 14px 14px 12px; display: flex; flex-direction: column; min-width: 0;
}
.tier.featured { background: #fffdf6; outline: 2px solid var(--amber); }
- .tier-name {
- margin: 0 0 6px;
- font: 700 12px/1 var(--mono);
- letter-spacing: .12em;
- text-transform: uppercase;
- color: var(--title);
- }
+ .tier-name { margin: 0 0 6px; font: 700 12px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; color: var(--title); }
.price { margin: 0; font: 700 26px/1.15 var(--sans); letter-spacing: -.02em; }
.price small { display: block; margin-top: 4px; font: 400 13px/1.35 var(--sans); color: var(--mute); }
.for-whom { margin: 8px 0; color: var(--mute); font-size: 13.5px; }
@@ -389,7 +353,7 @@
OOXML parts this page actually handles
-
Only parts the inspector reads are listed. Each link goes to the ECMA-376 5th edition page that publishes the section defining it. Parts we do not understand still appear in the diagram, labelled as unhandled.
+
Only parts the inspector reads are listed. Each link goes to the ECMA-376 5th edition page. Parts we do not understand still appear in the diagram, labelled as unhandled.
| Part | What we read | Spec |
@@ -542,70 +506,20 @@
"use strict";
var SAMPLE_NAME = "Hale-engagement-letter.docx";
- var SAMPLE_NOTE = "Preloaded sample (sample.docx, a real .docx). It is fetched and unzipped by the same code as a file you drop — not a canned report.";
+ var SAMPLE_NOTE = "Preloaded sample, decoded from sample.b64 and unzipped by the same code as a file you drop — not a canned report.";
var ECMA = "https://ecma-international.org/publications-and-standards/standards/ecma-376/";
var PARTS = {
- "[Content_Types].xml": {
- title: "Content Types stream",
- spec: "ECMA-376 Part 2, §10.1.2",
- note: "Names the media type of every part. Office uses it to decide which XML vocabulary a part speaks. It is not itself a leak, but a missing or odd override is a sign the package was hand-edited.",
- send: "yes"
- },
- "docProps/core.xml": {
- title: "Core properties",
- spec: "ECMA-376 Part 2, §11.1",
- note: "Dublin Core metadata: title, creator, lastModifiedBy, revision, created and modified. This is the usual place a previous client's name or your own name survives a Save As.",
- send: "no"
- },
- "docProps/app.xml": {
- title: "Extended properties",
- spec: "ECMA-376 Part 1, §22.2",
- note: "Application, company, template name, and page or word counts. The template name often identifies the firm or the prior engagement the file was cloned from.",
- send: "careful"
- },
- "docProps/thumbnail.jpeg": {
- title: "Package thumbnail",
- spec: "ECMA-376 Part 2, thumbnail relationship",
- note: "A JPEG preview of the document, stored even when you never inserted an image. It can show a page you have since deleted, and its EXIF can name the author.",
- send: "no"
- },
- "word/document.xml": {
- title: "Main document part",
- spec: "ECMA-376 Part 1, §17.13",
- note: "The visible text, plus tracked insertions (w:ins), tracked deletions (w:del / w:delText) and comment markers. Accepting changes in Word is not the same as this XML being clean.",
- send: "careful"
- },
- "word/comments.xml": {
- title: "Comments part",
- spec: "ECMA-376 Part 1, §17.13.4",
- note: "Comment authors, timestamps and the comment text. Deleting a comment in the UI does not always remove this part.",
- send: "no"
- },
- "word/people.xml": {
- title: "People part",
- spec: "ECMA-376 people part",
- note: "Authors who have touched the document, sometimes with a provider user id or email. Easy to miss because Word never shows this file.",
- send: "no"
- },
- "word/styles.xml": {
- title: "Styles part",
- spec: "ECMA-376 Part 1, §17.7",
- note: "Paragraph and character styles. Rarely a direct leak; style names copied from a client template can still identify the source firm.",
- send: "yes"
- },
- "xl/workbook.xml": {
- title: "Workbook part",
- spec: "ECMA-376 Part 1, §18.2",
- note: "Sheet index. state=\"hidden\" or state=\"veryHidden\" marks a sheet the recipient will not see in the tab bar but can unhide.",
- send: "careful"
- },
- "ppt/presentation.xml": {
- title: "Presentation part",
- spec: "ECMA-376 Part 1, §19.2",
- note: "Slide list. show=\"0\" marks a hidden slide. Hidden is not deleted — the slide XML is still in the package.",
- send: "careful"
- }
+ "[Content_Types].xml": { title: "Content Types stream", spec: "ECMA-376 Part 2, §10.1.2", note: "Names the media type of every part. Office uses it to decide which XML vocabulary a part speaks. It is not itself a leak, but a missing or odd override is a sign the package was hand-edited.", send: "yes" },
+ "docProps/core.xml": { title: "Core properties", spec: "ECMA-376 Part 2, §11.1", note: "Dublin Core metadata: title, creator, lastModifiedBy, revision, created and modified. This is the usual place a previous client's name or your own name survives a Save As.", send: "no" },
+ "docProps/app.xml": { title: "Extended properties", spec: "ECMA-376 Part 1, §22.2", note: "Application, company, template name, and page or word counts. The template name often identifies the firm or the prior engagement the file was cloned from.", send: "careful" },
+ "docProps/thumbnail.jpeg": { title: "Package thumbnail", spec: "ECMA-376 Part 2, thumbnail relationship", note: "A JPEG preview of the document, stored even when you never inserted an image. It can show a page you have since deleted, and its EXIF can name the author.", send: "no" },
+ "word/document.xml": { title: "Main document part", spec: "ECMA-376 Part 1, §17.13", note: "The visible text, plus tracked insertions (w:ins), tracked deletions (w:del / w:delText) and comment markers. Accepting changes in Word is not the same as this XML being clean.", send: "careful" },
+ "word/comments.xml": { title: "Comments part", spec: "ECMA-376 Part 1, §17.13.4", note: "Comment authors, timestamps and the comment text. Deleting a comment in the UI does not always remove this part.", send: "no" },
+ "word/people.xml": { title: "People part", spec: "ECMA-376 people part", note: "Authors who have touched the document, sometimes with a provider user id or email. Easy to miss because Word never shows this file.", send: "no" },
+ "word/styles.xml": { title: "Styles part", spec: "ECMA-376 Part 1, §17.7", note: "Paragraph and character styles. Rarely a direct leak; style names copied from a client template can still identify the source firm.", send: "yes" },
+ "xl/workbook.xml": { title: "Workbook part", spec: "ECMA-376 Part 1, §18.2", note: "Sheet index. state=\"hidden\" or state=\"veryHidden\" marks a sheet the recipient will not see in the tab bar but can unhide.", send: "careful" },
+ "ppt/presentation.xml": { title: "Presentation part", spec: "ECMA-376 Part 1, §19.2", note: "Slide list. show=\"0\" marks a hidden slide. Hidden is not deleted — the slide XML is still in the package.", send: "careful" }
};
var treeEl = document.getElementById("tree");
@@ -620,17 +534,20 @@
var dropEl = document.getElementById("drop");
var fileEl = document.getElementById("file");
- function esc(s) {
- return String(s).replace(/&/g, "&").replace(//g, ">");
- }
+ function esc(s) { return String(s).replace(/&/g, "&").replace(//g, ">"); }
function u16(b, o) { return b[o] | (b[o + 1] << 8); }
function u32(b, o) { return (b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24)) >>> 0; }
+ function b64ToBytes(b64) {
+ var bin = atob(b64.replace(/\s+/g, ""));
+ var out = new Uint8Array(bin.length);
+ for (var i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
+ return out;
+ }
+
function findEocd(bytes) {
var start = Math.max(0, bytes.length - 22 - 65535);
- for (var i = bytes.length - 22; i >= start; i--) {
- if (u32(bytes, i) === 0x06054b50) return i;
- }
+ for (var i = bytes.length - 22; i >= start; i--) if (u32(bytes, i) === 0x06054b50) return i;
return -1;
}
@@ -642,9 +559,7 @@
var entries = [];
var p = cdOff;
for (var i = 0; i < count; i++) {
- if (p + 46 > bytes.length || u32(bytes, p) !== 0x02014b50) {
- throw new Error("Central directory is truncated or not a ZIP.");
- }
+ if (p + 46 > bytes.length || u32(bytes, p) !== 0x02014b50) throw new Error("Central directory is truncated or not a ZIP.");
var method = u16(bytes, p + 10);
var compSize = u32(bytes, p + 20);
var nameLen = u16(bytes, p + 28);
@@ -670,14 +585,11 @@
function readLocal(bytes, entry) {
var lp = entry.localOff;
- if (lp + 30 > bytes.length || u32(bytes, lp) !== 0x04034b50) {
- throw new Error("Local header missing for " + entry.name);
- }
+ if (lp + 30 > bytes.length || u32(bytes, lp) !== 0x04034b50) throw new Error("Local header missing for " + entry.name);
var method = u16(bytes, lp + 8);
var nlen = u16(bytes, lp + 26);
var elen = u16(bytes, lp + 28);
- var start = lp + 30 + nlen + elen;
- var comp = bytes.subarray(start, start + entry.compSize);
+ var comp = bytes.subarray(lp + 30 + nlen + elen, lp + 30 + nlen + elen + entry.compSize);
if (method === 0) return Promise.resolve(comp.slice());
if (method === 8) return inflateRaw(comp);
return Promise.reject(new Error(entry.name + " uses ZIP method " + method + ". Docscope only inflates stored (0) and deflate (8)."));
@@ -702,38 +614,28 @@
}
function asText(bytes) { return new TextDecoder("utf-8", { fatal: false }).decode(bytes); }
-
function parseXml(text) {
var doc = new DOMParser().parseFromString(text, "application/xml");
- if (doc.querySelector("parsererror")) return null;
- return doc;
+ return doc.querySelector("parsererror") ? null : doc;
}
-
function localName(el) { return el.localName || el.nodeName; }
-
function all(doc, name) {
if (!doc) return [];
- var out = [];
- var nodes = doc.getElementsByTagName("*");
+ var out = [], nodes = doc.getElementsByTagName("*");
for (var i = 0; i < nodes.length; i++) if (localName(nodes[i]) === name) out.push(nodes[i]);
return out;
}
-
function textOf(el) { return (el.textContent || "").replace(/\s+/g, " ").trim(); }
-
function attr(el, name) {
if (el.hasAttribute(name)) return el.getAttribute(name);
- var attrs = el.attributes;
- for (var i = 0; i < attrs.length; i++) {
- if (attrs[i].localName === name || attrs[i].name === name) return attrs[i].value;
+ for (var i = 0; i < el.attributes.length; i++) {
+ if (el.attributes[i].localName === name || el.attributes[i].name === name) return el.attributes[i].value;
}
return "";
}
-
function looksText(bytes) {
if (!bytes.length) return false;
- var n = Math.min(bytes.length, 800);
- var odd = 0;
+ var n = Math.min(bytes.length, 800), odd = 0;
for (var i = 0; i < n; i++) {
var c = bytes[i];
if (c === 0) return false;
@@ -741,13 +643,10 @@
}
return odd / n < 0.05;
}
-
function hexPreview(bytes) {
- var n = Math.min(bytes.length, 256);
- var lines = [];
+ var n = Math.min(bytes.length, 256), lines = [];
for (var i = 0; i < n; i += 16) {
- var hex = [];
- var asc = "";
+ var hex = [], asc = "";
for (var j = 0; j < 16 && i + j < n; j++) {
var c = bytes[i + j];
hex.push((c < 16 ? "0" : "") + c.toString(16));
@@ -769,15 +668,12 @@
if (marker === 0xd8 || marker === 0xd9) { o += 2; continue; }
var len = (bytes[o + 2] << 8) | bytes[o + 3];
if (len < 2 || o + 2 + len > bytes.length) break;
- if (marker === 0xe1 && asText(bytes.subarray(o + 4, o + 10)).indexOf("Exif") === 0) {
- tags = tags.concat(readTiff(bytes.subarray(o + 10, o + 2 + len)));
- }
+ if (marker === 0xe1 && asText(bytes.subarray(o + 4, o + 10)).indexOf("Exif") === 0) tags = tags.concat(readTiff(bytes.subarray(o + 10, o + 2 + len)));
o += 2 + len;
if (marker === 0xda) break;
}
return tags;
}
-
function readTiff(tiff) {
var out = [];
if (tiff.length < 8) return out;
@@ -786,9 +682,8 @@
if (!le && !be) return out;
function r16(off) { return le ? tiff[off] | (tiff[off + 1] << 8) : (tiff[off] << 8) | tiff[off + 1]; }
function r32(off) {
- return le
- ? (tiff[off] | (tiff[off + 1] << 8) | (tiff[off + 2] << 16) | (tiff[off + 3] << 24)) >>> 0
- : ((tiff[off] << 24) | (tiff[off + 1] << 16) | (tiff[off + 2] << 8) | tiff[off + 3]) >>> 0;
+ return le ? (tiff[off] | (tiff[off + 1] << 8) | (tiff[off + 2] << 16) | (tiff[off + 3] << 24)) >>> 0
+ : ((tiff[off] << 24) | (tiff[off + 1] << 16) | (tiff[off + 2] << 8) | tiff[off + 3]) >>> 0;
}
var names = { 0x010e: "ImageDescription", 0x010f: "Make", 0x0110: "Model", 0x0131: "Software", 0x0132: "DateTime", 0x013b: "Artist", 0x8298: "Copyright", 0x9003: "DateTimeOriginal" };
function walk(ifd, depth) {
@@ -797,9 +692,7 @@
for (var i = 0; i < n; i++) {
var e = ifd + 2 + i * 12;
if (e + 12 > tiff.length) break;
- var tag = r16(e);
- var typ = r16(e + 2);
- var count = r32(e + 4);
+ var tag = r16(e), typ = r16(e + 2), count = r32(e + 4);
if (typ === 2 && count > 1 && count < 400) {
var start = count <= 4 ? e + 8 : r32(e + 8);
if (start >= 0 && start + count <= tiff.length) {
@@ -816,7 +709,6 @@
walk(r32(4), 0);
return out;
}
-
function readPngText(bytes) {
var out = [];
if (bytes.length < 8 || bytes[0] !== 0x89 || bytes[1] !== 0x50) return out;
@@ -835,21 +727,18 @@
}
return out;
}
-
function partByName(parts, name) {
for (var i = 0; i < parts.length; i++) if (parts[i].name === name) return parts[i];
return null;
}
function analyze(pkg) {
- var findings = [];
- var flags = {};
+ var findings = [], flags = {};
function flag(name, sev) { if (!flags[name] || sev === "leak") flags[name] = sev; }
function add(sev, label, html, partName) {
- findings.push({ sev: sev, label: label, html: html, part: partName });
+ findings.push({ sev: sev, label: label, html: html });
if (partName) flag(partName, sev);
}
-
var core = partByName(pkg.parts, "docProps/core.xml");
if (core) {
var cdoc = parseXml(asText(core.bytes));
@@ -861,9 +750,7 @@
if (modifiedBy.length) add("leak", "Last modified by", "cp:lastModifiedBy is " + esc(modifiedBy.join(", ")) + "" + (modified.length ? " (" + esc(modified.join(", ")) + ")" : "") + ".", "docProps/core.xml");
if (title.length) add("info", "Title", "dc:title is “" + esc(title.join(" / ")) + "”.", "docProps/core.xml");
if (!creator.length && !modifiedBy.length) add("ok", "Core properties", "No creator or lastModifiedBy in docProps/core.xml.", "docProps/core.xml");
- } else {
- add("ok", "Core properties", "No docProps/core.xml in this package.");
- }
+ } else add("ok", "Core properties", "No docProps/core.xml in this package.");
var app = partByName(pkg.parts, "docProps/app.xml");
if (app) {
@@ -880,41 +767,26 @@
if (text.indexOf("<") === -1) return;
var doc = parseXml(text);
if (!doc) return;
- var ins = all(doc, "ins");
- var del = all(doc, "del");
+ var ins = all(doc, "ins"), del = all(doc, "del");
if (ins.length || del.length) {
var bits = [];
- ins.slice(0, 6).forEach(function (el) {
- bits.push("insertion by " + esc(attr(el, "author") || "unknown") + (textOf(el) ? ": “" + esc(textOf(el).slice(0, 140)) + "”" : ""));
- });
- del.slice(0, 6).forEach(function (el) {
- bits.push("deletion by " + esc(attr(el, "author") || "unknown") + (textOf(el) ? ": “" + esc(textOf(el).slice(0, 140)) + "”" : ""));
- });
+ ins.slice(0, 6).forEach(function (el) { bits.push("insertion by " + esc(attr(el, "author") || "unknown") + (textOf(el) ? ": “" + esc(textOf(el).slice(0, 140)) + "”" : "")); });
+ del.slice(0, 6).forEach(function (el) { bits.push("deletion by " + esc(attr(el, "author") || "unknown") + (textOf(el) ? ": “" + esc(textOf(el).slice(0, 140)) + "”" : "")); });
add("leak", "Tracked changes", ins.length + " insertion" + (ins.length === 1 ? "" : "s") + " (w:ins) and " + del.length + " deletion" + (del.length === 1 ? "" : "s") + " (w:del) still in " + esc(part.name) + ". " + bits.join("; ") + ".", part.name);
}
var comments = all(doc, "comment").filter(function (el) { return attr(el, "author") || textOf(el); });
if (comments.length && /comment/i.test(part.name)) {
- var lines = comments.slice(0, 8).map(function (el) {
+ add("leak", "Comments", comments.length + " comment" + (comments.length === 1 ? "" : "s") + " in " + esc(part.name) + ". " + comments.slice(0, 8).map(function (el) {
return esc(attr(el, "author") || "unknown") + ": “" + esc(textOf(el).slice(0, 160)) + "”";
- });
- add("leak", "Comments", comments.length + " comment" + (comments.length === 1 ? "" : "s") + " in " + esc(part.name) + ". " + lines.join(" · "), part.name);
+ }).join(" · "), part.name);
}
if (/people\.xml$/i.test(part.name)) {
var people = all(doc, "person");
- if (people.length) {
- add("leak", "People", people.length + " person record" + (people.length === 1 ? "" : "s") + " in people.xml: " + esc(people.map(function (el) { return attr(el, "author") || textOf(el); }).filter(Boolean).join(", ")) + ".", part.name);
- }
+ if (people.length) add("leak", "People", people.length + " person record" + (people.length === 1 ? "" : "s") + " in people.xml: " + esc(people.map(function (el) { return attr(el, "author") || textOf(el); }).filter(Boolean).join(", ")) + ".", part.name);
}
if (/workbook\.xml$/i.test(part.name)) {
- var hidden = all(doc, "sheet").filter(function (el) {
- var st = (attr(el, "state") || "").toLowerCase();
- return st === "hidden" || st === "veryhidden";
- });
- if (hidden.length) {
- add("leak", "Hidden sheets", hidden.map(function (el) {
- return esc(attr(el, "name") || "(unnamed)") + " (" + esc(attr(el, "state")) + ")";
- }).join(", ") + " — still in the workbook, just not on the tab bar.", part.name);
- }
+ var hidden = all(doc, "sheet").filter(function (el) { var st = (attr(el, "state") || "").toLowerCase(); return st === "hidden" || st === "veryhidden"; });
+ if (hidden.length) add("leak", "Hidden sheets", hidden.map(function (el) { return esc(attr(el, "name") || "(unnamed)") + " (" + esc(attr(el, "state")) + ")"; }).join(", ") + " — still in the workbook, just not on the tab bar.", part.name);
}
if (/presentation\.xml$/i.test(part.name)) {
var sld = all(doc, "sldId").filter(function (el) { return attr(el, "show") === "0"; });
@@ -925,9 +797,7 @@
pkg.parts.forEach(function (part) {
if (part.dir) return;
var lower = part.name.toLowerCase();
- if (/thumbnail/.test(lower)) {
- add("leak", "Thumbnail", "Embedded thumbnail preview at " + esc(part.name) + " (" + part.size + " bytes). Recipients can open it without Word.", part.name);
- }
+ if (/thumbnail/.test(lower)) add("leak", "Thumbnail", "Embedded thumbnail preview at " + esc(part.name) + " (" + part.size + " bytes). Recipients can open it without Word.", part.name);
if (/\.jpe?g$/.test(lower)) {
var exif = readExif(part.bytes);
if (exif.length) add("leak", "EXIF", esc(part.name) + " carries " + exif.map(function (t) { return esc(t.tag) + "=" + esc(t.value); }).join(", ") + ".", part.name);
@@ -937,7 +807,6 @@
if (pngText.length) add("warn", "PNG text", esc(part.name) + ": " + pngText.map(function (t) { return esc(t.key) + "=" + esc(t.value); }).join(", ") + ".", part.name);
}
});
-
if (!findings.some(function (f) { return f.sev === "leak"; })) {
add("ok", "No leak flags", "Nothing in the handled parts matched author names, tracked changes, comments, people, thumbnails, EXIF, PNG text, or hidden sheets/slides. Unhandled parts are still listed in the diagram.");
}
@@ -954,9 +823,8 @@
if (known) return known;
if (/\.rels$/i.test(part.name)) return { title: "Relationships part", spec: "ECMA-376 Part 2, §9.2", note: "Points from one part to another (styles, comments, images, the thumbnail). A thumbnail relationship here is how Explorer finds the preview.", send: "careful" };
if (/media\//i.test(part.name)) return { title: "Embedded media", spec: "ECMA-376 Part 1, §15.2.14", note: "A binary file stored inside the package. Images can carry EXIF or PNG text even when the slide looks clean.", send: "careful" };
- return { title: "Unhandled part", spec: "", note: "This page lists the part because it is in the ZIP central directory, but it does not interpret this vocabulary. The bytes are shown as-is.", send: "careful" };
+ return { title: "Unhandled part", spec: "", note: "Listed because it is in the ZIP central directory. This page does not interpret this vocabulary; the bytes are shown as-is.", send: "careful" };
}
-
function verdictLabel(send, flagged) {
if (flagged === "leak" || send === "no") return { cls: "no", text: "Not safe to send as-is" };
if (flagged === "warn" || send === "careful") return { cls: "careful", text: "Check before you send" };
@@ -990,18 +858,14 @@
treeEl.innerHTML = "" + pkg.parts.length + " central-directory entries, shown verbatim. Red marks a part the leak report cited.
" + walk(root);
var buttons = treeEl.querySelectorAll(".part");
for (var i = 0; i < buttons.length; i++) {
- buttons[i].addEventListener("click", function (ev) {
- showPart(pkg, flags, Number(ev.currentTarget.getAttribute("data-index")));
- });
+ buttons[i].addEventListener("click", function (ev) { showPart(pkg, flags, Number(ev.currentTarget.getAttribute("data-index"))); });
}
}
-
function renderReport(findings) {
reportEl.innerHTML = "" + findings.map(function (f) {
return "
" + esc(f.label) + "" + f.html + "
";
}).join("") + "
";
}
-
function showPart(pkg, flags, index) {
var part = pkg.parts[index];
if (!part) return;
@@ -1016,12 +880,10 @@
for (var i = 0; i < buttons.length; i++) buttons[i].classList.toggle("on", Number(buttons[i].getAttribute("data-index")) === index);
statusRight.textContent = part.name + " · " + part.size + " bytes · method " + part.method;
}
-
function renderPackage(pkg, fileName, note) {
var report = analyze(pkg);
captionEl.textContent = "Docscope — " + fileName;
- var names = pkg.entries.map(function (e) { return e.name; });
- metaEl.textContent = fileName + " · " + pkg.entries.length + " central-directory entries · " + names.join(" · ") + (note ? " — " + note : "");
+ metaEl.textContent = fileName + " · " + pkg.entries.length + " central-directory entries · " + pkg.entries.map(function (e) { return e.name; }).join(" · ") + (note ? " — " + note : "");
statusLeft.textContent = fileName + " · " + pkg.parts.length + " parts · parsed locally";
renderTree(pkg, report.flags);
renderReport(report.findings);
@@ -1032,17 +894,14 @@
});
if (first >= 0) showPart(pkg, report.flags, first);
}
-
function fail(err) {
metaEl.innerHTML = "" + esc(err && err.message ? err.message : String(err)) + "";
statusLeft.textContent = "could not read that file";
}
-
function loadBytes(bytes, name, note) {
metaEl.textContent = "Unzipping " + name + "…";
- unzipPackage(bytes).then(function (pkg) { renderPackage(pkg, name, note); }).catch(fail);
+ return unzipPackage(bytes).then(function (pkg) { renderPackage(pkg, name, note); return pkg; }).catch(fail);
}
-
function onFile(file) {
if (!file) return;
var name = file.name || "dropped-file";
@@ -1062,11 +921,11 @@
});
fileEl.addEventListener("change", function () { onFile(fileEl.files && fileEl.files[0]); });
- fetch("sample.docx").then(function (res) {
- if (!res.ok) throw new Error("Could not load the preloaded sample.docx (" + res.status + "). Drop a file instead.");
- return res.arrayBuffer();
- }).then(function (buf) {
- loadBytes(new Uint8Array(buf), SAMPLE_NAME, SAMPLE_NOTE);
+ fetch("sample.b64").then(function (res) {
+ if (!res.ok) throw new Error("Could not load the preloaded sample (" + res.status + "). Drop a file instead.");
+ return res.text();
+ }).then(function (text) {
+ return loadBytes(b64ToBytes(text), SAMPLE_NAME, SAMPLE_NOTE);
}).catch(fail);
})();