Wire sidecar profile data isolation

This commit is contained in:
2026-05-08 22:10:10 -04:00
parent 2bd94710df
commit 8334fcbd6e
16 changed files with 523 additions and 141 deletions
@@ -4,7 +4,7 @@ use std::{
time::{Duration, Instant},
};
use ely_domain::{ProfileId, TabId};
use ely_domain::TabId;
use ely_servo_host::{
KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, ScrollRequest,
ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost, TouchTapRequest,
@@ -51,10 +51,13 @@ enum SidecarError {
}
fn run_snapshot(args: SnapshotArgs) -> Result<(), SidecarError> {
let mut host = SoftwareServoHost::new(ServoSurfaceSize::new(args.width, args.height))?;
std::fs::create_dir_all(&args.profile_data_dir)?;
let mut host = SoftwareServoHost::new_with_config_dir(
ServoSurfaceSize::new(args.width, args.height),
Some(args.profile_data_dir.clone()),
)?;
let tab_id = TabId::new();
let profile_id = ProfileId::new();
let webview_id = host.create_webview(tab_id.clone(), profile_id)?;
let webview_id = host.create_webview(tab_id.clone(), args.profile_id.clone())?;
host.navigate(NavigationRequest {
webview_id: webview_id.clone(),
@@ -1,6 +1,6 @@
use std::{env, num::ParseIntError, path::PathBuf};
use ely_domain::UrlText;
use ely_domain::{ProfileId, UrlText};
use thiserror::Error;
pub(super) enum SidecarCommand {
@@ -9,6 +9,8 @@ pub(super) enum SidecarCommand {
pub(super) struct SnapshotArgs {
pub(super) url: UrlText,
pub(super) profile_id: ProfileId,
pub(super) profile_data_dir: PathBuf,
pub(super) rgba_out: PathBuf,
pub(super) width: u32,
pub(super) height: u32,
@@ -69,8 +71,8 @@ pub(super) enum SidecarArgsError {
#[error("--touch-x and --touch-y must be provided together")]
IncompleteTouchPoint,
#[error("rgba output path is empty")]
EmptyRgbaOutputPath,
#[error("{name} path is empty")]
EmptyPath { name: &'static str },
#[error(transparent)]
Domain(#[from] ely_domain::DomainError),
@@ -98,6 +100,8 @@ fn parse_snapshot_args(
) -> Result<SnapshotArgs, SidecarArgsError> {
let mut args = args.into_iter();
let mut url = None;
let mut profile_id = None;
let mut profile_data_dir = None;
let mut rgba_out = None;
let mut width = None;
let mut height = None;
@@ -116,8 +120,17 @@ fn parse_snapshot_args(
while let Some(name) = args.next() {
match name.as_str() {
"--url" => url = Some(UrlText::parse(next_argument(&mut args, "--url")?)?),
"--profile-id" => {
profile_id = Some(ProfileId::parse(next_argument(&mut args, "--profile-id")?)?)
}
"--profile-data-dir" => {
profile_data_dir = Some(parse_path(
"--profile-data-dir",
next_argument(&mut args, "--profile-data-dir")?,
)?)
}
"--rgba-out" => {
rgba_out = Some(parse_output_path(next_argument(&mut args, "--rgba-out")?)?)
rgba_out = Some(parse_path("--rgba-out", next_argument(&mut args, "--rgba-out")?)?)
}
"--width" => {
width = Some(parse_dimension("--width", next_argument(&mut args, "--width")?)?)
@@ -207,6 +220,10 @@ fn parse_snapshot_args(
Ok(SnapshotArgs {
url: url.ok_or(SidecarArgsError::MissingRequiredArgument { name: "--url" })?,
profile_id: profile_id
.ok_or(SidecarArgsError::MissingRequiredArgument { name: "--profile-id" })?,
profile_data_dir: profile_data_dir
.ok_or(SidecarArgsError::MissingRequiredArgument { name: "--profile-data-dir" })?,
rgba_out: rgba_out
.ok_or(SidecarArgsError::MissingRequiredArgument { name: "--rgba-out" })?,
width: width.ok_or(SidecarArgsError::MissingRequiredArgument { name: "--width" })?,
@@ -248,10 +265,81 @@ fn parse_click_coordinate(name: &'static str, value: String) -> Result<u32, Side
value.parse::<u32>().map_err(|source| SidecarArgsError::InvalidInteger { name, value, source })
}
fn parse_output_path(value: String) -> Result<PathBuf, SidecarArgsError> {
fn parse_path(name: &'static str, value: String) -> Result<PathBuf, SidecarArgsError> {
if value.trim().is_empty() {
return Err(SidecarArgsError::EmptyRgbaOutputPath);
return Err(SidecarArgsError::EmptyPath { name });
}
Ok(PathBuf::from(value))
}
#[cfg(test)]
mod tests {
use std::{env, path::PathBuf};
use super::{SidecarArgsError, SidecarCommand, parse_command};
use ely_domain::{DomainError, ProfileId};
#[test]
fn parses_snapshot_profile_identity() -> Result<(), SidecarArgsError> {
let profile_id = ProfileId::new();
let profile_data_dir = env::temp_dir().join(profile_id.as_str());
let command = parse_snapshot_command(&profile_id, profile_data_dir.clone())?;
let SidecarCommand::Snapshot(args) = command;
assert_eq!(args.profile_id, profile_id);
assert_eq!(args.profile_data_dir, profile_data_dir);
Ok(())
}
#[test]
fn rejects_invalid_snapshot_profile_id() {
let command = parse_command(
[
"ely_servo_sidecar",
"snapshot",
"--url",
"https://example.com",
"--profile-id",
"profile_invalid",
"--profile-data-dir",
"/tmp/profile",
"--rgba-out",
"/tmp/frame.rgba",
"--width",
"64",
"--height",
"64",
]
.into_iter()
.map(str::to_string),
);
assert!(matches!(
command,
Err(SidecarArgsError::Domain(DomainError::InvalidEntityId { .. }))
));
}
fn parse_snapshot_command(
profile_id: &ProfileId,
profile_data_dir: PathBuf,
) -> Result<SidecarCommand, SidecarArgsError> {
parse_command([
"ely_servo_sidecar".to_string(),
"snapshot".to_string(),
"--url".to_string(),
"https://example.com".to_string(),
"--profile-id".to_string(),
profile_id.as_str().to_string(),
"--profile-data-dir".to_string(),
profile_data_dir.display().to_string(),
"--rgba-out".to_string(),
"/tmp/frame.rgba".to_string(),
"--width".to_string(),
"64".to_string(),
"--height".to_string(),
"64".to_string(),
])
}
}
@@ -14,6 +14,7 @@ pub(super) struct SnapshotInputChanges {
#[derive(Serialize)]
pub(super) struct SnapshotReport {
requested_url: String,
profile_id: String,
loaded_url: Option<String>,
title: Option<String>,
rgba_path: String,
@@ -52,6 +53,7 @@ impl SnapshotReport {
) -> Self {
Self {
requested_url: args.url.as_str().to_string(),
profile_id: snapshot.profile_id().as_str().to_string(),
loaded_url: snapshot.url().map(str::to_string),
title: snapshot.title().map(str::to_string),
rgba_path: args.rgba_out.display().to_string(),
+2
View File
@@ -7,6 +7,8 @@ mod runtime;
#[cfg(feature = "servo-engine")]
mod runtime_input;
#[cfg(feature = "servo-engine")]
mod runtime_permissions;
#[cfg(feature = "servo-engine")]
mod runtime_waker;
pub use error::ServoHostError;
+21 -21
View File
@@ -1,6 +1,7 @@
use std::{
cell::{Cell, RefCell},
collections::HashMap,
path::PathBuf,
rc::Rc,
sync::{
Arc,
@@ -13,7 +14,7 @@ use std::{
use dpi::PhysicalSize;
use ely_domain::{ProfileId, TabId, WebViewId};
use servo::{
DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePoint, DeviceVector2D, LoadStatus,
DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePoint, DeviceVector2D, LoadStatus, Opts,
RenderingContext, Scroll, Servo, ServoBuilder, WebView, WebViewBuilder, WebViewDelegate,
WebViewPoint, WebViewVector,
};
@@ -24,6 +25,7 @@ use crate::{
PermissionDecision, PermissionRequest, RenderedFrame, ResizeRequest, ScreenshotRequest,
ScrollRequest, ServoHost, ServoHostError, TouchTapRequest, WebViewSnapshot, WebViewState,
runtime_input::{send_keyboard_text, send_mouse_click, send_mouse_drag, send_touch_tap},
runtime_permissions::{PermissionKey, PermissionStore},
runtime_waker::ServoWakeFlag,
};
@@ -59,6 +61,13 @@ pub struct SoftwareServoHost {
impl SoftwareServoHost {
pub fn new(size: ServoSurfaceSize) -> Result<Self, ServoHostError> {
Self::new_with_config_dir(size, None)
}
pub fn new_with_config_dir(
size: ServoSurfaceSize,
config_dir: Option<PathBuf>,
) -> Result<Self, ServoHostError> {
if SERVO_RUNTIME_STARTED
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
@@ -66,14 +75,17 @@ impl SoftwareServoHost {
return Err(ServoHostError::RuntimeAlreadyStarted);
}
let host = Self::new_started(size);
let host = Self::new_started(size, config_dir);
if host.is_err() {
SERVO_RUNTIME_STARTED.store(false, Ordering::Release);
}
host
}
fn new_started(size: ServoSurfaceSize) -> Result<Self, ServoHostError> {
fn new_started(
size: ServoSurfaceSize,
config_dir: Option<PathBuf>,
) -> Result<Self, ServoHostError> {
let rendering_context = Rc::new(
servo::SoftwareRenderingContext::new(size.physical())
.map_err(|_| ServoHostError::RenderingContextUnavailable)?,
@@ -81,9 +93,12 @@ impl SoftwareServoHost {
rendering_context.make_current().map_err(|_| ServoHostError::RenderingContextNotCurrent)?;
let wake_requested = Arc::new(AtomicBool::new(false));
let servo = ServoBuilder::default()
.event_loop_waker(Box::new(ServoWakeFlag::new(wake_requested.clone())))
.build();
let mut builder = ServoBuilder::default()
.event_loop_waker(Box::new(ServoWakeFlag::new(wake_requested.clone())));
if let Some(config_dir) = config_dir {
builder = builder.opts(Opts { config_dir: Some(config_dir), ..Opts::default() });
}
let servo = builder.build();
Ok(Self {
servo,
@@ -481,18 +496,3 @@ impl WebViewDelegate for HostWebViewDelegate {
}
}
}
type PermissionStore = Rc<RefCell<HashMap<PermissionKey, PermissionDecision>>>;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct PermissionKey {
profile_id: ProfileId,
tab_id: TabId,
feature: String,
}
impl PermissionKey {
fn new(profile_id: ProfileId, tab_id: TabId, feature: String) -> Self {
Self { profile_id, tab_id, feature }
}
}
@@ -0,0 +1,20 @@
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use ely_domain::{ProfileId, TabId};
use crate::PermissionDecision;
pub(super) type PermissionStore = Rc<RefCell<HashMap<PermissionKey, PermissionDecision>>>;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub(super) struct PermissionKey {
profile_id: ProfileId,
tab_id: TabId,
feature: String,
}
impl PermissionKey {
pub(super) fn new(profile_id: ProfileId, tab_id: TabId, feature: String) -> Self {
Self { profile_id, tab_id, feature }
}
}