diff options
Diffstat (limited to 'src/client/state.rs')
| -rw-r--r-- | src/client/state.rs | 619 |
1 files changed, 592 insertions, 27 deletions
diff --git a/src/client/state.rs b/src/client/state.rs index 83b07429..e6b8abf3 100644 --- a/src/client/state.rs +++ b/src/client/state.rs @@ -1,9 +1,39 @@ use super::*; +#[cfg(unix)] +const MAX_RETIRED_DIRECT_GRAPHICS: usize = 64; + +#[cfg(unix)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum RetiredDirectGraphicsMatch { + None, + Exact, + Saturated, +} + +#[cfg(unix)] +pub(super) struct RetiredDirectGraphics { + generation: u64, + transfers: Vec<(u64, u32)>, + saturated: bool, +} + +#[cfg(unix)] +impl RetiredDirectGraphics { + fn new(generation: u64) -> Self { + Self { + generation, + transfers: Vec::new(), + saturated: false, + } + } +} + /// State tracking for the thin client. pub(super) struct ClientState { /// Stateful semantic-frame encoder used when the server sends FrameData. pub(super) blit_encoder: render_ansi::BlitEncoder, + pub(super) image_files: image_files::FileTransport, pub(super) mouse_capture_active: bool, pub(super) endpoint_mouse_capture_requested: bool, pub(super) endpoint_sgr_pixels_requested: bool, @@ -24,10 +54,15 @@ pub(super) struct ClientState { #[cfg(unix)] pub(super) direct_graphics_response: Arc<Mutex<direct_graphics::ResponseMatcher>>, #[cfg(unix)] - pub(super) retired_direct_graphics: Option<(endpoint::ClientEndpointId, u64, u32)>, + pub(super) retired_direct_graphics: HashMap<endpoint::ClientEndpointId, RetiredDirectGraphics>, #[cfg(unix)] - pub(super) pending_surface_graphics: - HashMap<(endpoint::ClientEndpointId, u64, u32), crate::protocol::SurfaceGraphicsAssetKey>, + pub(super) disabled_native_graphics: HashMap<endpoint::ClientEndpointId, u64>, + pub(super) pending_native_cleanup: Vec<u8>, + #[cfg(unix)] + pub(super) pending_surface_graphics: HashMap< + (endpoint::ClientEndpointId, u64, u64, u32), + crate::protocol::SurfaceGraphicsAssetKey, + >, pub(super) attach_escape: Option<AttachEscapeState>, #[cfg(unix)] pub(super) mouse_scroll_lines: usize, @@ -63,6 +98,7 @@ impl ClientState { pub(super) fn test_new() -> Self { Self { blit_encoder: render_ansi::BlitEncoder::new(), + image_files: image_files::FileTransport::default(), mouse_capture_active: false, endpoint_mouse_capture_requested: false, endpoint_sgr_pixels_requested: false, @@ -81,7 +117,10 @@ impl ClientState { #[cfg(unix)] direct_graphics_response: Default::default(), #[cfg(unix)] - retired_direct_graphics: None, + retired_direct_graphics: HashMap::new(), + #[cfg(unix)] + disabled_native_graphics: Default::default(), + pending_native_cleanup: Vec::new(), #[cfg(unix)] pending_surface_graphics: HashMap::new(), attach_escape: None, @@ -162,11 +201,109 @@ impl ClientState { /// Present a composed error/chrome frame while retaining the handoff input freeze. The pane /// cells are still the last coherent surface; only client chrome (including the error) moves. - pub(super) fn present_frozen_chrome(&mut self, frame_data: FrameData) { + pub(super) fn present_frozen_chrome( + &mut self, + frame_data: impl Into<frame_output::ComposedFrame>, + ) { let frozen = self.presentation_frozen; + // Chrome can repaint the frozen source, but cannot retire its staged images. + let deferred_cleanup = frozen.then(|| std::mem::take(&mut self.pending_native_cleanup)); self.presentation_frozen = false; self.present_frame(frame_data); self.presentation_frozen = frozen; + if let Some(cleanup) = deferred_cleanup { + self.pending_native_cleanup = cleanup; + } + } + + #[cfg(unix)] + pub(super) fn queue_native_image_cleanup(&mut self, image_id: u32) { + crate::kitty_graphics::encode_delete_image(&mut self.pending_native_cleanup, image_id); + } + + /// Retirement is lifecycle bookkeeping, not a presentation effect. The caller has + /// already checked the endpoint generation, even for inactive/frozen owners. + #[cfg(unix)] + pub(super) fn receive_graphics_retirement( + &mut self, + endpoint_id: &endpoint::ClientEndpointId, + generation: u64, + transfer_id: u64, + image_id: u32, + owner_active: bool, + ) { + if transfer_id & crate::kitty_graphics::surface::NATIVE_TRANSFER_BIT != 0 { + self.disabled_native_graphics + .insert(endpoint_id.clone(), generation); + } + self.record_retired_direct_graphics(endpoint_id.clone(), generation, transfer_id, image_id); + let upload_pending = self + .pending_surface_graphics + .remove(&(endpoint_id.clone(), generation, transfer_id, image_id)) + .is_some(); + // Retirement cleanup is terminal-owned. A native retirement owns the + // ID only while its exact upload is pending: collision rejection and a + // late post-ACK retirement must not delete the currently visible bank. + // API/non-native retirement retains its prior ownership behavior. + let owns_image = self.queue_retired_graphics_cleanup( + transfer_id, + image_id, + upload_pending, + owner_active, + ); + let frame = if owns_image && owner_active { + let frozen = self.presentation_frozen; + let size = self.reported_size; + self.shell.as_mut().and_then(|shell| { + shell.retire_direct_graphics_image(image_id); + (!frozen).then(|| shell.compose(size.0, size.1)).flatten() + }) + } else { + None + }; + // A retirement repaint is a normal synchronized frame. In particular, + // do not extract its graphics into a graphics-only swap. + if let Some(frame) = frame { + self.present_frame(frame); + } + if let Ok(mut matcher) = self.direct_graphics_response.lock() { + matcher.retire(transfer_id); + } + } + + /// Returns whether a retirement owns an image that may be removed from the terminal/cache. + /// Native rejections can retire a proposed colliding ID without ever uploading it; only an + /// exact still-pending native upload gives that retirement ownership of the image. + #[cfg(unix)] + pub(super) fn queue_retired_graphics_cleanup( + &mut self, + transfer_id: u64, + image_id: u32, + upload_pending: bool, + owner_active: bool, + ) -> bool { + let native = transfer_id & crate::kitty_graphics::surface::NATIVE_TRANSFER_BIT != 0; + if native && !upload_pending { + return false; + } + if native || !owner_active { + self.queue_native_image_cleanup(image_id); + } + true + } + + #[cfg(unix)] + pub(super) fn flush_native_cleanup( + &mut self, + writer: &mut impl std::io::Write, + ) -> io::Result<()> { + if self.presentation_frozen || self.pending_native_cleanup.is_empty() { + return Ok(()); + } + writer.write_all(&self.pending_native_cleanup)?; + writer.flush()?; + self.pending_native_cleanup.clear(); + Ok(()) } pub(super) fn present_graphics(&mut self, graphics: &[u8]) { @@ -182,7 +319,10 @@ impl ClientState { &mut self, patch: shell::ClientComposedSurfacePatch, ) -> io::Result<bool> { - if self.presentation_frozen || self.repaint_pending { + if self.presentation_frozen + || self.repaint_pending + || (self.kitty_graphics_enabled && !self.pending_native_cleanup.is_empty()) + { crate::render_prof::event("client_surface_patch.fallback.repaint"); return Ok(false); } @@ -222,21 +362,28 @@ impl ClientState { } #[cfg(unix)] - pub(super) fn retire_endpoint_graphics(&mut self, endpoint_id: &endpoint::ClientEndpointId) { - let transfer_ids = self + fn retire_pending_endpoint_graphics( + &mut self, + endpoint_id: &endpoint::ClientEndpointId, + generation: Option<u64>, + ) { + let retired = self .pending_surface_graphics .keys() - .filter(|(owner, _, _)| owner == endpoint_id) - .map(|(_, transfer_id, _)| *transfer_id) + .filter(|(owner, pending_generation, _, _)| { + owner == endpoint_id + && generation.is_none_or(|generation| *pending_generation == generation) + }) + .cloned() .collect::<Vec<_>>(); - self.pending_surface_graphics - .retain(|(owner, _, _), _| owner != endpoint_id); - if self - .retired_direct_graphics - .as_ref() - .is_some_and(|(owner, _, _)| owner == endpoint_id) - { - self.retired_direct_graphics = None; + let mut transfer_ids = Vec::with_capacity(retired.len()); + for key in retired { + let (_, _, transfer_id, image_id) = &key; + if transfer_id & crate::kitty_graphics::surface::NATIVE_TRANSFER_BIT != 0 { + self.queue_native_image_cleanup(*image_id); + } + transfer_ids.push(*transfer_id); + self.pending_surface_graphics.remove(&key); } if let Ok(mut matcher) = self.direct_graphics_response.lock() { for transfer_id in transfer_ids { @@ -245,10 +392,162 @@ impl ClientState { } } - pub(super) fn present_frame(&mut self, frame_data: FrameData) { - if self.presentation_frozen { + #[cfg(unix)] + pub(super) fn record_retired_direct_graphics( + &mut self, + endpoint_id: endpoint::ClientEndpointId, + generation: u64, + transfer_id: u64, + image_id: u32, + ) { + let retired = self + .retired_direct_graphics + .entry(endpoint_id) + .or_insert_with(|| RetiredDirectGraphics::new(generation)); + if retired.generation != generation { + *retired = RetiredDirectGraphics::new(generation); + } + if retired.saturated || retired.transfers.contains(&(transfer_id, image_id)) { + return; + } + if retired.transfers.len() == MAX_RETIRED_DIRECT_GRAPHICS { + // Never evict an older tombstone: a delayed file for an evicted tuple could otherwise + // overwrite a visible image. Reject every unrecognized file until generation reset. + retired.saturated = true; return; } + retired.transfers.push((transfer_id, image_id)); + } + + #[cfg(unix)] + pub(super) fn match_retired_direct_graphics( + &mut self, + endpoint_id: &endpoint::ClientEndpointId, + generation: u64, + transfer_id: u64, + image_id: u32, + ) -> RetiredDirectGraphicsMatch { + let Some(retired) = self.retired_direct_graphics.get_mut(endpoint_id) else { + return RetiredDirectGraphicsMatch::None; + }; + if retired.generation != generation { + return RetiredDirectGraphicsMatch::None; + } + if let Some(index) = retired + .transfers + .iter() + .position(|transfer| *transfer == (transfer_id, image_id)) + { + retired.transfers.swap_remove(index); + if retired.transfers.is_empty() && !retired.saturated { + self.retired_direct_graphics.remove(endpoint_id); + } + return RetiredDirectGraphicsMatch::Exact; + } + if retired.saturated { + RetiredDirectGraphicsMatch::Saturated + } else { + RetiredDirectGraphicsMatch::None + } + } + + /// Forget graphics guards only for the connection generation which actually disconnected. + /// A delayed disconnect from an older generation must not clear its replacement's guards. + #[cfg(unix)] + pub(super) fn retire_endpoint_graphics( + &mut self, + endpoint_id: &endpoint::ClientEndpointId, + generation: u64, + ) { + self.retire_pending_endpoint_graphics(endpoint_id, Some(generation)); + if self.disabled_native_graphics.get(endpoint_id) == Some(&generation) { + self.disabled_native_graphics.remove(endpoint_id); + } + if self + .retired_direct_graphics + .get(endpoint_id) + .is_some_and(|retired| retired.generation == generation) + { + self.retired_direct_graphics.remove(endpoint_id); + } + } + + /// Reset bounded per-endpoint state when a replacement connection becomes authoritative. + #[cfg(unix)] + pub(super) fn start_endpoint_graphics_generation( + &mut self, + endpoint_id: &endpoint::ClientEndpointId, + generation: u64, + ) { + self.retire_pending_endpoint_graphics(endpoint_id, None); + if self.disabled_native_graphics.get(endpoint_id) != Some(&generation) { + self.disabled_native_graphics.remove(endpoint_id); + } + if self + .retired_direct_graphics + .get(endpoint_id) + .is_some_and(|retired| retired.generation != generation) + { + self.retired_direct_graphics.remove(endpoint_id); + } + } + + #[cfg(unix)] + pub(super) fn forget_endpoint_graphics(&mut self, endpoint_id: &endpoint::ClientEndpointId) { + self.retire_pending_endpoint_graphics(endpoint_id, None); + self.disabled_native_graphics.remove(endpoint_id); + self.retired_direct_graphics.remove(endpoint_id); + } + + pub(super) fn present_frame(&mut self, frame_data: impl Into<frame_output::ComposedFrame>) { + let _ = self.try_present_frame(frame_data); + } + + fn write_composed_output( + &mut self, + writer: &mut impl io::Write, + encoded: &[u8], + mut graphics: crate::kitty_graphics::GraphicsOutput, + ) -> io::Result<()> { + if self.kitty_graphics_enabled { + if !self.pending_native_cleanup.is_empty() { + graphics.operations.insert( + 0, + crate::kitty_graphics::GraphicsOperation::Bytes( + self.pending_native_cleanup.clone(), + ), + ); + } + frame_output::write_composed_frame( + writer.by_ref(), + encoded, + &graphics, + &mut self.image_files, + )?; + } else { + writer.write_all(encoded)?; + } + writer.flush()?; + if self.kitty_graphics_enabled { + self.pending_native_cleanup.clear(); + } + Ok(()) + } + + /// Presents and commits a frame only after all terminal output has been written successfully. + /// Callers which acknowledge presentation-sensitive work use the return value rather than + /// treating composition as presentation. + pub(super) fn try_present_frame( + &mut self, + frame_data: impl Into<frame_output::ComposedFrame>, + ) -> bool { + if self.presentation_frozen { + return false; + } + let frame_output::ComposedFrame { + frame: frame_data, + graphics, + } = frame_data.into(); let frame_data = if self.draw_host_cursor { render_ansi::frame_with_drawn_cursor(frame_data) } else { @@ -261,14 +560,280 @@ impl ClientState { self.blit_encoder.encode(&frame_data, self.repaint_pending) }; let mut stdout = io::stdout(); - let graphics = if self.kitty_graphics_enabled { - frame_data.graphics.as_slice() - } else { - &[] - }; - let _ = write_encoded_frame_with_graphics(&mut stdout, &encoded.bytes, graphics); - let _ = stdout.flush(); + if let Err(error) = self.write_composed_output(&mut stdout, &encoded.bytes, graphics) { + tracing::warn!(%error, "failed to present client frame"); + self.repaint_pending = true; + return false; + } self.blit_encoder.commit(frame_data, encoded); self.repaint_pending = false; + true + } +} + +#[cfg(all(test, unix))] +mod native_cleanup_tests { + use super::*; + + #[test] + fn retired_graphics_tombstones_survive_handoff_and_only_exact_files_consume_them() { + let mut state = ClientState::test_new(); + let endpoint = endpoint::ClientEndpointId::Local; + state.record_retired_direct_graphics(endpoint.clone(), 7, 11, 1234); + state.record_retired_direct_graphics(endpoint.clone(), 7, 12, 5678); + state.disabled_native_graphics.insert(endpoint.clone(), 7); + + // Switching away and back does not reset guards, and an unrelated queued file cannot + // consume either of two sequential API retirement tombstones. + assert_eq!( + state.match_retired_direct_graphics(&endpoint, 7, 13, 9999), + RetiredDirectGraphicsMatch::None + ); + assert_eq!( + state + .retired_direct_graphics + .get(&endpoint) + .unwrap() + .transfers + .len(), + 2 + ); + assert_eq!(state.disabled_native_graphics.get(&endpoint), Some(&7)); + assert_eq!( + state.match_retired_direct_graphics(&endpoint, 7, 11, 1234), + RetiredDirectGraphicsMatch::Exact + ); + assert!(state + .retired_direct_graphics + .get(&endpoint) + .unwrap() + .transfers + .contains(&(12, 5678))); + assert_eq!( + state.match_retired_direct_graphics(&endpoint, 7, 12, 5678), + RetiredDirectGraphicsMatch::Exact + ); + assert!(!state.retired_direct_graphics.contains_key(&endpoint)); + } + + #[test] + fn replacement_generation_resets_bounded_graphics_guards() { + let mut state = ClientState::test_new(); + let endpoint = endpoint::ClientEndpointId::Local; + let native = crate::kitty_graphics::surface::NATIVE_TRANSFER_BIT | 11; + state.record_retired_direct_graphics(endpoint.clone(), 7, native, 1234); + state.disabled_native_graphics.insert(endpoint.clone(), 7); + + state.start_endpoint_graphics_generation(&endpoint, 8); + + assert_eq!( + state.match_retired_direct_graphics(&endpoint, 7, native, 1234), + RetiredDirectGraphicsMatch::None + ); + assert!(!state.disabled_native_graphics.contains_key(&endpoint)); + } + + #[test] + fn retired_graphics_tombstone_overflow_fails_closed_until_generation_reset() { + let mut state = ClientState::test_new(); + let endpoint = endpoint::ClientEndpointId::Local; + for transfer_id in 0..=MAX_RETIRED_DIRECT_GRAPHICS as u64 { + state.record_retired_direct_graphics( + endpoint.clone(), + 7, + transfer_id, + transfer_id as u32, + ); + } + let retired = state.retired_direct_graphics.get(&endpoint).unwrap(); + assert_eq!(retired.transfers.len(), MAX_RETIRED_DIRECT_GRAPHICS); + assert!(retired.saturated); + assert_eq!( + state.match_retired_direct_graphics(&endpoint, 7, 1, 1), + RetiredDirectGraphicsMatch::Exact + ); + assert_eq!( + state.match_retired_direct_graphics(&endpoint, 7, 10_000, 10_000), + RetiredDirectGraphicsMatch::Saturated + ); + assert!( + state + .retired_direct_graphics + .get(&endpoint) + .unwrap() + .saturated + ); + + state.start_endpoint_graphics_generation(&endpoint, 8); + assert!(!state.retired_direct_graphics.contains_key(&endpoint)); + state.record_retired_direct_graphics(endpoint.clone(), 8, 1, 2); + assert_eq!( + state.match_retired_direct_graphics(&endpoint, 8, 9, 9), + RetiredDirectGraphicsMatch::None + ); + assert_eq!( + state.match_retired_direct_graphics(&endpoint, 8, 1, 2), + RetiredDirectGraphicsMatch::Exact + ); + } + + #[test] + fn pending_none_native_retirement_does_not_touch_cleanup_or_shell_cache() { + let mut state = ClientState::test_new(); + state.pending_native_cleanup.extend_from_slice(b"existing"); + let transfer = crate::kitty_graphics::surface::NATIVE_TRANSFER_BIT | 7; + + assert!(!state.queue_retired_graphics_cleanup(transfer, 1234, false, true)); + assert_eq!(state.pending_native_cleanup, b"existing"); + assert!(state + .shell + .as_mut() + .expect("test shell") + .take_pending_graphics_cleanup() + .is_empty()); + } + + #[test] + fn queued_cleanup_is_synchronized_and_respects_graphics_capability() { + let mut state = ClientState::test_new(); + state.kitty_graphics_enabled = true; + state.queue_native_image_cleanup(42); + let encoded = b"\x1b[?2026htext\x1b[?2026l"; + let mut output = Vec::new(); + state + .write_composed_output(&mut output, encoded, Default::default()) + .unwrap(); + let text = String::from_utf8(output).unwrap(); + assert!(text.find("\x1b[?2026h").unwrap() < text.find("a=d,d=I,i=42").unwrap()); + assert!(text.find("a=d,d=I,i=42").unwrap() < text.find("\x1b[?2026l").unwrap()); + assert!(state.pending_native_cleanup.is_empty()); + state.kitty_graphics_enabled = false; + state.queue_native_image_cleanup(43); + let mut output = Vec::new(); + state + .write_composed_output(&mut output, encoded, Default::default()) + .unwrap(); + assert_eq!(output, encoded); + assert!(!state.pending_native_cleanup.is_empty()); + state.kitty_graphics_enabled = true; + let mut no_capacity = &mut [][..]; + assert!(state + .write_composed_output(&mut no_capacity, encoded, Default::default()) + .is_err()); + assert!(!state.pending_native_cleanup.is_empty()); + } + + #[test] + fn active_api_retirement_leaves_cleanup_to_composition() { + let mut state = ClientState::test_new(); + assert!(state.queue_retired_graphics_cleanup(7, 1234, true, true)); + assert!(state.pending_native_cleanup.is_empty()); + assert!(state.queue_retired_graphics_cleanup(8, 5678, true, false)); + assert!(!state.pending_native_cleanup.is_empty()); + } + + #[test] + fn unacknowledged_native_upload_cleanup_survives_frozen_disconnect() { + let mut state = ClientState::test_new(); + let endpoint = endpoint::ClientEndpointId::Local; + let key = crate::protocol::SurfaceGraphicsAssetKey { + source: crate::protocol::SurfaceGraphicsSource::Terminal { + target: crate::protocol::SurfaceGraphicsTarget::Pane { + pane_id: "p".into(), + }, + image_id: 1, + }, + image_width: 1, + image_height: 1, + format: crate::protocol::SurfaceGraphicsFormat::Rgba, + data_len: 4, + data_fingerprint: 1, + }; + let transfer = crate::kitty_graphics::surface::NATIVE_TRANSFER_BIT | 2; + state + .pending_surface_graphics + .insert((endpoint.clone(), 9, transfer, 1234), key); + state.presentation_frozen = true; + state.retire_endpoint_graphics(&endpoint, 9); + assert!(state.pending_surface_graphics.is_empty()); + let mut output = Vec::new(); + state.flush_native_cleanup(&mut output).unwrap(); + assert!(output.is_empty()); + assert!(!state.pending_native_cleanup.is_empty()); + state.unfreeze_presentation(); + state.flush_native_cleanup(&mut output).unwrap(); + assert!(String::from_utf8(output.clone()) + .unwrap() + .contains("a=d,d=I,i=1234")); + state.flush_native_cleanup(&mut output).unwrap(); + assert_eq!( + String::from_utf8(output).unwrap().matches("a=d,").count(), + 1 + ); + } + + #[test] + fn unacknowledged_native_upload_retirement_survives_frozen_handoff() { + let mut state = ClientState::test_new(); + let endpoint = endpoint::ClientEndpointId::Local; + let key = crate::protocol::SurfaceGraphicsAssetKey { + source: crate::protocol::SurfaceGraphicsSource::Terminal { + target: crate::protocol::SurfaceGraphicsTarget::Pane { + pane_id: "p".into(), + }, + image_id: 1, + }, + image_width: 1, + image_height: 1, + format: crate::protocol::SurfaceGraphicsFormat::Rgba, + data_len: 4, + data_fingerprint: 1, + }; + let transfer = crate::kitty_graphics::surface::NATIVE_TRANSFER_BIT | 2; + state + .pending_surface_graphics + .insert((endpoint.clone(), 9, transfer, 1234), key); + state.presentation_frozen = true; + // The source connection is still live, but no longer owns the active shell. + state.receive_graphics_retirement(&endpoint, 8, transfer, 1234, false); + assert_eq!(state.pending_surface_graphics.len(), 1); + assert!(state.pending_native_cleanup.is_empty()); + state.receive_graphics_retirement(&endpoint, 9, transfer, 1234, false); + assert_eq!( + state.match_retired_direct_graphics(&endpoint, 9, transfer, 1234), + RetiredDirectGraphicsMatch::Exact + ); + assert!(state + .shell + .as_mut() + .unwrap() + .take_pending_graphics_cleanup() + .is_empty()); + assert!(state.pending_surface_graphics.is_empty()); + let mut output = Vec::new(); + state.flush_native_cleanup(&mut output).unwrap(); + assert!(output.is_empty()); + assert!(!state.pending_native_cleanup.is_empty()); + let cleanup = state.pending_native_cleanup.clone(); + let frame = crate::protocol::FrameData::from_ratatui_buffer( + &ratatui::buffer::Buffer::empty(ratatui::layout::Rect::new(0, 0, 1, 1)), + None, + ); + state.kitty_graphics_enabled = true; + assert!(!state.try_present_frame(frame.clone())); + assert_eq!(state.pending_native_cleanup, cleanup); + state.present_frozen_chrome(frame); + assert!(state.presentation_frozen); + assert_eq!(state.pending_native_cleanup, cleanup); + state.unfreeze_presentation(); + state.flush_native_cleanup(&mut output).unwrap(); + assert!(String::from_utf8(output.clone()) + .unwrap() + .contains("a=d,d=I,i=1234")); + state.flush_native_cleanup(&mut output).unwrap(); + assert_eq!( + String::from_utf8(output).unwrap().matches("a=d,").count(), + 1 + ); } } |
