web: Remove jszip, use rust

This commit is contained in:
Nathan Adams 2024-06-08 18:06:22 +02:00
parent eb26616886
commit 0ded7c61ea
8 changed files with 95 additions and 34 deletions

1
Cargo.lock generated
View File

@ -4662,6 +4662,7 @@ dependencies = [
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams", "wasm-streams",
"web-sys", "web-sys",
"zip",
] ]
[[package]] [[package]]

View File

@ -59,6 +59,7 @@ gloo-net = { version = "0.5.0", default-features = false, features = ["websocke
rfd = { version = "0.14.1", features = ["file-handle-inner"] } rfd = { version = "0.14.1", features = ["file-handle-inner"] }
wasm-streams = "0.4.0" wasm-streams = "0.4.0"
futures = { workspace = true } futures = { workspace = true }
zip = { version = "2.1.2", default-features = false, features = ["deflate"]}
[dependencies.ruffle_core] [dependencies.ruffle_core]
path = "../core" path = "../core"

32
web/package-lock.json generated
View File

@ -4376,7 +4376,8 @@
"node_modules/core-util-is": { "node_modules/core-util-is": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true
}, },
"node_modules/cosmiconfig": { "node_modules/cosmiconfig": {
"version": "9.0.0", "version": "9.0.0",
@ -7162,7 +7163,8 @@
"node_modules/immediate": { "node_modules/immediate": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"dev": true
}, },
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
@ -7231,7 +7233,8 @@
"node_modules/inherits": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
}, },
"node_modules/ini": { "node_modules/ini": {
"version": "1.3.8", "version": "1.3.8",
@ -7532,7 +7535,8 @@
"node_modules/isarray": { "node_modules/isarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true
}, },
"node_modules/isexe": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
@ -8062,6 +8066,7 @@
"version": "3.10.1", "version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"dev": true,
"dependencies": { "dependencies": {
"lie": "~3.3.0", "lie": "~3.3.0",
"pako": "~1.0.2", "pako": "~1.0.2",
@ -8073,6 +8078,7 @@
"version": "2.3.8", "version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"dependencies": { "dependencies": {
"core-util-is": "~1.0.0", "core-util-is": "~1.0.0",
"inherits": "~2.0.3", "inherits": "~2.0.3",
@ -8086,12 +8092,14 @@
"node_modules/jszip/node_modules/safe-buffer": { "node_modules/jszip/node_modules/safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
}, },
"node_modules/jszip/node_modules/string_decoder": { "node_modules/jszip/node_modules/string_decoder": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"dependencies": { "dependencies": {
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
@ -8222,6 +8230,7 @@
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dev": true,
"dependencies": { "dependencies": {
"immediate": "~3.0.5" "immediate": "~3.0.5"
} }
@ -9503,7 +9512,8 @@
"node_modules/pako": { "node_modules/pako": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true
}, },
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
@ -9919,7 +9929,8 @@
"node_modules/process-nextick-args": { "node_modules/process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
}, },
"node_modules/progress": { "node_modules/progress": {
"version": "2.0.3", "version": "2.0.3",
@ -11165,7 +11176,8 @@
"node_modules/setimmediate": { "node_modules/setimmediate": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"dev": true
}, },
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
@ -12500,7 +12512,8 @@
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
}, },
"node_modules/utils-merge": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
@ -13581,7 +13594,6 @@
"version": "0.1.0", "version": "0.1.0",
"license": "(MIT OR Apache-2.0)", "license": "(MIT OR Apache-2.0)",
"dependencies": { "dependencies": {
"jszip": "^3.10.1",
"wasm-feature-detect": "^1.6.1" "wasm-feature-detect": "^1.6.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -18,7 +18,6 @@
"checkTypes": "tsc --noemit" "checkTypes": "tsc --noemit"
}, },
"dependencies": { "dependencies": {
"jszip": "^3.10.1",
"wasm-feature-detect": "^1.6.1" "wasm-feature-detect": "^1.6.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -9,7 +9,7 @@ import {
signExtensions, signExtensions,
referenceTypes, referenceTypes,
} from "wasm-feature-detect"; } from "wasm-feature-detect";
import type { RuffleInstanceBuilder } from "../dist/ruffle_web"; import type { RuffleInstanceBuilder, ZipWriter } from "../dist/ruffle_web";
import { setPolyfillsOnLoad } from "./js-polyfills"; import { setPolyfillsOnLoad } from "./js-polyfills";
import { publicPath } from "./public-path"; import { publicPath } from "./public-path";
import { BaseLoadOptions } from "./load-options"; import { BaseLoadOptions } from "./load-options";
@ -35,7 +35,7 @@ type ProgressCallback = (bytesLoaded: number, bytesTotal: number) => void;
async function fetchRuffle( async function fetchRuffle(
config: BaseLoadOptions, config: BaseLoadOptions,
progressCallback?: ProgressCallback, progressCallback?: ProgressCallback,
): Promise<typeof RuffleInstanceBuilder> { ): Promise<[typeof RuffleInstanceBuilder, typeof ZipWriter]> {
// Apply some pure JavaScript polyfills to prevent conflicts with external // Apply some pure JavaScript polyfills to prevent conflicts with external
// libraries, if needed. // libraries, if needed.
setPolyfillsOnLoad(); setPolyfillsOnLoad();
@ -65,7 +65,11 @@ async function fetchRuffle(
// Note: The argument passed to import() has to be a simple string literal, // Note: The argument passed to import() has to be a simple string literal,
// otherwise some bundler will get confused and won't include the module? // otherwise some bundler will get confused and won't include the module?
const { default: init, RuffleInstanceBuilder } = await (extensionsSupported const {
default: init,
RuffleInstanceBuilder,
ZipWriter,
} = await (extensionsSupported
? import("../dist/ruffle_web-wasm_extensions") ? import("../dist/ruffle_web-wasm_extensions")
: import("../dist/ruffle_web")); : import("../dist/ruffle_web"));
let response; let response;
@ -113,10 +117,12 @@ async function fetchRuffle(
await init(response); await init(response);
return RuffleInstanceBuilder; return [RuffleInstanceBuilder, ZipWriter];
} }
let nativeConstructor: Promise<typeof RuffleInstanceBuilder> | null = null; let nativeConstructors: Promise<
[typeof RuffleInstanceBuilder, typeof ZipWriter]
> | null = null;
/** /**
* Obtain an instance of `Ruffle`. * Obtain an instance of `Ruffle`.
@ -130,11 +136,11 @@ let nativeConstructor: Promise<typeof RuffleInstanceBuilder> | null = null;
export async function createRuffleBuilder( export async function createRuffleBuilder(
config: BaseLoadOptions, config: BaseLoadOptions,
progressCallback?: ProgressCallback, progressCallback?: ProgressCallback,
): Promise<RuffleInstanceBuilder> { ): Promise<[RuffleInstanceBuilder, () => ZipWriter]> {
if (nativeConstructor === null) { if (nativeConstructors === null) {
nativeConstructor = fetchRuffle(config, progressCallback); nativeConstructors = fetchRuffle(config, progressCallback);
} }
const constructor = await nativeConstructor; const constructors = await nativeConstructors;
return new constructor(); return [new constructors[0](), () => new constructors[1]()];
} }

View File

@ -1,4 +1,4 @@
import type { RuffleHandle } from "../dist/ruffle_web"; import type { RuffleHandle, ZipWriter } from "../dist/ruffle_web";
import { createRuffleBuilder } from "./load-ruffle"; import { createRuffleBuilder } from "./load-ruffle";
import { applyStaticStyles, ruffleShadowTemplate } from "./shadow-template"; import { applyStaticStyles, ruffleShadowTemplate } from "./shadow-template";
import { lookupElement } from "./register-element"; import { lookupElement } from "./register-element";
@ -15,7 +15,6 @@ import type { MovieMetadata } from "./movie-metadata";
import { swfFileName } from "./swf-utils"; import { swfFileName } from "./swf-utils";
import { buildInfo } from "./build-info"; import { buildInfo } from "./build-info";
import { text, textAsParagraphs } from "./i18n"; import { text, textAsParagraphs } from "./i18n";
import JSZip from "jszip";
import { isExtension } from "./current-script"; import { isExtension } from "./current-script";
import { configureBuilder } from "./internal/builder"; import { configureBuilder } from "./internal/builder";
@ -167,6 +166,7 @@ export class RufflePlayer extends HTMLElement {
private swfUrl?: URL; private swfUrl?: URL;
private instance: RuffleHandle | null; private instance: RuffleHandle | null;
private newZipWriter: (() => ZipWriter) | null;
private lastActivePlayingState: boolean; private lastActivePlayingState: boolean;
private _metadata: MovieMetadata | null; private _metadata: MovieMetadata | null;
@ -333,6 +333,7 @@ export class RufflePlayer extends HTMLElement {
); );
this.instance = null; this.instance = null;
this.newZipWriter = null;
this.onFSCommand = null; this.onFSCommand = null;
this._readyState = ReadyState.HaveNothing; this._readyState = ReadyState.HaveNothing;
@ -673,7 +674,8 @@ export class RufflePlayer extends HTMLElement {
'The configuration option contextMenu no longer takes a boolean. Use "on", "off", or "rightClickOnly".', 'The configuration option contextMenu no longer takes a boolean. Use "on", "off", or "rightClickOnly".',
); );
} }
const builder = await createRuffleBuilder(
const [builder, zipWriterClass] = await createRuffleBuilder(
this.loadedConfig || {}, this.loadedConfig || {},
this.onRuffleDownloadProgress.bind(this), this.onRuffleDownloadProgress.bind(this),
).catch((e) => { ).catch((e) => {
@ -714,6 +716,7 @@ export class RufflePlayer extends HTMLElement {
this.panic(e); this.panic(e);
throw e; throw e;
}); });
this.newZipWriter = zipWriterClass;
configureBuilder(builder, this.loadedConfig || {}); configureBuilder(builder, this.loadedConfig || {});
builder.setVolume(this.volumeSettings.get_volume()); builder.setVolume(this.volumeSettings.get_volume());
@ -1159,13 +1162,17 @@ export class RufflePlayer extends HTMLElement {
event.pointerType === "touch" || event.pointerType === "pen"; event.pointerType === "touch" || event.pointerType === "pen";
} }
private base64ToBlob(bytesBase64: string, mimeString: string): Blob { private base64ToArray(bytesBase64: string): Uint8Array {
const byteString = atob(bytesBase64); const byteString = atob(bytesBase64);
const ab = new ArrayBuffer(byteString.length); const ia = new Uint8Array(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) { for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i); ia[i] = byteString.charCodeAt(i);
} }
return ia;
}
private base64ToBlob(bytesBase64: string, mimeString: string): Blob {
const ab = this.base64ToArray(bytesBase64);
const blob = new Blob([ab], { type: mimeString }); const blob = new Blob([ab], { type: mimeString });
return blob; return blob;
} }
@ -1342,16 +1349,13 @@ export class RufflePlayer extends HTMLElement {
* Gets the local save information as SOL files and downloads them as a single ZIP file. * Gets the local save information as SOL files and downloads them as a single ZIP file.
*/ */
private async backupSaves(): Promise<void> { private async backupSaves(): Promise<void> {
const zip = new JSZip(); const zip = this.newZipWriter!();
const duplicateNames: string[] = []; const duplicateNames: string[] = [];
Object.keys(localStorage).forEach((key) => { Object.keys(localStorage).forEach((key) => {
let solName = String(key.split("/").pop()); let solName = String(key.split("/").pop());
const solData = localStorage.getItem(key); const solData = localStorage.getItem(key);
if (solData && this.isB64SOL(solData)) { if (solData && this.isB64SOL(solData)) {
const blob = this.base64ToBlob( const array = this.base64ToArray(solData);
solData,
"application/octet-stream",
);
const duplicate = duplicateNames.filter( const duplicate = duplicateNames.filter(
(value) => value === solName, (value) => value === solName,
).length; ).length;
@ -1359,10 +1363,10 @@ export class RufflePlayer extends HTMLElement {
if (duplicate > 0) { if (duplicate > 0) {
solName += ` (${duplicate + 1})`; solName += ` (${duplicate + 1})`;
} }
zip.file(solName + ".sol", blob); zip.addFile(solName + ".sol", array);
} }
}); });
const blob = await zip.generateAsync({ type: "blob" }); const blob = new Blob([zip.save()]);
this.saveFile(blob, "saves.zip"); this.saveFile(blob, "saves.zip");
} }

View File

@ -10,6 +10,7 @@ mod log_adapter;
mod navigator; mod navigator;
mod storage; mod storage;
mod ui; mod ui;
mod zip;
use crate::builder::RuffleInstanceBuilder; use crate::builder::RuffleInstanceBuilder;
use external_interface::{external_to_js_value, js_to_external_value}; use external_interface::{external_to_js_value, js_to_external_value};

37
web/src/zip.rs Normal file
View File

@ -0,0 +1,37 @@
use std::collections::HashMap;
use std::io::Write;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
use zip::write::SimpleFileOptions;
#[wasm_bindgen]
#[derive(Default)]
pub struct ZipWriter {
files: HashMap<String, Vec<u8>>,
}
#[wasm_bindgen]
impl ZipWriter {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self::default()
}
#[wasm_bindgen(js_name = "addFile")]
pub fn add_file(&mut self, name: String, bytes: Vec<u8>) {
self.files.insert(name, bytes);
}
pub fn save(&self) -> Result<Vec<u8>, JsValue> {
let mut buffer = Vec::new();
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buffer));
for (name, content) in &self.files {
zip.start_file(name.to_string(), SimpleFileOptions::default())
.map_err(|e| e.to_string())?;
zip.write_all(content).map_err(|e| e.to_string())?;
}
zip.finish().map_err(|e| e.to_string())?;
Ok(buffer)
}
}