@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.
Files changed (54) hide show
  1. package/README.md +90 -57
  2. package/dist/cli.d.ts +7 -0
  3. package/dist/cli.js +68 -3
  4. package/dist/colors.d.ts +6 -0
  5. package/dist/colors.js +7 -0
  6. package/dist/context.d.ts +10 -1
  7. package/dist/context.js +7 -0
  8. package/dist/description.d.ts +2 -0
  9. package/dist/description.js +8 -0
  10. package/dist/electron-config.d.ts +62 -0
  11. package/dist/electron-config.js +134 -0
  12. package/dist/electron-host.d.ts +7 -0
  13. package/dist/electron-host.js +54 -0
  14. package/dist/electron-native.d.ts +15 -0
  15. package/dist/electron-native.js +73 -0
  16. package/dist/electron.d.ts +4 -9
  17. package/dist/electron.js +85 -133
  18. package/dist/index.js +21 -0
  19. package/dist/native.d.ts +9 -24
  20. package/dist/native.js +48 -106
  21. package/dist/project-vite.d.ts +3 -0
  22. package/dist/project-vite.js +21 -0
  23. package/dist/resolve-tool-packages.d.ts +1 -0
  24. package/dist/resolve-tool-packages.js +29 -0
  25. package/dist/steam-upload.d.ts +27 -0
  26. package/dist/steam-upload.js +131 -0
  27. package/dist/steam.d.ts +22 -0
  28. package/dist/steam.js +193 -0
  29. package/dist/tool-packages.d.ts +15 -0
  30. package/dist/tool-packages.js +74 -0
  31. package/dist/web.js +10 -4
  32. package/dist/xbox.d.ts +1 -1
  33. package/dist/xbox.js +60 -57
  34. package/docs/capacitor.md +201 -0
  35. package/docs/native-bridge.md +147 -0
  36. package/docs/steam.md +384 -0
  37. package/docs/xbox.md +145 -0
  38. package/package.json +9 -12
  39. package/templates/electron/entitlements.plist +12 -0
  40. package/templates/electron/icons/icon.icns +0 -0
  41. package/templates/electron/icons/icon.ico +0 -0
  42. package/templates/electron/icons/icon.png +0 -0
  43. package/templates/electron/index.js +119 -0
  44. package/templates/electron/local-assets.cjs +24 -0
  45. package/templates/electron/native-bridge.cjs +98 -0
  46. package/templates/electron/package.json +11 -0
  47. package/templates/electron/preload.cjs +11 -0
  48. package/templates/xbox/src/platform/StbTextRasterizer.cpp +14 -10
  49. package/templates/xbox/tests/check_local.sh +12 -4
  50. package/templates/xbox/tests/stb_safety_test.cpp +126 -0
  51. package/templates/xbox/third_party/LICENSES.md +15 -0
  52. package/templates/xbox/third_party/stb_image.h +15 -10
  53. package/templates/xbox/third_party/stb_truetype.h +4 -4
  54. 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
+ {
2
+ "name": "next2d-game",
3
+ "version": "1.0.0",
4
+ "description": "Next2D desktop game",
5
+ "private": true,
6
+ "type": "commonjs",
7
+ "main": "index.js",
8
+ "devDependencies": {
9
+ "electron": "44.4.3"
10
+ }
11
+ }
@@ -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 * lo.scale;
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])) * lo.scale;
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 * lo.scale;
244
- out.descent = -lo.descent * lo.scale; // stbtt の 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 ascent_px = lo.ascent * lo.scale;
260
- const double descent_px = -lo.descent * lo.scale;
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 * lo.scale - std::floor(pen_x + lsb * lo.scale));
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 * lo.scale));
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 * lo.scale;
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])) * lo.scale;
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. V8 依存ソースの構文チェック (実 V8 ヘッダに対して。Windows API 非依存の12ファイル)
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/3 RasterCore unit tests =="
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/3 RasterCore unit tests (ASan/UBSan) =="
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/3 V8-dependent sources syntax check (V8 ${V8_TAG}) =="
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 = (stbi__uint16 *) stbi__malloc(req_comp * x * y * 2);
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 *) stbi__malloc(4 * w*h);
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 (f->stream_start + loc >= f->stream_end || f->stream_start + loc < f->stream_start) {
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;