web: Don't parse config in Rust, do it in Typescript with some tests

This commit is contained in:
Nathan Adams 2024-06-05 12:25:22 +02:00
parent 6e53f98068
commit 3fa8735e97
7 changed files with 490 additions and 288 deletions

View File

@ -41,7 +41,7 @@ impl FromStr for Letterbox {
/// The networking API access mode of the Ruffle player.
/// This setting is only used on web.
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum NetworkingAccessMode {
/// All networking APIs are permitted in the SWF file.
#[serde(rename = "all")]

View File

@ -0,0 +1,150 @@
import type { RuffleInstanceBuilder } from "../../dist/ruffle_web";
import { BaseLoadOptions, Duration, SecsDuration } from "../load-options";
/**
* Checks if the given value is explicitly `T` (not null, not undefined)
*
* @param value The value to test
* @returns true if the value isn't null or undefined
*/
function isExplicit<T>(value: T | undefined | null): value is T {
return value !== null && value !== undefined;
}
/**
* Configures the given RuffleInstanceBuilder for the general options provided.
*
* This is the translation layer between what we allow users to provide through e.g. `window.RufflePlayer.config`,
* which is quite relaxed and may evolve over time,
* and the actual values we accept inside Rust (which is quite strict).
*
* This allows us to change the rust side at will, and without needing to worry about backwards compatibility, parsing, etc.
*
* @param builder The builder to set the options on
* @param config The options to apply
*/
export function configureBuilder(
builder: RuffleInstanceBuilder,
config: BaseLoadOptions,
) {
// Guard things for being explicitly set, so that we don't need to specify defaults in yet another place...
if (isExplicit(config.allowScriptAccess)) {
builder.setAllowScriptAccess(config.allowScriptAccess);
}
if (isExplicit(config.backgroundColor)) {
builder.setBackgroundColor(parseColor(config.backgroundColor));
}
if (isExplicit(config.upgradeToHttps)) {
builder.setUpgradeToHttps(config.upgradeToHttps);
}
if (isExplicit(config.compatibilityRules)) {
builder.setCompatibilityRules(config.compatibilityRules);
}
if (isExplicit(config.letterbox)) {
builder.setLetterbox(config.letterbox.toLowerCase());
}
if (isExplicit(config.base)) {
builder.setBaseUrl(config.base);
}
if (isExplicit(config.menu)) {
builder.setShowMenu(config.menu);
}
if (isExplicit(config.allowFullscreen)) {
builder.setAllowFullscreen(config.allowFullscreen);
}
if (isExplicit(config.salign)) {
builder.setStageAlign(config.salign.toLowerCase());
}
if (isExplicit(config.forceAlign)) {
builder.setForceAlign(config.forceAlign);
}
if (isExplicit(config.quality)) {
builder.setQuality(config.quality.toLowerCase());
}
if (isExplicit(config.scale)) {
builder.setScale(config.scale.toLowerCase());
}
if (isExplicit(config.forceScale)) {
builder.setForceScale(config.forceScale);
}
if (isExplicit(config.frameRate)) {
builder.setFrameRate(config.frameRate);
}
if (isExplicit(config.wmode)) {
builder.setWmode(config.wmode);
}
if (isExplicit(config.logLevel)) {
builder.setLogLevel(config.logLevel);
}
if (isExplicit(config.maxExecutionDuration)) {
builder.setMaxExecutionDuration(
parseDuration(config.maxExecutionDuration),
);
}
if (isExplicit(config.playerVersion)) {
builder.setPlayerVersion(config.playerVersion);
}
if (isExplicit(config.preferredRenderer)) {
builder.setPreferredRenderer(config.preferredRenderer);
}
if (isExplicit(config.openUrlMode)) {
builder.setOpenUrlMode(config.openUrlMode.toLowerCase());
}
if (isExplicit(config.allowNetworking)) {
builder.setAllowNetworking(config.allowNetworking.toLowerCase());
}
if (isExplicit(config.credentialAllowList)) {
builder.setCredentialAllowList(config.credentialAllowList);
}
if (isExplicit(config.playerRuntime)) {
builder.setPlayerRuntime(config.playerRuntime);
}
if (isExplicit(config.socketProxy)) {
for (const proxy of config.socketProxy) {
builder.addSocketProxy(proxy.host, proxy.port, proxy.proxyUrl);
}
}
}
/**
* Parses a color into an RGB value.
*
* @param color The color string to parse
* @returns A valid RGB number, or undefined if invalid
*/
export function parseColor(color: string): number | undefined {
if (color.startsWith("#")) {
color = color.substring(1);
}
if (color.length < 6) {
return undefined;
}
let result = 0;
for (let i = 0; i < 6; i++) {
const digit = parseInt(color[i]!, 16);
if (!isNaN(digit)) {
result = (result << 4) | digit;
} else {
result = result << 4;
}
}
return result;
}
/**
* Parses a duration into number of seconds.
*
* @param value The duration to parse
* @returns A valid number of seconds
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function parseDuration(value: Duration): SecsDuration {
if (typeof value === "number") {
return value;
}
return value.secs;
}

View File

@ -9,11 +9,10 @@ import {
signExtensions,
referenceTypes,
} from "wasm-feature-detect";
import type { RuffleHandle } from "../dist/ruffle_web";
import type { RuffleInstanceBuilder } from "../dist/ruffle_web";
import { setPolyfillsOnLoad } from "./js-polyfills";
import { publicPath } from "./public-path";
import { BaseLoadOptions } from "./load-options";
import { RufflePlayer } from "./ruffle-player";
declare global {
let __webpack_public_path__: string;
@ -30,13 +29,13 @@ type ProgressCallback = (bytesLoaded: number, bytesTotal: number) => void;
*
* @param config The `window.RufflePlayer.config` object.
* @param progressCallback The callback that will be run with Ruffle's download progress.
* @returns A ruffle constructor that may be used to create new Ruffle
* @returns A ruffle-builder constructor that may be used to create new RuffleInstanceBuilder
* instances.
*/
async function fetchRuffle(
config: BaseLoadOptions,
progressCallback?: ProgressCallback,
): Promise<typeof RuffleHandle> {
): Promise<typeof RuffleInstanceBuilder> {
// Apply some pure JavaScript polyfills to prevent conflicts with external
// libraries, if needed.
setPolyfillsOnLoad();
@ -66,7 +65,7 @@ async function fetchRuffle(
// 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?
const { default: init, RuffleHandle } = await (extensionsSupported
const { default: init, RuffleInstanceBuilder } = await (extensionsSupported
? import("../dist/ruffle_web-wasm_extensions")
: import("../dist/ruffle_web"));
let response;
@ -114,32 +113,28 @@ async function fetchRuffle(
await init(response);
return RuffleHandle;
return RuffleInstanceBuilder;
}
let nativeConstructor: Promise<typeof RuffleHandle> | null = null;
let nativeConstructor: Promise<typeof RuffleInstanceBuilder> | null = null;
/**
* Obtain an instance of `Ruffle`.
*
* This function returns a promise which yields `Ruffle` asynchronously.
* This function returns a promise which yields a new `RuffleInstanceBuilder` asynchronously.
*
* @param container The container that the resulting canvas will be added to.
* @param player The `RufflePlayer` object responsible for this instance of Ruffle.
* @param config The `window.RufflePlayer.config` object.
* @param progressCallback The callback that will be run with Ruffle's download progress.
* @returns A ruffle instance.
* @returns A ruffle instance builder.
*/
export async function loadRuffle(
container: HTMLElement,
player: RufflePlayer,
export async function createRuffleBuilder(
config: BaseLoadOptions,
progressCallback?: ProgressCallback,
): Promise<RuffleHandle> {
): Promise<RuffleInstanceBuilder> {
if (nativeConstructor === null) {
nativeConstructor = fetchRuffle(config, progressCallback);
}
const constructor = await nativeConstructor;
return new constructor(container, player, config);
return new constructor();
}

View File

@ -1,5 +1,5 @@
import type { RuffleHandle } from "../dist/ruffle_web";
import { loadRuffle } from "./load-ruffle";
import { createRuffleBuilder } from "./load-ruffle";
import { applyStaticStyles, ruffleShadowTemplate } from "./shadow-template";
import { lookupElement } from "./register-element";
import { DEFAULT_CONFIG } from "./config";
@ -17,6 +17,7 @@ import { buildInfo } from "./build-info";
import { text, textAsParagraphs } from "./i18n";
import JSZip from "jszip";
import { isExtension } from "./current-script";
import { configureBuilder } from "./internal/builder";
const RUFFLE_ORIGIN = "https://ruffle.rs";
const DIMENSION_REGEX = /^\s*(\d+(\.\d+)?(%)?)/;
@ -672,9 +673,7 @@ export class RufflePlayer extends HTMLElement {
'The configuration option contextMenu no longer takes a boolean. Use "on", "off", or "rightClickOnly".',
);
}
this.instance = await loadRuffle(
this.container,
this,
const builder = await createRuffleBuilder(
this.loadedConfig || {},
this.onRuffleDownloadProgress.bind(this),
).catch((e) => {
@ -715,6 +714,12 @@ export class RufflePlayer extends HTMLElement {
this.panic(e);
throw e;
});
configureBuilder(builder, this.loadedConfig || {});
this.instance = await builder.build(this.container, this).catch((e) => {
console.error(`Serious error loading Ruffle: ${e}`);
this.panic(e);
throw e;
});
if (this.loadedConfig?.fontSources) {
for (const url of this.loadedConfig.fontSources) {

View File

@ -0,0 +1,30 @@
import { strict as assert } from "assert";
import { parseColor, parseDuration } from "../src/internal/builder";
describe("Color parsing", function () {
it("should parse a valid RRGGBB hex, with hash", function () {
assert.strictEqual(parseColor("#A1B2C3"), 0xa1b2c3);
});
it("should parse a valid RRGGBB hex, without hash", function () {
assert.strictEqual(parseColor("#1A2B3C"), 0x1a2b3c);
});
it("should fail with not enough digits", function () {
assert.strictEqual(parseColor("123"), undefined);
});
it("should treat invalid hex as 0", function () {
assert.strictEqual(parseColor("#AX2Y3Z"), 0xa02030);
});
});
describe("Duration parsing", function () {
it("should accept number of seconds as number", function () {
assert.strictEqual(parseDuration(12.3), 12.3);
});
it("should accept a legacy style duration", function () {
assert.strictEqual(parseDuration({ secs: 12.3, nanos: 400000 }), 12.3);
});
});

278
web/src/builder.rs Normal file
View File

@ -0,0 +1,278 @@
use crate::{set_panic_hook, JavascriptPlayer, RuffleHandle, SocketProxy, RUFFLE_GLOBAL_PANIC};
use js_sys::Promise;
use ruffle_core::backend::navigator::OpenURLMode;
use ruffle_core::config::{Letterbox, NetworkingAccessMode};
use ruffle_core::{Color, PlayerRuntime, StageAlign, StageScaleMode};
use ruffle_render::quality::StageQuality;
use std::str::FromStr;
use std::time::Duration;
use wasm_bindgen::prelude::*;
use web_sys::HtmlElement;
#[wasm_bindgen(inspectable)]
#[derive(Debug, Clone)]
pub struct RuffleInstanceBuilder {
pub(crate) allow_script_access: bool,
pub(crate) background_color: Option<Color>,
pub(crate) letterbox: Letterbox,
pub(crate) upgrade_to_https: bool,
pub(crate) compatibility_rules: bool,
pub(crate) base_url: Option<String>,
pub(crate) show_menu: bool,
pub(crate) allow_fullscreen: bool,
pub(crate) stage_align: StageAlign,
pub(crate) force_align: bool,
pub(crate) quality: Option<StageQuality>,
pub(crate) scale: Option<StageScaleMode>,
pub(crate) force_scale: bool,
pub(crate) frame_rate: Option<f64>,
pub(crate) wmode: Option<String>, // TODO: Enumify? `Player` is working in strings here too...
pub(crate) log_level: tracing::Level,
pub(crate) max_execution_duration: Duration,
pub(crate) player_version: Option<u8>,
pub(crate) preferred_renderer: Option<String>, // TODO: Enumify?
pub(crate) open_url_mode: OpenURLMode,
pub(crate) allow_networking: NetworkingAccessMode,
pub(crate) socket_proxy: Vec<SocketProxy>,
pub(crate) credential_allow_list: Vec<String>,
pub(crate) player_runtime: PlayerRuntime,
// TODO: Add font related options
// TODO: Add volume
}
impl Default for RuffleInstanceBuilder {
fn default() -> Self {
// Anything available in `BaseLoadOptions` should match the default we list in the docs there.
// Some options may be variable (eg allowScriptAccess based on URL) -
// those should be always overriding these values in JS
Self {
allow_script_access: false,
background_color: None,
letterbox: Letterbox::Fullscreen,
upgrade_to_https: true,
compatibility_rules: true,
base_url: None,
show_menu: true,
allow_fullscreen: false,
stage_align: StageAlign::empty(),
force_align: false,
quality: None,
scale: None,
force_scale: false,
frame_rate: None,
wmode: None,
log_level: tracing::Level::ERROR,
max_execution_duration: Duration::from_secs_f64(15.0),
player_version: None,
preferred_renderer: None,
open_url_mode: OpenURLMode::Allow,
allow_networking: NetworkingAccessMode::All,
socket_proxy: vec![],
credential_allow_list: vec![],
player_runtime: PlayerRuntime::FlashPlayer,
}
}
}
#[wasm_bindgen]
impl RuffleInstanceBuilder {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self::default()
}
#[wasm_bindgen(js_name = "setAllowScriptAccess")]
pub fn set_allow_script_access(&mut self, value: bool) {
self.allow_script_access = value;
}
#[wasm_bindgen(js_name = "setBackgroundColor")]
pub fn set_background_color(&mut self, value: Option<u32>) {
self.background_color = value.map(|rgb| Color::from_rgb(rgb, 255));
}
#[wasm_bindgen(js_name = "setUpgradeToHttps")]
pub fn set_upgrade_to_https(&mut self, value: bool) {
self.upgrade_to_https = value;
}
#[wasm_bindgen(js_name = "setCompatibilityRules")]
pub fn set_compatibility_rules(&mut self, value: bool) {
self.compatibility_rules = value;
}
#[wasm_bindgen(js_name = "setLetterbox")]
pub fn set_letterbox(&mut self, value: &str) {
self.letterbox = match value {
"off" => Letterbox::Off,
"fullscreen" => Letterbox::Fullscreen,
"on" => Letterbox::On,
_ => return,
};
}
#[wasm_bindgen(js_name = "setBaseUrl")]
pub fn set_base_url(&mut self, value: Option<String>) {
self.base_url = value;
}
#[wasm_bindgen(js_name = "setShowMenu")]
pub fn set_show_menu(&mut self, value: bool) {
self.show_menu = value;
}
#[wasm_bindgen(js_name = "setAllowFullscreen")]
pub fn set_allow_fullscreen(&mut self, value: bool) {
self.allow_fullscreen = value;
}
#[wasm_bindgen(js_name = "setStageAlign")]
pub fn set_stage_align(&mut self, value: &str) {
// [NA] This is weird. Do we really need this?
// Chars get converted into flags.
// This means "tbbtlbltblbrllrbltlrtbl" is valid, resulting in "TBLR".
let mut align = StageAlign::default();
for c in value.bytes().map(|c| c.to_ascii_uppercase()) {
match c {
b'T' => align.insert(StageAlign::TOP),
b'B' => align.insert(StageAlign::BOTTOM),
b'L' => align.insert(StageAlign::LEFT),
b'R' => align.insert(StageAlign::RIGHT),
_ => (),
}
}
self.stage_align = align;
}
#[wasm_bindgen(js_name = "setForceAlign")]
pub fn set_force_align(&mut self, value: bool) {
self.force_align = value;
}
#[wasm_bindgen(js_name = "setQuality")]
pub fn set_quality(&mut self, value: &str) {
self.quality = match value {
"low" => Some(StageQuality::Low),
"medium" => Some(StageQuality::Medium),
"high" => Some(StageQuality::High),
"best" => Some(StageQuality::Best),
"8x8" => Some(StageQuality::High8x8),
"8x8linear" => Some(StageQuality::High8x8Linear),
"16x16" => Some(StageQuality::High16x16),
"16x16linear" => Some(StageQuality::High16x16Linear),
_ => return,
};
}
#[wasm_bindgen(js_name = "setScale")]
pub fn set_scale(&mut self, value: &str) {
self.scale = match value {
"exactfit" => Some(StageScaleMode::ExactFit),
"noborder" => Some(StageScaleMode::NoBorder),
"noscale" => Some(StageScaleMode::NoScale),
"showall" => Some(StageScaleMode::ShowAll),
_ => return,
};
}
#[wasm_bindgen(js_name = "setForceScale")]
pub fn set_force_scale(&mut self, value: bool) {
self.force_scale = value;
}
#[wasm_bindgen(js_name = "setFrameRate")]
pub fn set_frame_rate(&mut self, value: Option<f64>) {
self.frame_rate = value;
}
#[wasm_bindgen(js_name = "setWmode")]
pub fn set_wmode(&mut self, value: Option<String>) {
self.wmode = value;
}
#[wasm_bindgen(js_name = "setLogLevel")]
pub fn set_log_level(&mut self, value: &str) {
if let Ok(level) = tracing::Level::from_str(value) {
self.log_level = level;
}
}
#[wasm_bindgen(js_name = "setMaxExecutionDuration")]
pub fn set_max_execution_duration(&mut self, value: f64) {
self.max_execution_duration = Duration::from_secs_f64(value);
}
#[wasm_bindgen(js_name = "setPlayerVersion")]
pub fn set_player_version(&mut self, value: Option<u8>) {
self.player_version = value;
}
#[wasm_bindgen(js_name = "setPreferredRenderer")]
pub fn set_preferred_renderer(&mut self, value: Option<String>) {
self.preferred_renderer = value;
}
#[wasm_bindgen(js_name = "setOpenUrlMode")]
pub fn set_open_url_mode(&mut self, value: &str) {
self.open_url_mode = match value {
"allow" => OpenURLMode::Allow,
"confirm" => OpenURLMode::Confirm,
"deny" => OpenURLMode::Deny,
_ => return,
};
}
#[wasm_bindgen(js_name = "setAllowNetworking")]
pub fn set_allow_networking(&mut self, value: &str) {
self.allow_networking = match value {
"all" => NetworkingAccessMode::All,
"internal" => NetworkingAccessMode::Internal,
"none" => NetworkingAccessMode::None,
_ => return,
};
}
#[wasm_bindgen(js_name = "addSocketProxy")]
pub fn add_socket_proxy(&mut self, host: String, port: u16, proxy_url: String) {
self.socket_proxy.push(SocketProxy {
host,
port,
proxy_url,
})
}
#[wasm_bindgen(js_name = "setCredentialAllowList")]
pub fn set_credential_allow_list(&mut self, value: Vec<String>) {
self.credential_allow_list = value;
}
#[wasm_bindgen(js_name = "setPlayerRuntime")]
pub fn set_player_runtime(&mut self, value: &str) {
self.player_runtime = match value {
"air" => PlayerRuntime::AIR,
"flashPlayer" => PlayerRuntime::FlashPlayer,
_ => return,
};
}
// TODO: This should be split into two methods that either load url or load data
// Right now, that's done immediately afterwards in TS
pub async fn build(&self, parent: HtmlElement, js_player: JavascriptPlayer) -> Promise {
let copy = self.clone();
wasm_bindgen_futures::future_to_promise(async move {
if RUFFLE_GLOBAL_PANIC.is_completed() {
// If an actual panic happened, then we can't trust the state it left us in.
// Prevent future players from loading so that they can inform the user about the error.
return Err("Ruffle is panicking!".into());
}
set_panic_hook();
let ruffle = RuffleHandle::new_internal(parent, js_player, copy)
.await
.map_err(|err| JsValue::from(format!("Error creating player: {}", err)))?;
Ok(JsValue::from(ruffle))
})
}
}

View File

@ -3,6 +3,7 @@
//! Ruffle web frontend.
mod audio;
mod builder;
mod external_interface;
mod input;
mod log_adapter;
@ -10,20 +11,19 @@ mod navigator;
mod storage;
mod ui;
use crate::builder::RuffleInstanceBuilder;
use external_interface::{external_to_js_value, js_to_external_value, JavascriptInterface};
use input::{web_key_to_codepoint, web_to_ruffle_key_code, web_to_ruffle_text_control};
use js_sys::{Error as JsError, Promise, Uint8Array};
use ruffle_core::backend::navigator::OpenURLMode;
use js_sys::{Error as JsError, Uint8Array};
use ruffle_core::backend::ui::FontDefinition;
use ruffle_core::compatibility_rules::CompatibilityRules;
use ruffle_core::config::{Letterbox, NetworkingAccessMode};
use ruffle_core::config::NetworkingAccessMode;
use ruffle_core::context::UpdateContext;
use ruffle_core::events::{MouseButton, MouseWheelDelta, TextControlCode};
use ruffle_core::tag_utils::SwfMovie;
use ruffle_core::{swf, DefaultFont};
use ruffle_core::{
Color, Player, PlayerBuilder, PlayerEvent, PlayerRuntime, SandboxType, StageAlign,
StageScaleMode, StaticCallstack, ViewportDimensions,
Player, PlayerBuilder, PlayerEvent, SandboxType, StaticCallstack, ViewportDimensions,
};
use ruffle_render::quality::StageQuality;
use ruffle_video_software::backend::SoftwareVideoBackend;
@ -34,7 +34,6 @@ use std::rc::Rc;
use std::str::FromStr;
use std::sync::Once;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::{cell::RefCell, error::Error, num::NonZeroI32};
use tracing_subscriber::layer::{Layered, SubscriberExt};
use tracing_subscriber::registry::Registry;
@ -188,129 +187,7 @@ extern "C" {
fn display_unsupported_video(this: &JavascriptPlayer, url: &str);
}
fn deserialize_color<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
where
D: serde::Deserializer<'de>,
{
let color: Option<String> = serde::Deserialize::deserialize(deserializer)?;
let color = match color {
Some(color) => color,
None => return Ok(None),
};
// Parse classic HTML hex color (XXXXXX or #XXXXXX), attempting to match browser behavior.
// Optional leading #.
let color = color.strip_prefix('#').unwrap_or(&color);
// Fail if less than 6 digits.
let color = match color.get(..6) {
Some(color) => color,
None => return Ok(None),
};
let rgb = color.chars().fold(0, |acc, c| {
// Each char represents 4-bits. Invalid hex digit is allowed (converts to 0).
let digit = c.to_digit(16).unwrap_or_default();
(acc << 4) | digit
});
Ok(Some(Color::from_rgb(rgb, 255)))
}
fn deserialize_log_level<'de, D>(deserializer: D) -> Result<tracing::Level, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let value: String = serde::Deserialize::deserialize(deserializer)?;
tracing::Level::from_str(&value).map_err(Error::custom)
}
fn deserialize_player_runtime<'de, D>(deserializer: D) -> Result<PlayerRuntime, D::Error>
where
D: serde::Deserializer<'de>,
{
let value: String = serde::Deserialize::deserialize(deserializer)?;
Ok(match value.as_str() {
"air" => PlayerRuntime::AIR,
"flashPlayer" => PlayerRuntime::FlashPlayer,
_ => Default::default(),
})
}
fn deserialize_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
struct DurationVisitor;
impl<'de> serde::de::Visitor<'de> for DurationVisitor {
type Value = Duration;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str(
"Either a non-negative number (indicating seconds) or a {secs: number, nanos: number}."
)
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: Error,
{
Ok(Duration::from_secs_f64(if value < 1 {
1.0
} else {
value as f64
}))
}
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
where
E: Error,
{
Ok(Duration::from_secs_f64(if value.is_nan() || value < 1.0 {
1.0
} else {
value
}))
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut secs = None;
let mut nanos = None;
while let Some(key) = map.next_key::<String>()? {
let key_s = key.as_str();
match key_s {
"secs" => {
if secs.is_some() {
return Err(Error::duplicate_field("secs"));
}
secs = Some(map.next_value()?);
}
"nanos" => {
if nanos.is_some() {
return Err(Error::duplicate_field("nanos"));
}
nanos = Some(map.next_value()?);
}
_ => return Err(Error::unknown_field(key_s, &["secs", "nanos"])),
}
}
let secs = secs.ok_or_else(|| Error::missing_field("secs"))?;
let nanos = nanos.ok_or_else(|| Error::missing_field("nanos"))?;
Ok(Duration::new(secs, nanos))
}
}
deserializer.deserialize_any(DurationVisitor)
}
#[derive(Deserialize)]
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SocketProxy {
host: String,
@ -319,64 +196,6 @@ pub struct SocketProxy {
proxy_url: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Config {
allow_script_access: bool,
#[serde(deserialize_with = "deserialize_color")]
background_color: Option<Color>,
letterbox: Letterbox,
upgrade_to_https: bool,
compatibility_rules: bool,
#[serde(rename = "base")]
base_url: Option<String>,
#[serde(rename = "menu")]
show_menu: bool,
allow_fullscreen: bool,
salign: Option<String>,
force_align: bool,
quality: Option<String>,
scale: Option<String>,
force_scale: bool,
frame_rate: Option<f64>,
wmode: Option<String>,
#[serde(deserialize_with = "deserialize_log_level")]
log_level: tracing::Level,
#[serde(deserialize_with = "deserialize_duration")]
max_execution_duration: Duration,
player_version: Option<u8>,
preferred_renderer: Option<String>,
open_url_mode: OpenURLMode,
allow_networking: NetworkingAccessMode,
socket_proxy: Vec<SocketProxy>,
credential_allow_list: Vec<String>,
#[serde(deserialize_with = "deserialize_player_runtime")]
player_runtime: PlayerRuntime,
}
/// Metadata about the playing SWF file to be passed back to JavaScript.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
@ -394,27 +213,6 @@ struct MovieMetadata {
#[wasm_bindgen]
impl RuffleHandle {
#[allow(clippy::new_ret_no_self)]
#[wasm_bindgen(constructor)]
pub fn new(parent: HtmlElement, js_player: JavascriptPlayer, config: JsValue) -> Promise {
wasm_bindgen_futures::future_to_promise(async move {
let config: Config = serde_wasm_bindgen::from_value(config)
.map_err(|e| format!("Error parsing config: {e}"))?;
if RUFFLE_GLOBAL_PANIC.is_completed() {
// If an actual panic happened, then we can't trust the state it left us in.
// Prevent future players from loading so that they can inform the user about the error.
return Err("Ruffle is panicking!".into());
}
set_panic_hook();
let ruffle = Self::new_internal(parent, js_player, config)
.await
.map_err(|err| JsValue::from(format!("Error creating player: {}", err)))?;
Ok(JsValue::from(ruffle))
})
}
/// Stream an arbitrary movie file from (presumably) the Internet.
///
/// This method should only be called once per player.
@ -646,7 +444,7 @@ impl RuffleHandle {
async fn new_internal(
parent: HtmlElement,
js_player: JavascriptPlayer,
config: Config,
config: RuffleInstanceBuilder,
) -> Result<Self, Box<dyn Error>> {
// Redirect Log to Tracing if it isn't already
let _ = tracing_log::LogTracer::builder()
@ -730,63 +528,9 @@ impl RuffleHandle {
} else {
CompatibilityRules::empty()
})
.with_quality(
config
.quality
.and_then(|q| {
let quality = match q.to_ascii_lowercase().as_str() {
"low" => StageQuality::Low,
"medium" => StageQuality::Medium,
"high" => StageQuality::High,
"best" => StageQuality::Best,
"8x8" => StageQuality::High8x8,
"8x8linear" => StageQuality::High8x8Linear,
"16x16" => StageQuality::High16x16,
"16x16linear" => StageQuality::High16x16Linear,
_ => return None,
};
Some(quality)
})
.unwrap_or(default_quality),
)
.with_align(
config
.salign
.map(|s| {
// Chars get converted into flags.
// This means "tbbtlbltblbrllrbltlrtbl" is valid, resulting in "TBLR".
let mut align = StageAlign::default();
for c in s.bytes().map(|c| c.to_ascii_uppercase()) {
match c {
b'T' => align.insert(StageAlign::TOP),
b'B' => align.insert(StageAlign::BOTTOM),
b'L' => align.insert(StageAlign::LEFT),
b'R' => align.insert(StageAlign::RIGHT),
_ => (),
}
}
align
})
.unwrap_or_default(),
config.force_align,
)
.with_scale_mode(
config
.scale
.and_then(|s| {
let scale_mode = match s.to_ascii_lowercase().as_str() {
"exactfit" => StageScaleMode::ExactFit,
"noborder" => StageScaleMode::NoBorder,
"noscale" => StageScaleMode::NoScale,
"showall" => StageScaleMode::ShowAll,
_ => return None,
};
Some(scale_mode)
})
.unwrap_or_default(),
config.force_scale,
)
.with_quality(config.quality.unwrap_or(default_quality))
.with_align(config.stage_align, config.force_align)
.with_scale_mode(config.scale.unwrap_or_default(), config.force_scale)
.with_frame_rate(config.frame_rate)
// FIXME - should this be configurable?
.with_sandbox_type(SandboxType::Remote)
@ -1421,7 +1165,7 @@ pub enum RuffleInstanceError {
async fn create_renderer(
builder: PlayerBuilder,
document: &web_sys::Document,
config: &Config,
config: &RuffleInstanceBuilder,
) -> Result<(PlayerBuilder, HtmlCanvasElement), Box<dyn Error>> {
#[cfg(not(any(
feature = "canvas",