fix(servo): bound live protocol metadata
This commit is contained in:
@@ -10,11 +10,10 @@ use super::{
|
|||||||
ServoLiveError, ServoLiveFrame, ServoLivePermissionGrant,
|
ServoLiveError, ServoLiveFrame, ServoLivePermissionGrant,
|
||||||
wire::{
|
wire::{
|
||||||
LiveRequest, LiveResponse, LiveSurfaceHandle, MAX_FRAME_BYTE_COUNT, MAX_FRAME_DIMENSION,
|
LiveRequest, LiveResponse, LiveSurfaceHandle, MAX_FRAME_BYTE_COUNT, MAX_FRAME_DIMENSION,
|
||||||
|
MAX_RESPONSE_HEADER_BYTES,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_RESPONSE_HEADER_BYTES: usize = 256 * 1024;
|
|
||||||
|
|
||||||
pub(super) struct ServoLiveIpc {
|
pub(super) struct ServoLiveIpc {
|
||||||
requests: Option<Sender<IpcRequest>>,
|
requests: Option<Sender<IpcRequest>>,
|
||||||
thread: Option<JoinHandle<()>>,
|
thread: Option<JoinHandle<()>>,
|
||||||
@@ -257,6 +256,30 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reply_accepts_the_exact_header_limit() -> Result<(), ServoLiveError> {
|
||||||
|
let mut header = br#"{"protocol_version":3,"error":null,"frame":null}"#.to_vec();
|
||||||
|
header.resize(MAX_RESPONSE_HEADER_BYTES - 1, b' ');
|
||||||
|
header.push(b'\n');
|
||||||
|
|
||||||
|
let reply = read_reply(&mut Cursor::new(header))?;
|
||||||
|
|
||||||
|
assert_eq!(reply.protocol_version, Some(3));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reply_rejects_one_byte_over_the_header_limit() {
|
||||||
|
let mut header = br#"{"protocol_version":3,"error":null,"frame":null}"#.to_vec();
|
||||||
|
header.resize(MAX_RESPONSE_HEADER_BYTES, b' ');
|
||||||
|
header.push(b'\n');
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
read_reply(&mut Cursor::new(header)),
|
||||||
|
Err(ServoLiveError::ResponseHeaderTooLarge { limit: MAX_RESPONSE_HEADER_BYTES })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reply_parses_permission_consumption_without_a_frame() -> Result<(), ServoLiveError> {
|
fn reply_parses_permission_consumption_without_a_frame() -> Result<(), ServoLiveError> {
|
||||||
let profile_id = ely_domain::ProfileId::new();
|
let profile_id = ely_domain::ProfileId::new();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use super::ServoLiveSitePermission;
|
|||||||
pub(super) const LIVE_PROTOCOL_VERSION: u32 = 3;
|
pub(super) const LIVE_PROTOCOL_VERSION: u32 = 3;
|
||||||
pub(super) const MAX_FRAME_DIMENSION: u32 = 16_384;
|
pub(super) const MAX_FRAME_DIMENSION: u32 = 16_384;
|
||||||
pub(super) const MAX_FRAME_BYTE_COUNT: usize = 256 * 1024 * 1024;
|
pub(super) const MAX_FRAME_BYTE_COUNT: usize = 256 * 1024 * 1024;
|
||||||
|
pub(super) const MAX_RESPONSE_HEADER_BYTES: usize = 256 * 1024;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ pub use profile::{Profile, ProfileKind, ProfileSyncPolicy};
|
|||||||
pub use reading_list::{ReadingListEntry, ReadingProgress, ReadingProgressPercent};
|
pub use reading_list::{ReadingListEntry, ReadingProgress, ReadingProgressPercent};
|
||||||
pub use search::SearchEngine;
|
pub use search::SearchEngine;
|
||||||
pub use site_permission::{
|
pub use site_permission::{
|
||||||
SiteOrigin, SitePermissionAuditAction, SitePermissionAuditEvent, SitePermissionDecision,
|
MAX_SITE_ORIGIN_BYTES, SiteOrigin, SitePermissionAuditAction, SitePermissionAuditEvent,
|
||||||
SitePermissionEntry, SitePermissionFeature,
|
SitePermissionDecision, SitePermissionEntry, SitePermissionFeature,
|
||||||
};
|
};
|
||||||
pub use space::{
|
pub use space::{
|
||||||
ArchivePolicy, COLLAPSED_SIDEBAR_WIDTH_PX, DEFAULT_SIDEBAR_WIDTH_PX, HIDDEN_SIDEBAR_WIDTH_PX,
|
ArchivePolicy, COLLAPSED_SIDEBAR_WIDTH_PX, DEFAULT_SIDEBAR_WIDTH_PX, HIDDEN_SIDEBAR_WIDTH_PX,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use url::Url;
|
|||||||
|
|
||||||
use crate::{DomainError, ProfileId, UrlText};
|
use crate::{DomainError, ProfileId, UrlText};
|
||||||
|
|
||||||
|
pub const MAX_SITE_ORIGIN_BYTES: usize = 512;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
pub struct SiteOrigin(String);
|
pub struct SiteOrigin(String);
|
||||||
|
|
||||||
@@ -297,5 +299,36 @@ fn site_origin_from_url(url: &Url) -> Result<SiteOrigin, DomainError> {
|
|||||||
return Err(DomainError::InvalidSiteOrigin { value: url.to_string() });
|
return Err(DomainError::InvalidSiteOrigin { value: url.to_string() });
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SiteOrigin(url.origin().ascii_serialization()))
|
let origin = url.origin().ascii_serialization();
|
||||||
|
if origin.len() > MAX_SITE_ORIGIN_BYTES {
|
||||||
|
return Err(DomainError::InvalidSiteOrigin { value: origin });
|
||||||
|
}
|
||||||
|
Ok(SiteOrigin(origin))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn canonical_site_origin_accepts_512_bytes_and_rejects_513() -> Result<(), DomainError> {
|
||||||
|
let exact = SiteOrigin::parse(format!("https://{}", ascii_host(504)))?;
|
||||||
|
assert_eq!(exact.as_str().len(), MAX_SITE_ORIGIN_BYTES);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
SiteOrigin::parse(format!("https://{}", ascii_host(505))),
|
||||||
|
Err(DomainError::InvalidSiteOrigin { .. })
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ascii_host(mut bytes: usize) -> String {
|
||||||
|
let mut labels = Vec::new();
|
||||||
|
while bytes > 63 {
|
||||||
|
labels.push("a".repeat(63));
|
||||||
|
bytes -= 64;
|
||||||
|
}
|
||||||
|
labels.push("a".repeat(bytes));
|
||||||
|
labels.join(".")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{HashMap, VecDeque},
|
||||||
fs::{self, File, OpenOptions, TryLockError},
|
fs::{self, File, OpenOptions, TryLockError},
|
||||||
io::{self, BufRead},
|
io::{self, BufRead},
|
||||||
path::Path,
|
path::Path,
|
||||||
@@ -17,7 +17,7 @@ use super::{
|
|||||||
live_output::write_outcome,
|
live_output::write_outcome,
|
||||||
live_protocol::{
|
live_protocol::{
|
||||||
LIVE_PROTOCOL_VERSION, LiveFrameReport, LiveOutcome, LiveRequest, LiveSidecarError,
|
LIVE_PROTOCOL_VERSION, LiveFrameReport, LiveOutcome, LiveRequest, LiveSidecarError,
|
||||||
validated_frame_byte_count,
|
MAX_LOADED_URL_BYTES, MAX_PERMISSION_CONSUMPTIONS_PER_RESPONSE, validated_frame_byte_count,
|
||||||
},
|
},
|
||||||
live_session::{
|
live_session::{
|
||||||
LiveInput, LiveSession, apply_input, apply_layout, apply_permissions, bind_profile,
|
LiveInput, LiveSession, apply_input, apply_layout, apply_permissions, bind_profile,
|
||||||
@@ -55,6 +55,7 @@ pub(super) fn run(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
|||||||
let mut sessions = HashMap::new();
|
let mut sessions = HashMap::new();
|
||||||
let mut active_profile = None;
|
let mut active_profile = None;
|
||||||
let mut handshake_complete = false;
|
let mut handshake_complete = false;
|
||||||
|
let mut pending_permission_consumptions = VecDeque::new();
|
||||||
let stdin = io::stdin();
|
let stdin = io::stdin();
|
||||||
let mut stdout = io::stdout().lock();
|
let mut stdout = io::stdout().lock();
|
||||||
|
|
||||||
@@ -78,9 +79,13 @@ pub(super) fn run(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
|||||||
request,
|
request,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let outcome = outcome
|
pending_permission_consumptions.extend(host.take_consumed_permissions());
|
||||||
.map(|outcome| outcome.with_permission_consumptions(host.take_consumed_permissions()));
|
let outcome = match outcome {
|
||||||
write_outcome(&mut stdout, outcome)?;
|
Ok(outcome) => outcome,
|
||||||
|
Err(error) => LiveOutcome::error(error.to_string()),
|
||||||
|
}
|
||||||
|
.with_permission_consumptions(take_permission_batch(&mut pending_permission_consumptions));
|
||||||
|
write_outcome(&mut stdout, Ok(outcome))?;
|
||||||
if should_shutdown {
|
if should_shutdown {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -88,6 +93,11 @@ pub(super) fn run(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn take_permission_batch<T>(pending: &mut VecDeque<T>) -> Vec<T> {
|
||||||
|
let count = pending.len().min(MAX_PERMISSION_CONSUMPTIONS_PER_RESPONSE);
|
||||||
|
pending.drain(..count).collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn acquire_profile_data_lease(profile_data_dir: &Path) -> Result<File, LiveSidecarError> {
|
fn acquire_profile_data_lease(profile_data_dir: &Path) -> Result<File, LiveSidecarError> {
|
||||||
let lease = OpenOptions::new()
|
let lease = OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
@@ -110,7 +120,7 @@ fn handle_request(
|
|||||||
active_profile: &mut Option<ProfileId>,
|
active_profile: &mut Option<ProfileId>,
|
||||||
handshake_complete: &mut bool,
|
handshake_complete: &mut bool,
|
||||||
rendering_context_kind: RenderingContextKind,
|
rendering_context_kind: RenderingContextKind,
|
||||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))] hardware_transport: Option<
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))] mut hardware_transport: Option<
|
||||||
&mut HardwareSurfaceTransport,
|
&mut HardwareSurfaceTransport,
|
||||||
>,
|
>,
|
||||||
request: LiveRequest,
|
request: LiveRequest,
|
||||||
@@ -154,6 +164,9 @@ fn handle_request(
|
|||||||
pending_surface_ids,
|
pending_surface_ids,
|
||||||
} => {
|
} => {
|
||||||
validated_frame_byte_count(width, height)?;
|
validated_frame_byte_count(width, height)?;
|
||||||
|
if url.len() > MAX_LOADED_URL_BYTES {
|
||||||
|
return Err(LiveSidecarError::RequestUrlTooLong { limit: MAX_LOADED_URL_BYTES });
|
||||||
|
}
|
||||||
let tab = TabId::parse(tab_id.clone())?;
|
let tab = TabId::parse(tab_id.clone())?;
|
||||||
let profile = ProfileId::parse(profile_id)?;
|
let profile = ProfileId::parse(profile_id)?;
|
||||||
bind_profile(active_profile, &profile)?;
|
bind_profile(active_profile, &profile)?;
|
||||||
@@ -205,7 +218,16 @@ fn handle_request(
|
|||||||
rendering_context_kind,
|
rendering_context_kind,
|
||||||
&ready_surface_ids,
|
&ready_surface_ids,
|
||||||
&pending_surface_ids,
|
&pending_surface_ids,
|
||||||
)?;
|
);
|
||||||
|
retire_oversized_loaded_url_session(
|
||||||
|
host,
|
||||||
|
sessions,
|
||||||
|
&tab_id,
|
||||||
|
&outcome,
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
hardware_transport.as_deref_mut(),
|
||||||
|
);
|
||||||
|
let outcome = outcome?;
|
||||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
let outcome = {
|
let outcome = {
|
||||||
let mut outcome = outcome;
|
let mut outcome = outcome;
|
||||||
@@ -236,7 +258,16 @@ fn handle_request(
|
|||||||
rendering_context_kind,
|
rendering_context_kind,
|
||||||
&ready_surface_ids,
|
&ready_surface_ids,
|
||||||
&pending_surface_ids,
|
&pending_surface_ids,
|
||||||
)?;
|
);
|
||||||
|
retire_oversized_loaded_url_session(
|
||||||
|
host,
|
||||||
|
sessions,
|
||||||
|
&tab_id,
|
||||||
|
&outcome,
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
hardware_transport.as_deref_mut(),
|
||||||
|
);
|
||||||
|
let outcome = outcome?;
|
||||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
let outcome = {
|
let outcome = {
|
||||||
let mut outcome = outcome;
|
let mut outcome = outcome;
|
||||||
@@ -270,6 +301,27 @@ fn handle_request(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn retire_oversized_loaded_url_session(
|
||||||
|
host: &mut SoftwareServoHost,
|
||||||
|
sessions: &mut HashMap<String, LiveSession>,
|
||||||
|
tab_id: &str,
|
||||||
|
outcome: &Result<LiveOutcome, LiveSidecarError>,
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))] hardware_transport: Option<
|
||||||
|
&mut HardwareSurfaceTransport,
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
if !matches!(outcome, Err(LiveSidecarError::LoadedUrlTooLong { .. })) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(session) = sessions.remove(tab_id) {
|
||||||
|
host.close_webview(&session.webview_id);
|
||||||
|
}
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
if let Some(transport) = hardware_transport {
|
||||||
|
transport.close_tab(tab_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn poll_frame(
|
fn poll_frame(
|
||||||
host: &mut SoftwareServoHost,
|
host: &mut SoftwareServoHost,
|
||||||
session: &mut LiveSession,
|
session: &mut LiveSession,
|
||||||
@@ -303,7 +355,7 @@ fn poll_software_frame(
|
|||||||
{
|
{
|
||||||
let snapshot = host.snapshot_and_mark_metadata_observed(&session.webview_id)?;
|
let snapshot = host.snapshot_and_mark_metadata_observed(&session.webview_id)?;
|
||||||
let report =
|
let report =
|
||||||
LiveFrameReport::new(&snapshot, &frame, session.device_pixel_ratio(), false);
|
LiveFrameReport::new(&snapshot, &frame, session.device_pixel_ratio(), false)?;
|
||||||
return Ok(LiveOutcome::frame(report, frame));
|
return Ok(LiveOutcome::frame(report, frame));
|
||||||
}
|
}
|
||||||
return Ok(LiveOutcome::empty());
|
return Ok(LiveOutcome::empty());
|
||||||
@@ -313,7 +365,7 @@ fn poll_software_frame(
|
|||||||
let snapshot = host.snapshot_and_mark_metadata_observed(&session.webview_id)?;
|
let snapshot = host.snapshot_and_mark_metadata_observed(&session.webview_id)?;
|
||||||
let frame = host.last_rendered_frame()?;
|
let frame = host.last_rendered_frame()?;
|
||||||
session.last_frame = Some(frame.clone());
|
session.last_frame = Some(frame.clone());
|
||||||
let report = LiveFrameReport::new(&snapshot, &frame, session.device_pixel_ratio(), true);
|
let report = LiveFrameReport::new(&snapshot, &frame, session.device_pixel_ratio(), true)?;
|
||||||
Ok(LiveOutcome::frame(report, frame))
|
Ok(LiveOutcome::frame(report, frame))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +410,7 @@ fn poll_hardware_frame(
|
|||||||
identity.height,
|
identity.height,
|
||||||
session.device_pixel_ratio(),
|
session.device_pixel_ratio(),
|
||||||
false,
|
false,
|
||||||
);
|
)?;
|
||||||
return Ok(LiveOutcome::surface(report));
|
return Ok(LiveOutcome::surface(report));
|
||||||
}
|
}
|
||||||
HardwarePollAction::Empty => return Ok(LiveOutcome::empty()),
|
HardwarePollAction::Empty => return Ok(LiveOutcome::empty()),
|
||||||
@@ -380,7 +432,7 @@ fn poll_hardware_frame(
|
|||||||
identity.height,
|
identity.height,
|
||||||
session.device_pixel_ratio(),
|
session.device_pixel_ratio(),
|
||||||
true,
|
true,
|
||||||
);
|
)?;
|
||||||
Ok(LiveOutcome::surface(report))
|
Ok(LiveOutcome::surface(report))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,35 +466,6 @@ fn hardware_poll_action(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, feature = "hardware-render", target_os = "macos"))]
|
#[cfg(test)]
|
||||||
mod tests {
|
#[path = "live_tests.rs"]
|
||||||
use super::{HardwarePollAction, hardware_poll_action};
|
mod tests;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn newly_ready_surface_replays_before_pending_frame() {
|
|
||||||
assert_eq!(
|
|
||||||
hardware_poll_action(true, false, true, false, true, true),
|
|
||||||
HardwarePollAction::ReplaySurface
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
hardware_poll_action(true, false, false, false, true, true),
|
|
||||||
HardwarePollAction::PaintFrame
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn awaiting_ready_surface_backpressures_pending_frame() {
|
|
||||||
assert_eq!(
|
|
||||||
hardware_poll_action(true, true, false, false, true, false),
|
|
||||||
HardwarePollAction::Empty
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn missing_surface_replays_before_first_ready() {
|
|
||||||
assert_eq!(
|
|
||||||
hardware_poll_action(true, false, false, true, true, false),
|
|
||||||
HardwarePollAction::ReplaySurface
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use super::live_protocol::{LiveOutcome, LiveSidecarError, validated_frame_byte_count};
|
use super::live_protocol::{
|
||||||
|
LiveOutcome, LiveResponse, LiveSidecarError, MAX_ERROR_BYTES, MAX_RESPONSE_HEADER_BYTES,
|
||||||
|
MAX_TITLE_BYTES, validated_frame_byte_count,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TITLE_TRUNCATION_SUFFIX: &str = "…";
|
||||||
|
const ERROR_TRUNCATION_SUFFIX: &str = "… [truncated]";
|
||||||
|
|
||||||
pub(super) fn write_outcome(
|
pub(super) fn write_outcome(
|
||||||
stdout: &mut impl Write,
|
stdout: &mut impl Write,
|
||||||
@@ -17,8 +23,17 @@ pub(super) fn write_outcome(
|
|||||||
outcome = LiveOutcome::error(error.to_string());
|
outcome = LiveOutcome::error(error.to_string());
|
||||||
outcome.response.permission_consumptions = consumptions;
|
outcome.response.permission_consumptions = consumptions;
|
||||||
}
|
}
|
||||||
|
bound_response_text(&mut outcome.response);
|
||||||
|
|
||||||
serde_json::to_writer(&mut *stdout, &outcome.response)?;
|
let header = serde_json::to_vec(&outcome.response)?;
|
||||||
|
let wire_bytes = header.len().saturating_add(1);
|
||||||
|
if wire_bytes > MAX_RESPONSE_HEADER_BYTES {
|
||||||
|
return Err(LiveSidecarError::ResponseHeaderTooLarge {
|
||||||
|
bytes: wire_bytes,
|
||||||
|
limit: MAX_RESPONSE_HEADER_BYTES,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
stdout.write_all(&header)?;
|
||||||
stdout.write_all(b"\n")?;
|
stdout.write_all(b"\n")?;
|
||||||
if let Some(frame) = outcome.frame.as_ref() {
|
if let Some(frame) = outcome.frame.as_ref() {
|
||||||
stdout.write_all(frame.rgba_bytes())?;
|
stdout.write_all(frame.rgba_bytes())?;
|
||||||
@@ -27,6 +42,33 @@ pub(super) fn write_outcome(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bound_response_text(response: &mut LiveResponse) {
|
||||||
|
response.error = response
|
||||||
|
.error
|
||||||
|
.take()
|
||||||
|
.map(|error| truncate_utf8(error, MAX_ERROR_BYTES, ERROR_TRUNCATION_SUFFIX));
|
||||||
|
if let Some(frame) = response.frame.as_mut() {
|
||||||
|
frame.title = frame
|
||||||
|
.title
|
||||||
|
.take()
|
||||||
|
.map(|title| truncate_utf8(title, MAX_TITLE_BYTES, TITLE_TRUNCATION_SUFFIX));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate_utf8(mut value: String, limit: usize, suffix: &str) -> String {
|
||||||
|
if value.len() <= limit {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut end = limit - suffix.len();
|
||||||
|
while !value.is_char_boundary(end) {
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
value.truncate(end);
|
||||||
|
value.push_str(suffix);
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_frame(width: u32, height: u32, actual: usize) -> Result<(), LiveSidecarError> {
|
fn validate_frame(width: u32, height: u32, actual: usize) -> Result<(), LiveSidecarError> {
|
||||||
let expected = validated_frame_byte_count(width, height)?;
|
let expected = validated_frame_byte_count(width, height)?;
|
||||||
if expected != actual {
|
if expected != actual {
|
||||||
@@ -37,8 +79,107 @@ fn validate_frame(width: u32, height: u32, actual: usize) -> Result<(), LiveSide
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::super::live_protocol::{
|
||||||
|
LiveFrameReport, LivePermissionConsumption, MAX_LOADED_URL_BYTES,
|
||||||
|
MAX_PERMISSION_CONSUMPTIONS_PER_RESPONSE,
|
||||||
|
};
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overlong_utf8_title_stays_within_the_app_header_limit() -> Result<(), LiveSidecarError> {
|
||||||
|
let frame = one_pixel_frame();
|
||||||
|
let snapshot = snapshot(Some("https://example.com"), Some(&"界\"\n".repeat(40_000)));
|
||||||
|
let report = LiveFrameReport::new(&snapshot, &frame, 1.0, true)?;
|
||||||
|
let mut output = Vec::new();
|
||||||
|
|
||||||
|
write_outcome(&mut output, Ok(LiveOutcome::frame(report, frame)))?;
|
||||||
|
|
||||||
|
assert!(header_bytes(&output) <= MAX_RESPONSE_HEADER_BYTES);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wire_text_preserves_exact_boundary_and_marks_ascii_overflow() {
|
||||||
|
let exact = "a".repeat(MAX_TITLE_BYTES);
|
||||||
|
assert_eq!(truncate_utf8(exact.clone(), MAX_TITLE_BYTES, TITLE_TRUNCATION_SUFFIX), exact);
|
||||||
|
|
||||||
|
let overflow = truncate_utf8(
|
||||||
|
"a".repeat(MAX_TITLE_BYTES + 1),
|
||||||
|
MAX_TITLE_BYTES,
|
||||||
|
TITLE_TRUNCATION_SUFFIX,
|
||||||
|
);
|
||||||
|
assert_eq!(overflow.len(), MAX_TITLE_BYTES);
|
||||||
|
assert!(overflow.ends_with(TITLE_TRUNCATION_SUFFIX));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_text_truncates_on_a_utf8_boundary() {
|
||||||
|
let mut response = LiveOutcome::error("界".repeat(MAX_ERROR_BYTES)).response;
|
||||||
|
|
||||||
|
bound_response_text(&mut response);
|
||||||
|
|
||||||
|
let error = response.error.as_deref().unwrap_or_default();
|
||||||
|
assert!(error.len() <= MAX_ERROR_BYTES);
|
||||||
|
assert!(error.ends_with(ERROR_TRUNCATION_SUFFIX));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_heavy_maximum_frame_and_permission_batch_fit_header() -> Result<(), LiveSidecarError>
|
||||||
|
{
|
||||||
|
let frame = one_pixel_frame();
|
||||||
|
let report = LiveFrameReport {
|
||||||
|
loaded_url: Some("\u{2}".repeat(MAX_LOADED_URL_BYTES)),
|
||||||
|
title: Some("\u{1}".repeat(MAX_TITLE_BYTES)),
|
||||||
|
state: "complete",
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
device_pixel_ratio: 1.0,
|
||||||
|
css_viewport_width: 1,
|
||||||
|
css_viewport_height: 1,
|
||||||
|
rgba_byte_count: 4,
|
||||||
|
pixels_changed: true,
|
||||||
|
non_white_pixel_count: 0,
|
||||||
|
content_pixel_count: 0,
|
||||||
|
sample_hash: 0,
|
||||||
|
};
|
||||||
|
let mut outcome = LiveOutcome::frame(report, frame);
|
||||||
|
let profile_id = ely_domain::ProfileId::new().as_str().to_string();
|
||||||
|
outcome.response.permission_consumptions = (0..MAX_PERMISSION_CONSUMPTIONS_PER_RESPONSE)
|
||||||
|
.map(|revision| LivePermissionConsumption {
|
||||||
|
profile_id: profile_id.clone(),
|
||||||
|
origin: "\u{3}".repeat(ely_domain::MAX_SITE_ORIGIN_BYTES),
|
||||||
|
feature: "storage-persistence".to_string(),
|
||||||
|
grant_revision: revision as u64,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut output = Vec::new();
|
||||||
|
|
||||||
|
write_outcome(&mut output, Ok(outcome))?;
|
||||||
|
|
||||||
|
assert!(header_bytes(&output) <= MAX_RESPONSE_HEADER_BYTES);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loaded_url_preserves_exact_boundary_and_rejects_overflow() -> Result<(), LiveSidecarError> {
|
||||||
|
let prefix = "https://example.com/";
|
||||||
|
let exact_url = format!("{prefix}{}", "a".repeat(MAX_LOADED_URL_BYTES - prefix.len()));
|
||||||
|
assert!(url::Url::parse(&exact_url).is_ok());
|
||||||
|
let exact_snapshot = snapshot(Some(&exact_url), None);
|
||||||
|
let exact = LiveFrameReport::new(&exact_snapshot, &one_pixel_frame(), 1.0, true)?;
|
||||||
|
assert_eq!(exact.loaded_url.as_deref(), Some(exact_url.as_str()));
|
||||||
|
|
||||||
|
let overflow_url =
|
||||||
|
format!("{prefix}{}", "a".repeat(MAX_LOADED_URL_BYTES + 1 - prefix.len()));
|
||||||
|
assert!(url::Url::parse(&overflow_url).is_ok());
|
||||||
|
let overflow_snapshot = snapshot(Some(&overflow_url), None);
|
||||||
|
assert!(matches!(
|
||||||
|
LiveFrameReport::new(&overflow_snapshot, &one_pixel_frame(), 1.0, true),
|
||||||
|
Err(LiveSidecarError::LoadedUrlTooLong { limit: MAX_LOADED_URL_BYTES })
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mismatched_frame_becomes_header_only_error() -> Result<(), LiveSidecarError> {
|
fn mismatched_frame_becomes_header_only_error() -> Result<(), LiveSidecarError> {
|
||||||
let profile_id = ely_domain::ProfileId::new();
|
let profile_id = ely_domain::ProfileId::new();
|
||||||
@@ -52,8 +193,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
ely_servo_host::WebViewSnapshotPending::new(false, false),
|
ely_servo_host::WebViewSnapshotPending::new(false, false),
|
||||||
);
|
);
|
||||||
let report =
|
let report = LiveFrameReport::new(&snapshot, &frame, 1.0, true)?;
|
||||||
super::super::live_protocol::LiveFrameReport::new(&snapshot, &frame, 1.0, true);
|
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
let outcome = LiveOutcome::frame(report, frame).with_permission_consumptions(vec![
|
let outcome = LiveOutcome::frame(report, frame).with_permission_consumptions(vec![
|
||||||
ely_servo_host::ConsumedPermission {
|
ely_servo_host::ConsumedPermission {
|
||||||
@@ -74,4 +214,24 @@ mod tests {
|
|||||||
assert_eq!(response["permission_consumptions"][0]["grant_revision"], 7);
|
assert_eq!(response["permission_consumptions"][0]["grant_revision"], 7);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn one_pixel_frame() -> ely_servo_host::RenderedFrame {
|
||||||
|
ely_servo_host::RenderedFrame::from_rgba_bytes(1, 1, vec![255; 4])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot(url: Option<&str>, title: Option<&str>) -> ely_servo_host::WebViewSnapshot {
|
||||||
|
ely_servo_host::WebViewSnapshot::new(
|
||||||
|
ely_domain::WebViewId::new(),
|
||||||
|
ely_domain::TabId::new(),
|
||||||
|
ely_domain::ProfileId::new(),
|
||||||
|
ely_servo_host::WebViewState::Complete,
|
||||||
|
url.map(str::to_string),
|
||||||
|
title.map(str::to_string),
|
||||||
|
ely_servo_host::WebViewSnapshotPending::new(false, false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header_bytes(output: &[u8]) -> usize {
|
||||||
|
output.iter().position(|byte| *byte == b'\n').map_or(0, |end| end + 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ use super::iosurface_mach::IOSurfaceMachError;
|
|||||||
pub(super) const LIVE_PROTOCOL_VERSION: u32 = 3;
|
pub(super) const LIVE_PROTOCOL_VERSION: u32 = 3;
|
||||||
pub(super) const MAX_FRAME_DIMENSION: u32 = 16_384;
|
pub(super) const MAX_FRAME_DIMENSION: u32 = 16_384;
|
||||||
pub(super) const MAX_FRAME_BYTE_COUNT: usize = 256 * 1024 * 1024;
|
pub(super) const MAX_FRAME_BYTE_COUNT: usize = 256 * 1024 * 1024;
|
||||||
|
pub(super) const MAX_RESPONSE_HEADER_BYTES: usize = 256 * 1024;
|
||||||
|
pub(super) const MAX_TITLE_BYTES: usize = ely_servo_host::MAX_PAGE_TITLE_BYTES;
|
||||||
|
pub(super) const MAX_LOADED_URL_BYTES: usize = 32 * 1024;
|
||||||
|
pub(super) const MAX_ERROR_BYTES: usize = 32 * 1024;
|
||||||
|
pub(super) const MAX_PERMISSION_CONSUMPTIONS_PER_RESPONSE: usize = 8;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
@@ -208,12 +213,12 @@ impl LiveFrameReport {
|
|||||||
frame: &RenderedFrame,
|
frame: &RenderedFrame,
|
||||||
device_pixel_ratio: f32,
|
device_pixel_ratio: f32,
|
||||||
pixels_changed: bool,
|
pixels_changed: bool,
|
||||||
) -> Self {
|
) -> Result<Self, LiveSidecarError> {
|
||||||
let device_pixel_ratio = normalized_device_pixel_ratio(device_pixel_ratio);
|
let device_pixel_ratio = normalized_device_pixel_ratio(device_pixel_ratio);
|
||||||
let css_viewport_width = css_dimension(frame.width(), device_pixel_ratio);
|
let css_viewport_width = css_dimension(frame.width(), device_pixel_ratio);
|
||||||
let css_viewport_height = css_dimension(frame.height(), device_pixel_ratio);
|
let css_viewport_height = css_dimension(frame.height(), device_pixel_ratio);
|
||||||
Self {
|
Ok(Self {
|
||||||
loaded_url: snapshot.url().map(str::to_string),
|
loaded_url: loaded_url_for_response(snapshot.url())?,
|
||||||
title: snapshot.title().map(str::to_string),
|
title: snapshot.title().map(str::to_string),
|
||||||
state: state_label(snapshot.state()),
|
state: state_label(snapshot.state()),
|
||||||
width: frame.width(),
|
width: frame.width(),
|
||||||
@@ -226,7 +231,7 @@ impl LiveFrameReport {
|
|||||||
non_white_pixel_count: frame.non_white_pixel_count(),
|
non_white_pixel_count: frame.non_white_pixel_count(),
|
||||||
content_pixel_count: frame.content_pixel_count(),
|
content_pixel_count: frame.content_pixel_count(),
|
||||||
sample_hash: frame.sample_hash(),
|
sample_hash: frame.sample_hash(),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
@@ -236,10 +241,10 @@ impl LiveFrameReport {
|
|||||||
height: u32,
|
height: u32,
|
||||||
device_pixel_ratio: f32,
|
device_pixel_ratio: f32,
|
||||||
pixels_changed: bool,
|
pixels_changed: bool,
|
||||||
) -> Self {
|
) -> Result<Self, LiveSidecarError> {
|
||||||
let device_pixel_ratio = normalized_device_pixel_ratio(device_pixel_ratio);
|
let device_pixel_ratio = normalized_device_pixel_ratio(device_pixel_ratio);
|
||||||
Self {
|
Ok(Self {
|
||||||
loaded_url: snapshot.url().map(str::to_string),
|
loaded_url: loaded_url_for_response(snapshot.url())?,
|
||||||
title: snapshot.title().map(str::to_string),
|
title: snapshot.title().map(str::to_string),
|
||||||
state: state_label(snapshot.state()),
|
state: state_label(snapshot.state()),
|
||||||
width,
|
width,
|
||||||
@@ -252,7 +257,17 @@ impl LiveFrameReport {
|
|||||||
non_white_pixel_count: 0,
|
non_white_pixel_count: 0,
|
||||||
content_pixel_count: 0,
|
content_pixel_count: 0,
|
||||||
sample_hash: 0,
|
sample_hash: 0,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn loaded_url_for_response(value: Option<&str>) -> Result<Option<String>, LiveSidecarError> {
|
||||||
|
match value {
|
||||||
|
Some(url) if url.len() > MAX_LOADED_URL_BYTES => {
|
||||||
|
Err(LiveSidecarError::LoadedUrlTooLong { limit: MAX_LOADED_URL_BYTES })
|
||||||
|
}
|
||||||
|
Some(url) => Ok(Some(url.to_string())),
|
||||||
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +297,15 @@ pub(super) enum LiveSidecarError {
|
|||||||
#[error("live protocol mismatch: expected {expected}, received {actual}")]
|
#[error("live protocol mismatch: expected {expected}, received {actual}")]
|
||||||
ProtocolVersionMismatch { expected: u32, actual: u32 },
|
ProtocolVersionMismatch { expected: u32, actual: u32 },
|
||||||
|
|
||||||
|
#[error("request URL exceeds the {limit}-byte live protocol limit")]
|
||||||
|
RequestUrlTooLong { limit: usize },
|
||||||
|
|
||||||
|
#[error("loaded URL exceeds the {limit}-byte live protocol limit")]
|
||||||
|
LoadedUrlTooLong { limit: usize },
|
||||||
|
|
||||||
|
#[error("response header requires {bytes} bytes; the live protocol limit is {limit}")]
|
||||||
|
ResponseHeaderTooLarge { bytes: usize, limit: usize },
|
||||||
|
|
||||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
#[error("hardware rendering requires --iosurface-mach-service")]
|
#[error("hardware rendering requires --iosurface-mach-service")]
|
||||||
IOSurfaceMachServiceRequired,
|
IOSurfaceMachServiceRequired,
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use super::take_permission_batch;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nine_permission_consumptions_are_sent_in_order_across_two_responses() {
|
||||||
|
let mut pending = (1_u8..=9).collect::<VecDeque<_>>();
|
||||||
|
|
||||||
|
assert_eq!(take_permission_batch(&mut pending), (1_u8..=8).collect::<Vec<_>>());
|
||||||
|
assert_eq!(take_permission_batch(&mut pending), vec![9]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
mod hardware {
|
||||||
|
use super::super::{HardwarePollAction, hardware_poll_action};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn newly_ready_surface_replays_before_pending_frame() {
|
||||||
|
assert_eq!(
|
||||||
|
hardware_poll_action(true, false, true, false, true, true),
|
||||||
|
HardwarePollAction::ReplaySurface
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
hardware_poll_action(true, false, false, false, true, true),
|
||||||
|
HardwarePollAction::PaintFrame
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn awaiting_ready_surface_backpressures_pending_frame() {
|
||||||
|
assert_eq!(
|
||||||
|
hardware_poll_action(true, true, false, false, true, false),
|
||||||
|
HardwarePollAction::Empty
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_surface_replays_before_first_ready() {
|
||||||
|
assert_eq!(
|
||||||
|
hardware_poll_action(true, false, false, true, true, false),
|
||||||
|
HardwarePollAction::ReplaySurface
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ use ely_domain::{
|
|||||||
|
|
||||||
use crate::ServoHostError;
|
use crate::ServoHostError;
|
||||||
|
|
||||||
|
pub const MAX_PAGE_TITLE_BYTES: usize = 4 * 1024;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum WebViewState {
|
pub enum WebViewState {
|
||||||
Created,
|
Created,
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ pub use error::ServoHostError;
|
|||||||
#[cfg(feature = "hardware-render")]
|
#[cfg(feature = "hardware-render")]
|
||||||
pub use hardware_rendering_context::HardwareOffscreenContext;
|
pub use hardware_rendering_context::HardwareOffscreenContext;
|
||||||
pub use host::{
|
pub use host::{
|
||||||
ConsumedPermission, HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest,
|
ConsumedPermission, HidpiScaleRequest, KeyboardTextRequest, MAX_PAGE_TITLE_BYTES,
|
||||||
MouseDragRequest, MouseHoverRequest, NavigationRequest, PageZoomRequest, PermissionDecision,
|
MouseClickRequest, MouseDragRequest, MouseHoverRequest, NavigationRequest, PageZoomRequest,
|
||||||
PermissionSnapshotEntry, PermissionSnapshotRequest, PermissionSnapshotState, RenderedFrame,
|
PermissionDecision, PermissionSnapshotEntry, PermissionSnapshotRequest,
|
||||||
RenderedFrameSummary, ResizeRequest, ScrollRequest, ServoHost, TouchTapRequest,
|
PermissionSnapshotState, RenderedFrame, RenderedFrameSummary, ResizeRequest, ScrollRequest,
|
||||||
WebViewSnapshot, WebViewSnapshotPending, WebViewState,
|
ServoHost, TouchTapRequest, WebViewSnapshot, WebViewSnapshotPending, WebViewState,
|
||||||
};
|
};
|
||||||
pub use iosurface_handle::{IOSurfaceHandle, IOSurfaceIdentity};
|
pub use iosurface_handle::{IOSurfaceHandle, IOSurfaceIdentity};
|
||||||
#[cfg(feature = "servo-engine")]
|
#[cfg(feature = "servo-engine")]
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ use servo::{LoadStatus, RenderingContext, WebView, WebViewDelegate};
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
PermissionDecision, WebViewSnapshot, WebViewSnapshotPending, WebViewState,
|
MAX_PAGE_TITLE_BYTES, PermissionDecision, WebViewSnapshot, WebViewSnapshotPending,
|
||||||
|
WebViewState,
|
||||||
runtime_permissions::{PermissionStore, permission_decision_for_webview},
|
runtime_permissions::{PermissionStore, permission_decision_for_webview},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ impl HostWebView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn current_title(&self) -> Option<String> {
|
fn current_title(&self) -> Option<String> {
|
||||||
self.webview.page_title().or_else(|| self.delegate.title())
|
self.delegate.title().or_else(|| self.webview.page_title().map(bound_page_title))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +106,7 @@ impl HostWebViewDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn record_title_change(&self, title: Option<String>) {
|
fn record_title_change(&self, title: Option<String>) {
|
||||||
self.title.replace(title);
|
self.title.replace(title.map(bound_page_title));
|
||||||
self.has_pending_metadata.set(true);
|
self.has_pending_metadata.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +140,19 @@ impl HostWebViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bound_page_title(mut title: String) -> String {
|
||||||
|
if title.len() <= MAX_PAGE_TITLE_BYTES {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
let mut end = MAX_PAGE_TITLE_BYTES - "…".len();
|
||||||
|
while !title.is_char_boundary(end) {
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
title.truncate(end);
|
||||||
|
title.push('…');
|
||||||
|
title
|
||||||
|
}
|
||||||
|
|
||||||
impl WebViewDelegate for HostWebViewDelegate {
|
impl WebViewDelegate for HostWebViewDelegate {
|
||||||
fn notify_url_changed(&self, _webview: WebView, url: Url) {
|
fn notify_url_changed(&self, _webview: WebView, url: Url) {
|
||||||
self.record_url_change(url.to_string());
|
self.record_url_change(url.to_string());
|
||||||
@@ -209,4 +223,15 @@ mod tests {
|
|||||||
assert!(!delegate.has_pending_frame());
|
assert!(!delegate.has_pending_frame());
|
||||||
assert!(delegate.has_pending_metadata());
|
assert!(delegate.has_pending_metadata());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn snapshot_title_source_bounds_multibyte_metadata() {
|
||||||
|
let delegate = HostWebViewDelegate::new(ProfileId::new(), PermissionStore::default());
|
||||||
|
|
||||||
|
delegate.record_title_change(Some("界".repeat(super::MAX_PAGE_TITLE_BYTES)));
|
||||||
|
|
||||||
|
let title = delegate.title().unwrap_or_default();
|
||||||
|
assert!(title.len() <= super::MAX_PAGE_TITLE_BYTES);
|
||||||
|
assert!(title.ends_with('…'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,6 +190,81 @@ fn live_sidecar_rejects_oversized_frame_dimensions() -> Result<(), Box<dyn Error
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn live_sidecar_rejects_overlong_request_url_without_exiting() -> Result<(), Box<dyn Error>> {
|
||||||
|
let root = TestDirectory::new()?;
|
||||||
|
let profile_id = ProfileId::new();
|
||||||
|
let tab_id = TabId::new();
|
||||||
|
let mut sidecar = Sidecar::spawn(root.path())?;
|
||||||
|
let overlong_url = format!("https://example.com/{}", "a".repeat(32 * 1024));
|
||||||
|
|
||||||
|
let response = sidecar.exchange(&ensure_request(&tab_id, &profile_id, &overlong_url))?;
|
||||||
|
|
||||||
|
assert!(response.frame.is_none());
|
||||||
|
assert!(
|
||||||
|
response
|
||||||
|
.error
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|error| error == "request URL exceeds the 32768-byte live protocol limit")
|
||||||
|
);
|
||||||
|
sidecar.shutdown()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn live_sidecar_retires_a_session_with_an_overlong_loaded_url() -> Result<(), Box<dyn Error>> {
|
||||||
|
let server = TestServer::start()?;
|
||||||
|
let root = TestDirectory::new()?;
|
||||||
|
let profile_id = ProfileId::new();
|
||||||
|
let tab_id = TabId::new();
|
||||||
|
let mut sidecar = Sidecar::spawn(root.path())?;
|
||||||
|
let mut response = sidecar.exchange(&ensure_request(
|
||||||
|
&tab_id,
|
||||||
|
&profile_id,
|
||||||
|
&server.url("/oversized-history"),
|
||||||
|
))?;
|
||||||
|
let started_at = Instant::now();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if response.error.as_deref()
|
||||||
|
== Some("loaded URL exceeds the 32768-byte live protocol limit")
|
||||||
|
{
|
||||||
|
assert!(response.frame.is_none());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if started_at.elapsed() >= RESPONSE_TIMEOUT {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::TimedOut,
|
||||||
|
"timed out waiting for oversized loaded URL rejection",
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(2));
|
||||||
|
response = sidecar.exchange(&json!({ "type": "poll", "tab_id": tab_id.as_str() }))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut response =
|
||||||
|
sidecar.exchange(&ensure_request(&tab_id, &profile_id, &server.url("/white")))?;
|
||||||
|
let started_at = Instant::now();
|
||||||
|
while response.frame.as_ref().and_then(|frame| frame.title.as_deref()) != Some("white-ready") {
|
||||||
|
if let Some(error) = response.error {
|
||||||
|
return Err(io::Error::other(format!("replacement session failed: {error}")).into());
|
||||||
|
}
|
||||||
|
if started_at.elapsed() >= RESPONSE_TIMEOUT {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::TimedOut,
|
||||||
|
"timed out waiting for replacement session",
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(2));
|
||||||
|
response = sidecar.exchange(&json!({ "type": "poll", "tab_id": tab_id.as_str() }))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
sidecar.shutdown()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn live_sidecar_rejects_duplicate_permission_snapshot_entries() -> Result<(), Box<dyn Error>> {
|
fn live_sidecar_rejects_duplicate_permission_snapshot_entries() -> Result<(), Box<dyn Error>> {
|
||||||
let root = TestDirectory::new()?;
|
let root = TestDirectory::new()?;
|
||||||
|
|||||||
@@ -465,6 +465,8 @@ fn serve_connection(
|
|||||||
SET_PAGE
|
SET_PAGE
|
||||||
} else if path.starts_with("/history") {
|
} else if path.starts_with("/history") {
|
||||||
HISTORY_PAGE
|
HISTORY_PAGE
|
||||||
|
} else if path == "/oversized-history" {
|
||||||
|
OVERSIZED_HISTORY_PAGE
|
||||||
} else if path == "/white" {
|
} else if path == "/white" {
|
||||||
WHITE_PAGE
|
WHITE_PAGE
|
||||||
} else {
|
} else {
|
||||||
@@ -486,4 +488,6 @@ const READ_PAGE: &str = r#"<!doctype html><title>loading</title><style>body{font
|
|||||||
|
|
||||||
const HISTORY_PAGE: &str = r#"<!doctype html><title>loading</title><style>body{font:24px sans-serif;color:#111;background:#fff}</style><body>History mutation</body><script>history.replaceState({},'', '/history?state=1');document.title='history-ready';</script>"#;
|
const HISTORY_PAGE: &str = r#"<!doctype html><title>loading</title><style>body{font:24px sans-serif;color:#111;background:#fff}</style><body>History mutation</body><script>history.replaceState({},'', '/history?state=1');document.title='history-ready';</script>"#;
|
||||||
|
|
||||||
|
const OVERSIZED_HISTORY_PAGE: &str = r#"<!doctype html><title>oversized-history</title><script>history.replaceState({},'', '/oversized?' + 'a'.repeat(32768));</script>"#;
|
||||||
|
|
||||||
const WHITE_PAGE: &str = r#"<!doctype html><title>white-ready</title><style>html,body{margin:0;width:100%;height:100%;background:#fff}</style>"#;
|
const WHITE_PAGE: &str = r#"<!doctype html><title>white-ready</title><style>html,body{margin:0;width:100%;height:100%;background:#fff}</style>"#;
|
||||||
|
|||||||
Reference in New Issue
Block a user