@next2d/builder 0.1.11 → 0.3.0
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/README.md +90 -57
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +68 -3
- package/dist/colors.d.ts +6 -0
- package/dist/colors.js +7 -0
- package/dist/context.d.ts +10 -1
- package/dist/context.js +7 -0
- package/dist/description.d.ts +2 -0
- package/dist/description.js +8 -0
- package/dist/electron-config.d.ts +62 -0
- package/dist/electron-config.js +134 -0
- package/dist/electron-host.d.ts +7 -0
- package/dist/electron-host.js +54 -0
- package/dist/electron-native.d.ts +15 -0
- package/dist/electron-native.js +73 -0
- package/dist/electron.d.ts +4 -9
- package/dist/electron.js +85 -133
- package/dist/index.js +21 -0
- package/dist/native.d.ts +9 -24
- package/dist/native.js +48 -106
- package/dist/project-vite.d.ts +3 -0
- package/dist/project-vite.js +21 -0
- package/dist/resolve-tool-packages.d.ts +1 -0
- package/dist/resolve-tool-packages.js +29 -0
- package/dist/steam-upload.d.ts +27 -0
- package/dist/steam-upload.js +131 -0
- package/dist/steam.d.ts +22 -0
- package/dist/steam.js +193 -0
- package/dist/tool-packages.d.ts +15 -0
- package/dist/tool-packages.js +74 -0
- package/dist/web.js +10 -4
- package/dist/xbox.d.ts +1 -1
- package/dist/xbox.js +60 -57
- package/docs/capacitor.md +201 -0
- package/docs/native-bridge.md +147 -0
- package/docs/steam.md +384 -0
- package/docs/xbox.md +145 -0
- package/package.json +9 -12
- package/templates/electron/entitlements.plist +12 -0
- package/templates/electron/icons/icon.icns +0 -0
- package/templates/electron/icons/icon.ico +0 -0
- package/templates/electron/icons/icon.png +0 -0
- package/templates/electron/index.js +119 -0
- package/templates/electron/local-assets.cjs +24 -0
- package/templates/electron/native-bridge.cjs +98 -0
- package/templates/electron/package.json +11 -0
- package/templates/electron/preload.cjs +11 -0
- package/templates/xbox/src/platform/StbTextRasterizer.cpp +14 -10
- package/templates/xbox/tests/check_local.sh +12 -4
- package/templates/xbox/tests/stb_safety_test.cpp +126 -0
- package/templates/xbox/third_party/LICENSES.md +15 -0
- package/templates/xbox/third_party/stb_image.h +15 -10
- package/templates/xbox/third_party/stb_truetype.h +4 -4
- package/templates/xbox/third_party/stb_vorbis.c +1 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const { app, BrowserWindow, Menu, net, protocol, screen, session, ipcMain } = require("electron");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const { pathToFileURL } = require("node:url");
|
|
5
|
+
const { resolveAssetPath, CSP } = require("./local-assets.cjs");
|
|
6
|
+
const config = require("./runtime-config.json");
|
|
7
|
+
const { NativeBridge, isTrustedSender } = require("./native-bridge.cjs");
|
|
8
|
+
let nativeBridge;
|
|
9
|
+
const closeNativeBridge = () => nativeBridge?.close();
|
|
10
|
+
process.on("exit", closeNativeBridge);
|
|
11
|
+
app.on("before-quit", closeNativeBridge);
|
|
12
|
+
// Keep saves in a stable user directory even if the visible game name changes.
|
|
13
|
+
app.setName(config.appName);
|
|
14
|
+
const userDataOverride = app.commandLine.getSwitchValue("user-data-dir");
|
|
15
|
+
app.setPath("userData", userDataOverride ? path.resolve(userDataOverride) : path.join(app.getPath("appData"), config.appId));
|
|
16
|
+
const assetRoot = app.isPackaged
|
|
17
|
+
? path.join(process.resourcesPath, "resources")
|
|
18
|
+
: path.join(__dirname, "resources");
|
|
19
|
+
|
|
20
|
+
// A fixed secure origin supports fetch, module scripts, workers and localStorage.
|
|
21
|
+
protocol.registerSchemesAsPrivileged([{
|
|
22
|
+
scheme: "next2d",
|
|
23
|
+
privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, stream: true }
|
|
24
|
+
}]);
|
|
25
|
+
|
|
26
|
+
const startURL = "next2d://game/index.html";
|
|
27
|
+
let mainWindow;
|
|
28
|
+
const createWindow = async () => {
|
|
29
|
+
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
|
|
30
|
+
mainWindow = new BrowserWindow({
|
|
31
|
+
width: Math.min(config.window.width, width),
|
|
32
|
+
height: Math.min(config.window.height, height),
|
|
33
|
+
title: config.appName,
|
|
34
|
+
fullscreen: config.window.fullscreen,
|
|
35
|
+
...(config.icon ? { icon: path.join(assetRoot, config.icon) } : {}),
|
|
36
|
+
useContentSize: true,
|
|
37
|
+
backgroundColor: "#000000",
|
|
38
|
+
autoHideMenuBar: true,
|
|
39
|
+
show: false,
|
|
40
|
+
webPreferences: {
|
|
41
|
+
nodeIntegration: false, contextIsolation: true, sandbox: true,
|
|
42
|
+
...(config.nativeBridge ? { preload: path.join(__dirname, "preload.cjs"),
|
|
43
|
+
backgroundThrottling: config.nativeBridge.backgroundThrottling ?? false } : {})
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
mainWindow.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
|
47
|
+
mainWindow.webContents.on("will-navigate", (event, url) => {
|
|
48
|
+
if (!resolveAssetPath(assetRoot, url)) {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
mainWindow.webContents.on("before-input-event", (event, input) => {
|
|
53
|
+
if (input.type !== "keyDown") {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (input.key === "F11" || (input.alt && input.key === "Enter")) {
|
|
57
|
+
mainWindow.setFullScreen(!mainWindow.isFullScreen());
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
} else if (input.key === "Escape" && mainWindow.isFullScreen()) {
|
|
60
|
+
mainWindow.setFullScreen(false);
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
mainWindow.webContents.on("render-process-gone", (_event, details) => {
|
|
65
|
+
console.error("Renderer exited:", details.reason);
|
|
66
|
+
closeNativeBridge();
|
|
67
|
+
app.exit(1);
|
|
68
|
+
});
|
|
69
|
+
mainWindow.once("ready-to-show", () => mainWindow.show());
|
|
70
|
+
await mainWindow.loadURL(startURL);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
app.whenReady().then(async () => {
|
|
74
|
+
Menu.setApplicationMenu(process.platform === "darwin" ? Menu.buildFromTemplate([
|
|
75
|
+
{ role: "appMenu" }, { role: "editMenu" }, { role: "windowMenu" }
|
|
76
|
+
]) : null);
|
|
77
|
+
session.defaultSession.setPermissionRequestHandler((_contents, permission, callback) => {
|
|
78
|
+
callback(permission === "fullscreen");
|
|
79
|
+
});
|
|
80
|
+
session.defaultSession.setPermissionCheckHandler((_contents, permission) => permission === "fullscreen");
|
|
81
|
+
protocol.handle("next2d", async (request) => {
|
|
82
|
+
let asset = resolveAssetPath(assetRoot, request.url);
|
|
83
|
+
if (!asset || request.method !== "GET") {
|
|
84
|
+
return new Response("Not found", { status: 404 });
|
|
85
|
+
}
|
|
86
|
+
// Framework SPA routes use history.pushState (e.g. /home); reload the entry.
|
|
87
|
+
if (!path.extname(asset) && !fs.existsSync(asset)) {
|
|
88
|
+
asset = path.join(assetRoot, "index.html");
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const response = await net.fetch(pathToFileURL(asset).href);
|
|
92
|
+
const headers = new Headers(response.headers);
|
|
93
|
+
headers.set("Content-Security-Policy", CSP);
|
|
94
|
+
return new Response(response.body, { status: response.status, headers });
|
|
95
|
+
} catch {
|
|
96
|
+
return new Response("Not found", { status: 404 });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
if (config.nativeBridge) {
|
|
100
|
+
ipcMain.handle("next2d:native:request", (event, method, params) => {
|
|
101
|
+
if (!isTrustedSender(event, mainWindow)) throw new Error("Native bridge access denied");
|
|
102
|
+
if (!config.nativeBridge.methods.includes(method)) throw new Error("Native method not allowed");
|
|
103
|
+
if (!nativeBridge) {
|
|
104
|
+
const nativeRoot = path.join(app.isPackaged ? process.resourcesPath : __dirname, "native");
|
|
105
|
+
nativeBridge = new NativeBridge(path.join(nativeRoot, config.nativeBridge.executable), config.nativeBridge.methods, (message) => {
|
|
106
|
+
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send("next2d:native:event", message);
|
|
107
|
+
}, { cwd: nativeRoot });
|
|
108
|
+
}
|
|
109
|
+
return nativeBridge.request(method, params);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
await createWindow();
|
|
113
|
+
}).catch((error) => {
|
|
114
|
+
console.error(error);
|
|
115
|
+
app.exit(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Closing the game must stop Steam's "Running" status, including on macOS.
|
|
119
|
+
app.on("window-all-closed", () => app.quit());
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const path = require("node:path");
|
|
2
|
+
|
|
3
|
+
const CSP = "default-src 'self' data: blob:; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'none'";
|
|
4
|
+
const resolveAssetPath = (root, requestURL) => {
|
|
5
|
+
try {
|
|
6
|
+
const url = new URL(requestURL);
|
|
7
|
+
if (url.protocol !== "next2d:" || url.host !== "game" || url.username || url.password) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const name = decodeURIComponent(url.pathname);
|
|
11
|
+
if (name.includes("\0") || name.includes("\\")) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const file = path.resolve(root, `.${name === "/" ? "/index.html" : name}`);
|
|
15
|
+
const relative = path.relative(root, file);
|
|
16
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return file;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
module.exports = { CSP, resolveAssetPath };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const { spawn } = require("node:child_process");
|
|
2
|
+
const { StringDecoder } = require("node:string_decoder");
|
|
3
|
+
const MAX_BYTES = 256 * 1024;
|
|
4
|
+
|
|
5
|
+
class NativeBridge {
|
|
6
|
+
constructor(executable, methods, onEvent, options = {}) {
|
|
7
|
+
this.methods = new Set(methods);
|
|
8
|
+
this.pending = new Map();
|
|
9
|
+
this.sequence = 0;
|
|
10
|
+
this.failure = null;
|
|
11
|
+
this.timeout = options.timeout ?? 15000;
|
|
12
|
+
this.onEvent = onEvent;
|
|
13
|
+
this.child = spawn(executable, options.args ?? [], {
|
|
14
|
+
cwd: options.cwd, shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"]
|
|
15
|
+
});
|
|
16
|
+
const decoder = new StringDecoder("utf8");
|
|
17
|
+
let buffer = "";
|
|
18
|
+
this.child.stdout.on("data", (chunk) => {
|
|
19
|
+
if (this.failure) return;
|
|
20
|
+
buffer += decoder.write(chunk);
|
|
21
|
+
let end;
|
|
22
|
+
while ((end = buffer.indexOf("\n")) >= 0) {
|
|
23
|
+
const line = buffer.slice(0, end);
|
|
24
|
+
buffer = buffer.slice(end + 1);
|
|
25
|
+
if (Buffer.byteLength(line) > MAX_BYTES) return this.fail("Native message too large");
|
|
26
|
+
try {
|
|
27
|
+
const message = JSON.parse(line);
|
|
28
|
+
if (!message || typeof message !== "object") throw new Error("Invalid message");
|
|
29
|
+
if (typeof message.event === "string" && message.event.length <= 64 && message.event !== "bridge.closed") {
|
|
30
|
+
this.onEvent({ event: message.event, data: message.data ?? null });
|
|
31
|
+
} else if (Number.isSafeInteger(message.id)) {
|
|
32
|
+
const pending = this.pending.get(message.id);
|
|
33
|
+
if (!pending) continue;
|
|
34
|
+
clearTimeout(pending.timer);
|
|
35
|
+
this.pending.delete(message.id);
|
|
36
|
+
if (typeof message.error === "string") pending.reject(new Error(message.error));
|
|
37
|
+
else pending.resolve(message.result ?? null);
|
|
38
|
+
} else throw new Error("Invalid message");
|
|
39
|
+
} catch {
|
|
40
|
+
return this.fail("Invalid native protocol");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (Buffer.byteLength(buffer) > MAX_BYTES) this.fail("Native message too large");
|
|
44
|
+
});
|
|
45
|
+
// Drain diagnostics without sending credentials or SDK logs to the renderer.
|
|
46
|
+
this.child.stderr.resume();
|
|
47
|
+
this.child.stdin.on("error", () => this.fail("Native input closed"));
|
|
48
|
+
this.child.once("error", () => this.fail("Native process could not start"));
|
|
49
|
+
this.child.once("exit", () => this.fail("Native process exited"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async request(method, params) {
|
|
53
|
+
if (this.failure) throw new Error(this.failure);
|
|
54
|
+
if (!this.methods.has(method)) throw new Error("Native method not allowed");
|
|
55
|
+
if (this.pending.size >= 128 || this.child.stdin.writableLength > MAX_BYTES) throw new Error("Native bridge busy");
|
|
56
|
+
const id = ++this.sequence;
|
|
57
|
+
const line = JSON.stringify({ id, method, params: params ?? null }) + "\n";
|
|
58
|
+
if (Buffer.byteLength(line) > MAX_BYTES) throw new Error("Native request too large");
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
this.pending.delete(id);
|
|
62
|
+
reject(new Error("Native request timed out"));
|
|
63
|
+
}, this.timeout);
|
|
64
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
65
|
+
this.child.stdin.write(line);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fail(reason, graceful = false) {
|
|
70
|
+
if (this.failure) return;
|
|
71
|
+
this.failure = reason;
|
|
72
|
+
for (const pending of this.pending.values()) {
|
|
73
|
+
clearTimeout(pending.timer);
|
|
74
|
+
pending.reject(new Error(reason));
|
|
75
|
+
}
|
|
76
|
+
this.pending.clear();
|
|
77
|
+
if (graceful) {
|
|
78
|
+
this.child.stdin.end();
|
|
79
|
+
const timer = setTimeout(() => this.child.kill("SIGKILL"), 4000);
|
|
80
|
+
timer.unref();
|
|
81
|
+
this.child.once("exit", () => clearTimeout(timer));
|
|
82
|
+
} else this.child.kill("SIGKILL");
|
|
83
|
+
this.onEvent({ event: "bridge.closed", data: { reason } });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
close() { this.fail("Native bridge closed", true); }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const isTrustedSender = (event, window) => {
|
|
90
|
+
if (!window || window.isDestroyed() || event.sender !== window.webContents
|
|
91
|
+
|| event.senderFrame !== window.webContents.mainFrame) return false;
|
|
92
|
+
try {
|
|
93
|
+
const url = new URL(event.senderFrame.url);
|
|
94
|
+
return url.protocol === "next2d:" && url.host === "game" && !url.username && !url.password;
|
|
95
|
+
} catch { return false; }
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
module.exports = { NativeBridge, isTrustedSender };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require("electron");
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld("next2dNative", {
|
|
4
|
+
request: (method, params) => ipcRenderer.invoke("next2d:native:request", method, params),
|
|
5
|
+
onEvent: (listener) => {
|
|
6
|
+
if (typeof listener !== "function") throw new TypeError("Expected event listener");
|
|
7
|
+
const handler = (_event, message) => listener(message);
|
|
8
|
+
ipcRenderer.on("next2d:native:event", handler);
|
|
9
|
+
return () => ipcRenderer.removeListener("next2d:native:event", handler);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
@@ -192,16 +192,18 @@ bool ComputeLayout(const std::string& css_font, const std::wstring& text, Layout
|
|
|
192
192
|
stbtt_GetFontVMetrics(&lo.font->info, &lo.ascent, &lo.descent, &line_gap);
|
|
193
193
|
lo.cps = ToCodepoints(text);
|
|
194
194
|
|
|
195
|
+
// Keep layout arithmetic in double; stb's bitmap API still takes float.
|
|
196
|
+
const double scale = lo.scale;
|
|
195
197
|
double x = 0;
|
|
196
198
|
for (size_t i = 0; i < lo.cps.size(); ++i) {
|
|
197
199
|
int advance = 0, lsb = 0;
|
|
198
200
|
stbtt_GetCodepointHMetrics(&lo.font->info, static_cast<int>(lo.cps[i]), &advance, &lsb);
|
|
199
|
-
x += advance *
|
|
201
|
+
x += advance * scale;
|
|
200
202
|
if (i + 1 < lo.cps.size()) {
|
|
201
203
|
x += stbtt_GetCodepointKernAdvance(
|
|
202
204
|
&lo.font->info,
|
|
203
205
|
static_cast<int>(lo.cps[i]),
|
|
204
|
-
static_cast<int>(lo.cps[i + 1])) *
|
|
206
|
+
static_cast<int>(lo.cps[i + 1])) * scale;
|
|
205
207
|
}
|
|
206
208
|
}
|
|
207
209
|
lo.width = x;
|
|
@@ -239,9 +241,10 @@ bool MeasureText(const std::string& css_font, const std::wstring& text,
|
|
|
239
241
|
if (!ComputeLayout(css_font, text, lo)) {
|
|
240
242
|
return false;
|
|
241
243
|
}
|
|
244
|
+
const double scale = lo.scale;
|
|
242
245
|
out.width = lo.width;
|
|
243
|
-
out.ascent = lo.ascent *
|
|
244
|
-
out.descent = -lo.descent *
|
|
246
|
+
out.ascent = lo.ascent * scale;
|
|
247
|
+
out.descent = -static_cast<double>(lo.descent) * scale; // stbtt の descent は負値
|
|
245
248
|
return true;
|
|
246
249
|
}
|
|
247
250
|
|
|
@@ -256,8 +259,9 @@ bool RasterizeText(const std::string& css_font, const std::wstring& text,
|
|
|
256
259
|
return false;
|
|
257
260
|
}
|
|
258
261
|
|
|
259
|
-
const double
|
|
260
|
-
const double
|
|
262
|
+
const double scale = lo.scale;
|
|
263
|
+
const double ascent_px = lo.ascent * scale;
|
|
264
|
+
const double descent_px = -static_cast<double>(lo.descent) * scale;
|
|
261
265
|
const int width = std::max(1, static_cast<int>(std::ceil(lo.width)) + 2);
|
|
262
266
|
const int height = std::max(1, static_cast<int>(std::ceil(ascent_px + descent_px)) + 2);
|
|
263
267
|
const int baseline = static_cast<int>(std::ceil(ascent_px)) + 1;
|
|
@@ -272,7 +276,7 @@ bool RasterizeText(const std::string& css_font, const std::wstring& text,
|
|
|
272
276
|
|
|
273
277
|
int x0 = 0, y0 = 0, x1 = 0, y1 = 0;
|
|
274
278
|
const float shift_x =
|
|
275
|
-
static_cast<float>(pen_x + lsb *
|
|
279
|
+
static_cast<float>(pen_x + lsb * scale - std::floor(pen_x + lsb * scale));
|
|
276
280
|
stbtt_GetCodepointBitmapBoxSubpixel(
|
|
277
281
|
&lo.font->info, cp, lo.scale, lo.scale, shift_x, 0, &x0, &y0, &x1, &y1);
|
|
278
282
|
|
|
@@ -284,7 +288,7 @@ bool RasterizeText(const std::string& css_font, const std::wstring& text,
|
|
|
284
288
|
&lo.font->info, glyph.data(), gw, gh, gw,
|
|
285
289
|
lo.scale, lo.scale, shift_x, 0, cp);
|
|
286
290
|
|
|
287
|
-
const int dst_x = static_cast<int>(std::floor(pen_x + lsb *
|
|
291
|
+
const int dst_x = static_cast<int>(std::floor(pen_x + lsb * scale));
|
|
288
292
|
const int dst_y = baseline + y0;
|
|
289
293
|
for (int yy = 0; yy < gh; ++yy) {
|
|
290
294
|
const int py = dst_y + yy;
|
|
@@ -298,10 +302,10 @@ bool RasterizeText(const std::string& css_font, const std::wstring& text,
|
|
|
298
302
|
}
|
|
299
303
|
}
|
|
300
304
|
|
|
301
|
-
pen_x += advance *
|
|
305
|
+
pen_x += advance * scale;
|
|
302
306
|
if (i + 1 < lo.cps.size()) {
|
|
303
307
|
pen_x += stbtt_GetCodepointKernAdvance(
|
|
304
|
-
&lo.font->info, cp, static_cast<int>(lo.cps[i + 1])) *
|
|
308
|
+
&lo.font->info, cp, static_cast<int>(lo.cps[i + 1])) * scale;
|
|
305
309
|
}
|
|
306
310
|
}
|
|
307
311
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
# macOS / Linux で Windows 実機なしに実行できる検証をまとめて回す。
|
|
3
3
|
# 1. RasterCore 単体テスト (実行)
|
|
4
4
|
# 2. RasterCore 単体テスト (ASan/UBSan)
|
|
5
|
-
# 3.
|
|
5
|
+
# 3. stb 境界値・デコード回帰テスト (通常 + ASan/UBSan)
|
|
6
|
+
# 4. V8 依存ソースの構文チェック (実 V8 ヘッダに対して)
|
|
6
7
|
#
|
|
7
8
|
# 使い方:
|
|
8
9
|
# tests/check_local.sh [V8_INCLUDE_DIR]
|
|
@@ -13,16 +14,23 @@ cd "$(dirname "$0")/.."
|
|
|
13
14
|
V8_TAG="13.7.152.19"
|
|
14
15
|
V8INC="${1:-}"
|
|
15
16
|
|
|
16
|
-
echo "== 1/
|
|
17
|
+
echo "== 1/4 RasterCore unit tests =="
|
|
17
18
|
c++ -std=c++17 -Wall -Wextra -O1 -o /tmp/next2d_raster_test tests/raster_test.cpp
|
|
18
19
|
/tmp/next2d_raster_test
|
|
19
20
|
|
|
20
|
-
echo "== 2/
|
|
21
|
+
echo "== 2/4 RasterCore unit tests (ASan/UBSan) =="
|
|
21
22
|
c++ -std=c++17 -g -fsanitize=address,undefined -fno-omit-frame-pointer \
|
|
22
23
|
-o /tmp/next2d_raster_test_asan tests/raster_test.cpp
|
|
23
24
|
/tmp/next2d_raster_test_asan
|
|
24
25
|
|
|
25
|
-
echo "== 3/
|
|
26
|
+
echo "== 3/4 stb safety regression tests =="
|
|
27
|
+
c++ -std=c++17 -O1 -o /tmp/next2d_stb_safety_test tests/stb_safety_test.cpp
|
|
28
|
+
/tmp/next2d_stb_safety_test
|
|
29
|
+
c++ -std=c++17 -g -fsanitize=address,undefined -fno-omit-frame-pointer \
|
|
30
|
+
-o /tmp/next2d_stb_safety_test_asan tests/stb_safety_test.cpp
|
|
31
|
+
/tmp/next2d_stb_safety_test_asan
|
|
32
|
+
|
|
33
|
+
echo "== 4/4 V8-dependent sources syntax check (V8 ${V8_TAG}) =="
|
|
26
34
|
if [ -z "${V8INC}" ]; then
|
|
27
35
|
if [ ! -d ".v8_headers/include" ]; then
|
|
28
36
|
git clone --depth 1 --branch "${V8_TAG}" --filter=blob:none --sparse \
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Regression tests for local stb patches. No Windows SDK, V8 or external assets.
|
|
2
|
+
#include <climits>
|
|
3
|
+
#include <cstdio>
|
|
4
|
+
#include <cstdlib>
|
|
5
|
+
#include <vector>
|
|
6
|
+
|
|
7
|
+
// Record allocation attempts without allocating huge buffers in boundary tests.
|
|
8
|
+
static size_t last_allocation = 0;
|
|
9
|
+
static void* TestMalloc(size_t size)
|
|
10
|
+
{
|
|
11
|
+
last_allocation = size;
|
|
12
|
+
return size > 64 * 1024 * 1024 ? nullptr : std::malloc(size);
|
|
13
|
+
}
|
|
14
|
+
static void* TestRealloc(void* data, size_t size)
|
|
15
|
+
{
|
|
16
|
+
last_allocation = size;
|
|
17
|
+
return size > 64 * 1024 * 1024 ? nullptr : std::realloc(data, size);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#define STBI_MALLOC TestMalloc
|
|
21
|
+
#define STBI_REALLOC TestRealloc
|
|
22
|
+
#define STBI_FREE std::free
|
|
23
|
+
#define STBI_NO_STDIO
|
|
24
|
+
#define STB_IMAGE_IMPLEMENTATION
|
|
25
|
+
#include "../third_party/stb_image.h"
|
|
26
|
+
|
|
27
|
+
#define STB_VORBIS_NO_STDIO
|
|
28
|
+
#define STB_VORBIS_NO_PUSHDATA_API
|
|
29
|
+
#include "../third_party/stb_vorbis.c"
|
|
30
|
+
|
|
31
|
+
static int failures = 0;
|
|
32
|
+
static int checks = 0;
|
|
33
|
+
#define EXPECT(condition) do { ++checks; if (!(condition)) { \
|
|
34
|
+
++failures; std::printf("not ok - line %d: %s\n", __LINE__, #condition); } } while (0)
|
|
35
|
+
|
|
36
|
+
static void Test16BitConversion()
|
|
37
|
+
{
|
|
38
|
+
auto* pixel = static_cast<stbi__uint16*>(std::malloc(3 * sizeof(stbi__uint16)));
|
|
39
|
+
pixel[0] = 123; pixel[1] = 456; pixel[2] = 789;
|
|
40
|
+
auto* rgba = stbi__convert_format16(pixel, 3, 4, 1, 1);
|
|
41
|
+
EXPECT(rgba != nullptr);
|
|
42
|
+
if (rgba) {
|
|
43
|
+
EXPECT(rgba[0] == 123 && rgba[1] == 456 && rgba[2] == 789 && rgba[3] == 65535);
|
|
44
|
+
std::free(rgba);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The original unsigned product wraps to zero for these dimensions.
|
|
48
|
+
// Conversion must reject the size before allocating or reading input pixels.
|
|
49
|
+
const unsigned int dimensions[][2] = {{65536, 8192}, {UINT_MAX, 1}, {1, UINT_MAX}};
|
|
50
|
+
for (const auto& size : dimensions) {
|
|
51
|
+
pixel = static_cast<stbi__uint16*>(std::malloc(3 * sizeof(stbi__uint16)));
|
|
52
|
+
last_allocation = 0;
|
|
53
|
+
rgba = stbi__convert_format16(pixel, 3, 4, size[0], size[1]);
|
|
54
|
+
EXPECT(rgba == nullptr);
|
|
55
|
+
EXPECT(last_allocation == 0);
|
|
56
|
+
std::free(rgba);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static void TestAnimatedGif()
|
|
61
|
+
{
|
|
62
|
+
// Original fixture: 1x1, black/red palette. Frame 2 restores frame 1;
|
|
63
|
+
// transparent frame 3 must therefore show red, not read before the buffer.
|
|
64
|
+
std::vector<stbi_uc> gif = {
|
|
65
|
+
'G', 'I', 'F', '8', '9', 'a', 1, 0, 1, 0, 0x80, 0, 0,
|
|
66
|
+
0, 0, 0, 255, 0, 0
|
|
67
|
+
};
|
|
68
|
+
const stbi_uc controls[] = {4, 12, 5};
|
|
69
|
+
const stbi_uc pixels[] = {0x4c, 0x44, 0x44};
|
|
70
|
+
for (int i = 0; i < 3; ++i) {
|
|
71
|
+
gif.insert(gif.end(), {0x21, 0xf9, 4, controls[i], 1, 0, 0, 0,
|
|
72
|
+
0x2c, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 2, pixels[i], 1, 0});
|
|
73
|
+
}
|
|
74
|
+
gif.push_back(0x3b);
|
|
75
|
+
int* delays = nullptr;
|
|
76
|
+
int width = 0, height = 0, frames = 0, components = 0;
|
|
77
|
+
stbi_uc* rgba = stbi_load_gif_from_memory(gif.data(), static_cast<int>(gif.size()),
|
|
78
|
+
&delays, &width, &height, &frames, &components, 4);
|
|
79
|
+
EXPECT(rgba != nullptr);
|
|
80
|
+
EXPECT(width == 1 && height == 1 && frames == 3 && components == 4);
|
|
81
|
+
if (rgba && frames == 3) {
|
|
82
|
+
EXPECT(rgba[0] == 255 && rgba[1] == 0 && rgba[2] == 0 && rgba[3] == 255);
|
|
83
|
+
EXPECT(rgba[4] == 0 && rgba[5] == 0 && rgba[6] == 0 && rgba[7] == 255);
|
|
84
|
+
EXPECT(rgba[8] == 255 && rgba[9] == 0 && rgba[10] == 0 && rgba[11] == 255);
|
|
85
|
+
}
|
|
86
|
+
EXPECT(delays != nullptr);
|
|
87
|
+
if (delays && frames == 3) EXPECT(delays[0] == 10 && delays[1] == 10 && delays[2] == 10);
|
|
88
|
+
stbi_image_free(rgba);
|
|
89
|
+
stbi_image_free(delays);
|
|
90
|
+
|
|
91
|
+
// Oversized canvas is rejected before any pixel buffer is allocated.
|
|
92
|
+
gif[6] = gif[7] = gif[8] = gif[9] = 255;
|
|
93
|
+
last_allocation = 0;
|
|
94
|
+
rgba = stbi_load_gif_from_memory(gif.data(), static_cast<int>(gif.size()),
|
|
95
|
+
&delays, &width, &height, &frames, &components, 4);
|
|
96
|
+
EXPECT(rgba == nullptr);
|
|
97
|
+
EXPECT(last_allocation == 0);
|
|
98
|
+
stbi_image_free(rgba);
|
|
99
|
+
stbi_image_free(delays);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static void TestVorbisSeekBounds()
|
|
103
|
+
{
|
|
104
|
+
unsigned char bytes[16] = {};
|
|
105
|
+
stb_vorbis decoder = {};
|
|
106
|
+
decoder.stream_start = bytes;
|
|
107
|
+
decoder.stream_end = bytes + sizeof(bytes);
|
|
108
|
+
for (unsigned int offset : {0U, 15U, 16U, 17U, UINT_MAX, 0U}) {
|
|
109
|
+
const bool valid = offset < sizeof(bytes);
|
|
110
|
+
EXPECT(set_file_offset(&decoder, offset) == static_cast<int>(valid));
|
|
111
|
+
EXPECT(decoder.eof == static_cast<int>(!valid));
|
|
112
|
+
EXPECT(decoder.stream == (valid ? bytes + offset : decoder.stream_end));
|
|
113
|
+
}
|
|
114
|
+
decoder.stream_end = decoder.stream_start;
|
|
115
|
+
EXPECT(set_file_offset(&decoder, 0) == 0);
|
|
116
|
+
EXPECT(decoder.eof == 1 && decoder.stream == decoder.stream_end);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
int main()
|
|
120
|
+
{
|
|
121
|
+
Test16BitConversion();
|
|
122
|
+
TestAnimatedGif();
|
|
123
|
+
TestVorbisSeekBounds();
|
|
124
|
+
std::printf("stb safety: %d checks, %d failures\n", checks, failures);
|
|
125
|
+
return failures ? 1 : 0;
|
|
126
|
+
}
|
|
@@ -12,3 +12,18 @@
|
|
|
12
12
|
| dr_wav.h | https://github.com/mackron/dr_libs | public domain (Unlicense) / MIT-0 のデュアル |
|
|
13
13
|
|
|
14
14
|
いずれも各ファイル末尾に原文のライセンス条文が含まれる。
|
|
15
|
+
|
|
16
|
+
## ローカル修正 / Local patches
|
|
17
|
+
|
|
18
|
+
同梱の stb には以下の修正を適用している。更新時は再適用の要否を確認し、
|
|
19
|
+
`tests/stb_safety_test.cpp` と既存のプラットフォームテストを実行する。
|
|
20
|
+
|
|
21
|
+
The vendored stb files include these local patches. When updating upstream code,
|
|
22
|
+
check whether they are still needed and run `tests/stb_safety_test.cpp` and the
|
|
23
|
+
existing platform tests.
|
|
24
|
+
|
|
25
|
+
| ファイル / File | 修正 / Patch |
|
|
26
|
+
|---|---|
|
|
27
|
+
| stb_image.h | メモリサイズを乗算前に拡張し、16bit変換・GIFフレーム追加時のオーバーフローを検証。GIFの過去フレーム参照をバッファ内に修正。 / Widen operands before size multiplication, validate overflow in 16-bit conversion and GIF frame growth, and keep GIF previous-frame references inside the buffer. |
|
|
28
|
+
| stb_truetype.h | ビットマップの確保・初期化サイズを `size_t` で乗算。 / Multiply bitmap allocation and initialization sizes in `size_t`. |
|
|
29
|
+
| stb_vorbis.c | ポインタ加算前に入力サイズとシーク位置を比較。 / Check seek offsets against the input size before pointer arithmetic. |
|
|
@@ -1817,7 +1817,8 @@ static stbi__uint16 *stbi__convert_format16(stbi__uint16 *data, int img_n, int r
|
|
|
1817
1817
|
if (req_comp == img_n) return data;
|
|
1818
1818
|
STBI_ASSERT(req_comp >= 1 && req_comp <= 4);
|
|
1819
1819
|
|
|
1820
|
-
good = (
|
|
1820
|
+
good = (x > INT_MAX || y > INT_MAX) ? NULL :
|
|
1821
|
+
(stbi__uint16 *) stbi__malloc_mad4(req_comp, (int) x, (int) y, 2, 0);
|
|
1821
1822
|
if (good == NULL) {
|
|
1822
1823
|
STBI_FREE(data);
|
|
1823
1824
|
return (stbi__uint16 *) stbi__errpuc("outofmem", "Out of memory");
|
|
@@ -4821,7 +4822,7 @@ static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 r
|
|
|
4821
4822
|
stbi__create_png_alpha_expand8(dest, dest, x, img_n);
|
|
4822
4823
|
} else if (depth == 8) {
|
|
4823
4824
|
if (img_n == out_n)
|
|
4824
|
-
memcpy(dest, cur, x*img_n);
|
|
4825
|
+
memcpy(dest, cur, (size_t) x*img_n);
|
|
4825
4826
|
else
|
|
4826
4827
|
stbi__create_png_alpha_expand8(dest, cur, x, img_n);
|
|
4827
4828
|
} else if (depth == 16) {
|
|
@@ -6201,7 +6202,7 @@ static void *stbi__psd_load(stbi__context *s, int *x, int *y, int *comp, int req
|
|
|
6201
6202
|
out = (stbi_uc *) stbi__malloc_mad3(8, w, h, 0);
|
|
6202
6203
|
ri->bits_per_channel = 16;
|
|
6203
6204
|
} else
|
|
6204
|
-
out = (stbi_uc *)
|
|
6205
|
+
out = (stbi_uc *) stbi__malloc_mad3(4, w, h, 0);
|
|
6205
6206
|
|
|
6206
6207
|
if (!out) return stbi__errpuc("outofmem", "Out of memory");
|
|
6207
6208
|
pixelCount = w*h;
|
|
@@ -6524,7 +6525,7 @@ static void *stbi__pic_load(stbi__context *s,int *px,int *py,int *comp,int req_c
|
|
|
6524
6525
|
// intermediate buffer is RGBA
|
|
6525
6526
|
result = (stbi_uc *) stbi__malloc_mad3(x, y, 4, 0);
|
|
6526
6527
|
if (!result) return stbi__errpuc("outofmem", "Out of memory");
|
|
6527
|
-
memset(result, 0xff, x*y*4);
|
|
6528
|
+
memset(result, 0xff, (size_t) x*y*4);
|
|
6528
6529
|
|
|
6529
6530
|
if (!stbi__pic_load_core(s,x,y,comp, result)) {
|
|
6530
6531
|
STBI_FREE(result);
|
|
@@ -6833,11 +6834,11 @@ static stbi_uc *stbi__gif_load_next(stbi__context *s, stbi__gif *g, int *comp, i
|
|
|
6833
6834
|
}
|
|
6834
6835
|
|
|
6835
6836
|
// background is what out is after the undoing of the previou frame;
|
|
6836
|
-
memcpy( g->background, g->out, 4 * g->w * g->h );
|
|
6837
|
+
memcpy( g->background, g->out, (size_t) 4 * g->w * g->h );
|
|
6837
6838
|
}
|
|
6838
6839
|
|
|
6839
6840
|
// clear my history;
|
|
6840
|
-
memset( g->history, 0x00, g->w * g->h ); // pixels that were affected previous frame
|
|
6841
|
+
memset( g->history, 0x00, (size_t) g->w * g->h ); // pixels that were affected previous frame
|
|
6841
6842
|
|
|
6842
6843
|
for (;;) {
|
|
6843
6844
|
int tag = stbi__get8(s);
|
|
@@ -6987,11 +6988,15 @@ static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y,
|
|
|
6987
6988
|
if (u) {
|
|
6988
6989
|
*x = g.w;
|
|
6989
6990
|
*y = g.h;
|
|
6990
|
-
++layers;
|
|
6991
6991
|
stride = g.w * g.h * 4;
|
|
6992
|
+
// Keep the frame count, byte offsets and allocator sizes representable.
|
|
6993
|
+
if (layers == INT_MAX || !stbi__mul2sizes_valid(layers + 1, stride) ||
|
|
6994
|
+
!stbi__mul2sizes_valid(layers + 1, sizeof(int)))
|
|
6995
|
+
return stbi__load_gif_main_outofmem(&g, out, delays);
|
|
6996
|
+
++layers;
|
|
6992
6997
|
|
|
6993
6998
|
if (out) {
|
|
6994
|
-
void *tmp = (stbi_uc*) STBI_REALLOC_SIZED( out, out_size, layers * stride );
|
|
6999
|
+
void *tmp = (stbi_uc*) STBI_REALLOC_SIZED( out, out_size, (size_t) layers * stride );
|
|
6995
7000
|
if (!tmp)
|
|
6996
7001
|
return stbi__load_gif_main_outofmem(&g, out, delays);
|
|
6997
7002
|
else {
|
|
@@ -7007,7 +7012,7 @@ static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y,
|
|
|
7007
7012
|
delays_size = layers * sizeof(int);
|
|
7008
7013
|
}
|
|
7009
7014
|
} else {
|
|
7010
|
-
out = (stbi_uc*)stbi__malloc( layers * stride );
|
|
7015
|
+
out = (stbi_uc*)stbi__malloc( (size_t) layers * stride );
|
|
7011
7016
|
if (!out)
|
|
7012
7017
|
return stbi__load_gif_main_outofmem(&g, out, delays);
|
|
7013
7018
|
out_size = layers * stride;
|
|
@@ -7020,7 +7025,7 @@ static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y,
|
|
|
7020
7025
|
}
|
|
7021
7026
|
memcpy( out + ((layers - 1) * stride), u, stride );
|
|
7022
7027
|
if (layers >= 2) {
|
|
7023
|
-
two_back = out - 2 * stride;
|
|
7028
|
+
two_back = out + (layers - 2) * stride;
|
|
7024
7029
|
}
|
|
7025
7030
|
|
|
7026
7031
|
if (delays) {
|
|
@@ -3739,7 +3739,7 @@ STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info
|
|
|
3739
3739
|
if (yoff ) *yoff = iy0;
|
|
3740
3740
|
|
|
3741
3741
|
if (gbm.w && gbm.h) {
|
|
3742
|
-
gbm.pixels = (unsigned char *) STBTT_malloc(gbm.w * gbm.h, info->userdata);
|
|
3742
|
+
gbm.pixels = (unsigned char *) STBTT_malloc((size_t) gbm.w * gbm.h, info->userdata);
|
|
3743
3743
|
if (gbm.pixels) {
|
|
3744
3744
|
gbm.stride = gbm.w;
|
|
3745
3745
|
|
|
@@ -3822,7 +3822,7 @@ static int stbtt_BakeFontBitmap_internal(unsigned char *data, int offset, // fo
|
|
|
3822
3822
|
f.userdata = NULL;
|
|
3823
3823
|
if (!stbtt_InitFont(&f, data, offset))
|
|
3824
3824
|
return -1;
|
|
3825
|
-
STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels
|
|
3825
|
+
STBTT_memset(pixels, 0, (size_t) pw*ph); // background of 0 around pixels
|
|
3826
3826
|
x=y=1;
|
|
3827
3827
|
bottom_y = 1;
|
|
3828
3828
|
|
|
@@ -3981,7 +3981,7 @@ STBTT_DEF int stbtt_PackBegin(stbtt_pack_context *spc, unsigned char *pixels, in
|
|
|
3981
3981
|
stbrp_init_target(context, pw-padding, ph-padding, nodes, num_nodes);
|
|
3982
3982
|
|
|
3983
3983
|
if (pixels)
|
|
3984
|
-
STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels
|
|
3984
|
+
STBTT_memset(pixels, 0, (size_t) pw*ph); // background of 0 around pixels
|
|
3985
3985
|
|
|
3986
3986
|
return 1;
|
|
3987
3987
|
}
|
|
@@ -4610,7 +4610,7 @@ STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float sc
|
|
|
4610
4610
|
float *precompute;
|
|
4611
4611
|
stbtt_vertex *verts;
|
|
4612
4612
|
int num_verts = stbtt_GetGlyphShape(info, glyph, &verts);
|
|
4613
|
-
data = (unsigned char *) STBTT_malloc(w * h, info->userdata);
|
|
4613
|
+
data = (unsigned char *) STBTT_malloc((size_t) w * h, info->userdata);
|
|
4614
4614
|
precompute = (float *) STBTT_malloc(num_verts * sizeof(float), info->userdata);
|
|
4615
4615
|
|
|
4616
4616
|
for (i=0,j=num_verts-1; i < num_verts; j=i++) {
|
|
@@ -1401,7 +1401,7 @@ static int set_file_offset(stb_vorbis *f, unsigned int loc)
|
|
|
1401
1401
|
#endif
|
|
1402
1402
|
f->eof = 0;
|
|
1403
1403
|
if (USE_MEMORY(f)) {
|
|
1404
|
-
if (
|
|
1404
|
+
if (loc >= (size_t) (f->stream_end - f->stream_start)) {
|
|
1405
1405
|
f->stream = f->stream_end;
|
|
1406
1406
|
f->eof = 1;
|
|
1407
1407
|
return 0;
|