memory-extract 0.1.2 → 0.1.4
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/package.json +1 -2
- package/tools/play.mjs +84 -64
- package/tools/playMemoryServer.mjs +193 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "memory-extract",
|
|
3
3
|
"description": "Tool for embedding data into PNG images",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.4",
|
|
6
6
|
"author": "semigarden",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
"pack": "node tools/pack.mjs",
|
|
30
30
|
"unpack": "node tools/unpack.mjs",
|
|
31
31
|
"play": "node tools/play.mjs",
|
|
32
|
-
"build:launcher": "node tools/buildPlayLauncher.mjs",
|
|
33
32
|
"test": "node --test test/memory.test.mjs"
|
|
34
33
|
}
|
|
35
34
|
}
|
package/tools/play.mjs
CHANGED
|
@@ -6,10 +6,12 @@ import os from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
8
|
import { resolveProjectPath } from "./hostProject.mjs";
|
|
9
|
-
import { extractMemory } from "./memoryFormat.mjs";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
import { extractMemory, listManifestFiles, readMemoryFile } from "./memoryFormat.mjs";
|
|
10
|
+
import { resolveMemoryPlayMode } from "./playMemory.mjs";
|
|
11
|
+
import {
|
|
12
|
+
startMemoryBrowseServer,
|
|
13
|
+
startMemoryPlayServer,
|
|
14
|
+
} from "./playMemoryServer.mjs";
|
|
13
15
|
|
|
14
16
|
export const resolvePlayEntry = (manifest, filePaths) => {
|
|
15
17
|
if (filePaths.includes(manifest.entry)) {
|
|
@@ -25,59 +27,6 @@ export const resolvePlayEntry = (manifest, filePaths) => {
|
|
|
25
27
|
|
|
26
28
|
export { resolveMemoryPlayMode } from "./playMemory.mjs";
|
|
27
29
|
|
|
28
|
-
export const buildPlayLauncherPage = (pngBuffer, bundleScript) => `<!DOCTYPE html>
|
|
29
|
-
<html lang="en">
|
|
30
|
-
<head>
|
|
31
|
-
<meta charset="utf-8" />
|
|
32
|
-
<title>Loading memory...</title>
|
|
33
|
-
<style>
|
|
34
|
-
body {
|
|
35
|
-
font-family: system-ui, sans-serif;
|
|
36
|
-
margin: 2rem;
|
|
37
|
-
}
|
|
38
|
-
</style>
|
|
39
|
-
</head>
|
|
40
|
-
<body>
|
|
41
|
-
<p id="status">Loading memory...</p>
|
|
42
|
-
<script>${bundleScript}</script>
|
|
43
|
-
<script>
|
|
44
|
-
(async () => {
|
|
45
|
-
const status = document.getElementById("status");
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const pngBytes = Uint8Array.from(
|
|
49
|
-
atob(${JSON.stringify(pngBuffer.toString("base64"))}),
|
|
50
|
-
(char) => char.charCodeAt(0)
|
|
51
|
-
);
|
|
52
|
-
const png = new Blob([pngBytes], { type: "image/png" });
|
|
53
|
-
const launch = await MemoryExtract.createMemoryBlob(png);
|
|
54
|
-
location.replace(URL.createObjectURL(launch));
|
|
55
|
-
} catch (error) {
|
|
56
|
-
status.textContent = error.message ?? "Failed to launch memory.";
|
|
57
|
-
}
|
|
58
|
-
})();
|
|
59
|
-
</script>
|
|
60
|
-
</body>
|
|
61
|
-
</html>
|
|
62
|
-
`;
|
|
63
|
-
|
|
64
|
-
export const writePlayLauncherPage = async (pngBuffer, bundleScript = "") => {
|
|
65
|
-
const script = bundleScript || await readFile(PLAY_BUNDLE_PATH, "utf8");
|
|
66
|
-
const tempDir = await mkdtemp(path.join(os.tmpdir(), "memory-play-"));
|
|
67
|
-
const pagePath = path.join(tempDir, "index.html");
|
|
68
|
-
|
|
69
|
-
await writeFile(
|
|
70
|
-
pagePath,
|
|
71
|
-
buildPlayLauncherPage(pngBuffer, script),
|
|
72
|
-
"utf8"
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
pagePath,
|
|
77
|
-
pageUrl: pathToFileURL(pagePath).href,
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
|
|
81
30
|
export const isCliEntry = (entryArg = process.argv[1]) => {
|
|
82
31
|
try {
|
|
83
32
|
return (
|
|
@@ -141,23 +90,94 @@ export const resolvePlayInput = (argv, projectPath = process.cwd()) => {
|
|
|
141
90
|
return resolveProjectPath(projectPath, input);
|
|
142
91
|
};
|
|
143
92
|
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
93
|
+
const waitForShutdown = (server) =>
|
|
94
|
+
new Promise((resolve) => {
|
|
95
|
+
const shutdown = () => {
|
|
96
|
+
server.close(() => resolve());
|
|
97
|
+
};
|
|
147
98
|
|
|
148
|
-
|
|
99
|
+
process.once("SIGINT", shutdown);
|
|
100
|
+
process.once("SIGTERM", shutdown);
|
|
101
|
+
});
|
|
149
102
|
|
|
103
|
+
const runServerSession = async ({ input, label, openUrl, server }) => {
|
|
150
104
|
console.log(`Playing memory from ${input}`);
|
|
151
|
-
console.log(`
|
|
105
|
+
console.log(`Local server: ${openUrl}`);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await openBrowser(openUrl);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.warn(`Could not open browser: ${error.message ?? error}`);
|
|
111
|
+
console.warn(`Open this URL manually: ${openUrl}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log("Press Ctrl+C to stop.");
|
|
115
|
+
await waitForShutdown(server);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const openSingleFile = async (manifest, fileBytes, filePath) => {
|
|
119
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), "memory-play-file-"));
|
|
120
|
+
const outPath = path.join(tempDir, path.basename(filePath));
|
|
121
|
+
await writeFile(outPath, readMemoryFile(manifest, filePath, fileBytes));
|
|
122
|
+
|
|
123
|
+
const fileUrl = pathToFileURL(outPath).href;
|
|
124
|
+
|
|
125
|
+
console.log(`Opening ${filePath} from memory`);
|
|
126
|
+
console.log(`File: ${outPath}`);
|
|
152
127
|
|
|
153
128
|
try {
|
|
154
|
-
await openBrowser(
|
|
129
|
+
await openBrowser(fileUrl);
|
|
155
130
|
} catch (error) {
|
|
156
131
|
console.warn(`Could not open browser: ${error.message ?? error}`);
|
|
157
|
-
console.warn(`Open this file manually: ${
|
|
132
|
+
console.warn(`Open this file manually: ${outPath}`);
|
|
158
133
|
}
|
|
159
134
|
};
|
|
160
135
|
|
|
136
|
+
const playMemoryPng = async (input, label = input) => {
|
|
137
|
+
const pngBuffer = await readFile(input);
|
|
138
|
+
ensureMemoryPng(pngBuffer, label);
|
|
139
|
+
|
|
140
|
+
const { manifest, fileBytes } = extractMemory(pngBuffer);
|
|
141
|
+
const filePaths = listManifestFiles(manifest);
|
|
142
|
+
const playMode = resolveMemoryPlayMode(manifest, filePaths);
|
|
143
|
+
|
|
144
|
+
if (playMode.mode === "file") {
|
|
145
|
+
await openSingleFile(manifest, fileBytes, playMode.path);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (playMode.mode === "play") {
|
|
150
|
+
const { server, baseUrl } = await startMemoryPlayServer(
|
|
151
|
+
manifest,
|
|
152
|
+
fileBytes,
|
|
153
|
+
filePaths,
|
|
154
|
+
playMode.entry
|
|
155
|
+
);
|
|
156
|
+
const openUrl = baseUrl;
|
|
157
|
+
|
|
158
|
+
await runServerSession({
|
|
159
|
+
input,
|
|
160
|
+
label,
|
|
161
|
+
openUrl,
|
|
162
|
+
server,
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const { server, baseUrl } = await startMemoryBrowseServer(
|
|
168
|
+
manifest,
|
|
169
|
+
fileBytes,
|
|
170
|
+
filePaths
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
await runServerSession({
|
|
174
|
+
input,
|
|
175
|
+
label,
|
|
176
|
+
openUrl: baseUrl,
|
|
177
|
+
server,
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
161
181
|
const main = async () => {
|
|
162
182
|
const projectPath = process.cwd();
|
|
163
183
|
const argv = process.argv.slice(2);
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { guessMimeType } from "../src/payloadFormat.js";
|
|
4
|
+
import {
|
|
5
|
+
buildBrowseListingDocument,
|
|
6
|
+
listVirtualEntries,
|
|
7
|
+
} from "../src/memoryPlay.js";
|
|
8
|
+
import { listManifestFiles, readMemoryFile } from "./memoryFormat.mjs";
|
|
9
|
+
|
|
10
|
+
export const normalizeUrlPath = (pathname) => {
|
|
11
|
+
const decoded = decodeURIComponent(pathname || "/");
|
|
12
|
+
const trimmed = decoded.replace(/\/+$/, "");
|
|
13
|
+
return trimmed || "/";
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const urlPathToManifestPath = (urlPath) =>
|
|
17
|
+
urlPath === "/" ? "" : urlPath.slice(1);
|
|
18
|
+
|
|
19
|
+
export const resolveEntryMountPrefix = (entryPath) => {
|
|
20
|
+
const slash = entryPath.lastIndexOf("/");
|
|
21
|
+
if (slash <= 0) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return entryPath.slice(0, slash + 1);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const hrefForEntry = (urlPath, name, isDirectory) => {
|
|
29
|
+
const base = urlPath === "/" ? "" : urlPath;
|
|
30
|
+
const href = `${base}/${name}${isDirectory ? "/" : ""}`.replace(/\/+/g, "/");
|
|
31
|
+
return encodeURI(href);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const resolveManifestPathForUrl = (urlPath, manifestPaths, entryPath = "") => {
|
|
35
|
+
const manifestSet = new Set(manifestPaths);
|
|
36
|
+
const direct = urlPathToManifestPath(urlPath);
|
|
37
|
+
|
|
38
|
+
if (direct && manifestSet.has(direct)) {
|
|
39
|
+
return direct;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const mountPrefix = resolveEntryMountPrefix(entryPath);
|
|
43
|
+
|
|
44
|
+
if (direct && mountPrefix) {
|
|
45
|
+
const mounted = `${mountPrefix}${direct}`.replace(/\/+/g, "/");
|
|
46
|
+
if (manifestSet.has(mounted)) {
|
|
47
|
+
return mounted;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const startHttpServer = async (handler) => {
|
|
55
|
+
const server = http.createServer(handler);
|
|
56
|
+
|
|
57
|
+
await new Promise((resolve, reject) => {
|
|
58
|
+
server.once("error", reject);
|
|
59
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const { port } = server.address();
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
server,
|
|
66
|
+
baseUrl: `http://127.0.0.1:${port}/`,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const serveManifestFile = (response, manifest, fileBytes, manifestPath) => {
|
|
71
|
+
const body = readMemoryFile(manifest, manifestPath, fileBytes);
|
|
72
|
+
response.writeHead(200, {
|
|
73
|
+
"Content-Type": guessMimeType(manifestPath),
|
|
74
|
+
});
|
|
75
|
+
response.end(body);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const isHtmlEntryPath = (entryPath) =>
|
|
79
|
+
entryPath.endsWith(".html") || entryPath.endsWith(".htm");
|
|
80
|
+
|
|
81
|
+
export const createMemoryPlayHandler = (manifest, fileBytes, filePaths, entryPath) => {
|
|
82
|
+
const manifestPaths = filePaths ?? listManifestFiles(manifest);
|
|
83
|
+
const mountPrefix = resolveEntryMountPrefix(entryPath);
|
|
84
|
+
|
|
85
|
+
return (request, response) => {
|
|
86
|
+
try {
|
|
87
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
88
|
+
const urlPath = normalizeUrlPath(url.pathname);
|
|
89
|
+
|
|
90
|
+
if (mountPrefix) {
|
|
91
|
+
const mountRoot = `/${mountPrefix.replace(/\/$/, "")}`;
|
|
92
|
+
|
|
93
|
+
if (urlPath === mountRoot || urlPath.startsWith(`${mountRoot}/`)) {
|
|
94
|
+
const relative =
|
|
95
|
+
urlPath === mountRoot ? "/" : urlPath.slice(mountRoot.length);
|
|
96
|
+
response.writeHead(302, {
|
|
97
|
+
Location: relative.startsWith("/") ? relative : `/${relative}`,
|
|
98
|
+
});
|
|
99
|
+
response.end();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let manifestPath = null;
|
|
105
|
+
|
|
106
|
+
if (urlPath === "/") {
|
|
107
|
+
manifestPath = entryPath;
|
|
108
|
+
} else {
|
|
109
|
+
manifestPath = resolveManifestPathForUrl(
|
|
110
|
+
urlPath,
|
|
111
|
+
manifestPaths,
|
|
112
|
+
entryPath
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (
|
|
117
|
+
!manifestPath &&
|
|
118
|
+
isHtmlEntryPath(entryPath) &&
|
|
119
|
+
request.method === "GET"
|
|
120
|
+
) {
|
|
121
|
+
manifestPath = entryPath;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!manifestPath) {
|
|
125
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
126
|
+
response.end("Not found");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
serveManifestFile(response, manifest, fileBytes, manifestPath);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
133
|
+
response.end(error.message ?? "Not found");
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const createMemoryBrowseHandler = (manifest, fileBytes, filePaths) => {
|
|
139
|
+
const manifestPaths = filePaths ?? listManifestFiles(manifest);
|
|
140
|
+
|
|
141
|
+
return (request, response) => {
|
|
142
|
+
try {
|
|
143
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
144
|
+
const urlPath = normalizeUrlPath(url.pathname);
|
|
145
|
+
const manifestPath = resolveManifestPathForUrl(urlPath, manifestPaths);
|
|
146
|
+
|
|
147
|
+
if (manifestPath) {
|
|
148
|
+
serveManifestFile(response, manifest, fileBytes, manifestPath);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const entries = listVirtualEntries(manifestPaths, urlPath);
|
|
153
|
+
|
|
154
|
+
if (entries.length === 0) {
|
|
155
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
156
|
+
response.end("Not found");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const listingEntries = entries.map((entry) => ({
|
|
161
|
+
name: entry.name,
|
|
162
|
+
isDirectory: entry.isDirectory,
|
|
163
|
+
href: hrefForEntry(urlPath, entry.name, entry.isDirectory),
|
|
164
|
+
}));
|
|
165
|
+
const parentHref =
|
|
166
|
+
urlPath === "/"
|
|
167
|
+
? null
|
|
168
|
+
: encodeURI(`${normalizeUrlPath(path.posix.dirname(urlPath) || "/")}/`.replace(/\/+/g, "/"));
|
|
169
|
+
const html = buildBrowseListingDocument(urlPath, listingEntries, {
|
|
170
|
+
showParent: urlPath !== "/",
|
|
171
|
+
parentHref: parentHref ?? "../",
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
response.writeHead(200, {
|
|
175
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
176
|
+
});
|
|
177
|
+
response.end(html);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
180
|
+
response.end(error.message ?? "Not found");
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export const startMemoryPlayServer = async (manifest, fileBytes, filePaths, entryPath) => {
|
|
186
|
+
const handler = createMemoryPlayHandler(manifest, fileBytes, filePaths, entryPath);
|
|
187
|
+
return startHttpServer(handler);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const startMemoryBrowseServer = async (manifest, fileBytes, filePaths) => {
|
|
191
|
+
const handler = createMemoryBrowseHandler(manifest, fileBytes, filePaths);
|
|
192
|
+
return startHttpServer(handler);
|
|
193
|
+
};
|