1pub mod bench;
4pub mod hydrolysis;
5
6use serde::de::{Error as DeError, Visitor as DeVisitor};
7use serde::{Deserialize, Serialize};
8use std::fmt;
9use std::str::FromStr;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12pub const PREVIEW_PROTOCOL_COMMIT: &str = env!("WATERUI_PREVIEW_PROTOCOL_COMMIT");
14
15#[must_use]
16pub fn protocol_info(waterui_core_fingerprint: impl Into<String>) -> PreviewProtocolInfo {
18 PreviewProtocolInfo {
19 build_commit: PREVIEW_PROTOCOL_COMMIT.to_string(),
20 waterui_core_fingerprint: waterui_core_fingerprint.into(),
21 platform: PreviewRuntimePlatform::current(),
22 }
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct PreviewProtocolInfo {
28 pub build_commit: String,
30 pub waterui_core_fingerprint: String,
32 pub platform: PreviewRuntimePlatform,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38pub enum PreviewRuntimePlatform {
39 Macos,
41 IosSimulator,
43 Ios,
45 Android,
47 Other,
49}
50
51impl PreviewRuntimePlatform {
52 #[must_use]
54 pub const fn current() -> Self {
55 if cfg!(target_os = "macos") {
56 Self::Macos
57 } else if cfg!(target_os = "ios") && cfg!(target_abi = "sim") {
58 Self::IosSimulator
59 } else if cfg!(target_os = "ios") {
60 Self::Ios
61 } else if cfg!(target_os = "android") {
62 Self::Android
63 } else {
64 Self::Other
65 }
66 }
67}
68
69pub mod registry {
70 use std::net::IpAddr;
73 use std::path::PathBuf;
74
75 use serde::{Deserialize, Serialize};
76
77 use super::{SystemTime, UNIX_EPOCH};
78
79 #[derive(Debug, Clone, Serialize, Deserialize)]
81 pub struct PreviewAppInstance {
82 pub pid: u32,
84 pub host: IpAddr,
86 pub port: u16,
88 pub waterui_core_fingerprint: String,
90 pub registered_at_unix_ms: u64,
92 }
93
94 impl PreviewAppInstance {
95 #[must_use]
96 pub fn new(
103 pid: u32,
104 host: IpAddr,
105 port: u16,
106 waterui_core_fingerprint: impl Into<String>,
107 ) -> Self {
108 Self {
109 pid,
110 host,
111 port,
112 waterui_core_fingerprint: waterui_core_fingerprint.into(),
113 registered_at_unix_ms: SystemTime::now()
114 .duration_since(UNIX_EPOCH)
115 .expect("system clock must not be earlier than the Unix epoch")
116 .as_millis()
117 .try_into()
118 .expect("preview registration timestamp must fit into u64"),
119 }
120 }
121 }
122
123 fn water_cache_dir() -> PathBuf {
124 if let Some(cache_dir) = std::env::var_os("WATER_CACHE_DIR") {
125 return PathBuf::from(cache_dir);
126 }
127
128 if let Some(cache_dir) = dirs::cache_dir() {
129 return cache_dir.join("waterui");
130 }
131
132 std::env::temp_dir().join("waterui-cache")
133 }
134
135 #[must_use]
136 pub fn preview_cache_root_dir() -> PathBuf {
138 water_cache_dir().join("preview")
139 }
140
141 #[must_use]
142 pub fn preview_instance_registry_dir() -> PathBuf {
144 preview_cache_root_dir().join("instances")
145 }
146
147 #[must_use]
148 pub fn preview_instance_registry_path(instance: &PreviewAppInstance) -> PathBuf {
150 preview_instance_registry_dir().join(format!("{}-{}.json", instance.pid, instance.port))
151 }
152}
153
154pub mod transport {
155 use std::io;
161
162 use futures_lite::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
163 use serde::Serialize;
164 use serde::de::DeserializeOwned;
165
166 pub const LEN_PREFIX_BYTES: usize = 4;
168
169 #[must_use]
177 pub fn max_frame_bytes() -> usize {
178 const DEFAULT: usize = 128 * 1024 * 1024;
179 match std::env::var("WATERUI_PREVIEW_MAX_FRAME_BYTES") {
180 Ok(value) => value.parse::<usize>().unwrap_or_else(|error| {
181 panic!("invalid WATERUI_PREVIEW_MAX_FRAME_BYTES value `{value}`: {error}")
182 }),
183 Err(std::env::VarError::NotPresent) => DEFAULT,
184 Err(std::env::VarError::NotUnicode(_)) => {
185 panic!("WATERUI_PREVIEW_MAX_FRAME_BYTES must be valid UTF-8")
186 }
187 }
188 }
189
190 pub async fn read_frame<R, T>(reader: &mut R) -> io::Result<T>
197 where
198 R: AsyncRead + Unpin + Send,
199 T: DeserializeOwned,
200 {
201 let mut len_buf = [0u8; LEN_PREFIX_BYTES];
202 reader.read_exact(&mut len_buf).await?;
203 let len = u32::from_be_bytes(len_buf) as usize;
204 let max = max_frame_bytes();
205 if len > max {
206 return Err(io::Error::new(
207 io::ErrorKind::InvalidData,
208 format!("preview frame too large: {len} bytes (max {max})"),
209 ));
210 }
211
212 let mut buf = vec![0u8; len];
213 reader.read_exact(&mut buf).await?;
214
215 let config = bincode::config::standard();
216 let (value, bytes_read): (T, usize) = bincode::serde::decode_from_slice(&buf, config)
217 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
218 if bytes_read != buf.len() {
219 return Err(io::Error::new(
220 io::ErrorKind::InvalidData,
221 "trailing bytes after preview frame payload",
222 ));
223 }
224 Ok(value)
225 }
226
227 pub async fn write_frame<W, T>(writer: &mut W, value: &T) -> io::Result<()>
234 where
235 W: AsyncWrite + Unpin + Send,
236 T: Serialize + Sync,
237 {
238 let config = bincode::config::standard();
239 let data = bincode::serde::encode_to_vec(value, config)
240 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
241 let len: u32 = data.len().try_into().map_err(|_| {
242 io::Error::new(
243 io::ErrorKind::InvalidData,
244 "preview frame too large for u32 length",
245 )
246 })?;
247
248 writer.write_all(&len.to_be_bytes()).await?;
249 writer.write_all(&data).await?;
250 writer.flush().await?;
251 Ok(())
252 }
253}
254
255pub mod tcp {
256 use std::net::{IpAddr, Ipv4Addr};
259 use std::ops::RangeInclusive;
260
261 use thiserror::Error;
262
263 pub const DEFAULT_HOST: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
265
266 pub const DEFAULT_PORT_START: u16 = 2106;
268
269 pub const DEFAULT_PORT_RANGE: u16 = 50;
271
272 #[derive(Debug, Clone, Copy)]
274 pub struct PreviewTcpConfig {
275 pub host: IpAddr,
277 pub port_start: u16,
279 pub port_range: u16,
281 }
282
283 impl PreviewTcpConfig {
284 #[must_use]
285 pub const fn default_localhost() -> Self {
287 Self {
288 host: DEFAULT_HOST,
289 port_start: DEFAULT_PORT_START,
290 port_range: DEFAULT_PORT_RANGE,
291 }
292 }
293
294 pub fn from_env() -> Result<Self, ConfigError> {
307 let mut cfg = Self::default_localhost();
308
309 if let Ok(host) = std::env::var("WATERUI_PREVIEW_HOST") {
310 cfg.host = host.parse().map_err(|_| ConfigError::InvalidHost)?;
311 }
312 if let Ok(port_start) = std::env::var("WATERUI_PREVIEW_PORT_START") {
313 cfg.port_start = port_start
314 .parse()
315 .map_err(|_| ConfigError::InvalidPortStart)?;
316 }
317 if let Ok(port_range) = std::env::var("WATERUI_PREVIEW_PORT_RANGE") {
318 cfg.port_range = port_range
319 .parse()
320 .map_err(|_| ConfigError::InvalidPortRange)?;
321 }
322
323 Ok(cfg)
324 }
325
326 #[must_use]
327 pub const fn ports(&self) -> RangeInclusive<u16> {
329 let end = self
330 .port_start
331 .saturating_add(self.port_range.saturating_sub(1));
332 self.port_start..=end
333 }
334 }
335
336 #[derive(Debug, Error)]
337 pub enum ConfigError {
339 #[error("invalid WATERUI_PREVIEW_HOST")]
340 InvalidHost,
342 #[error("invalid WATERUI_PREVIEW_PORT_START")]
343 InvalidPortStart,
345 #[error("invalid WATERUI_PREVIEW_PORT_RANGE")]
346 InvalidPortRange,
348 }
349}
350
351#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
353pub struct Size {
354 pub width: f32,
356 pub height: f32,
358}
359
360impl Size {
361 #[must_use]
363 pub const fn new(width: f32, height: f32) -> Self {
364 Self { width, height }
365 }
366}
367
368#[derive(Clone, Copy, PartialEq, Eq, Hash)]
370pub struct DylibId([u8; 32]);
371
372impl DylibId {
373 #[must_use]
374 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
376 Self(bytes)
377 }
378
379 #[must_use]
380 pub const fn as_bytes(&self) -> &[u8; 32] {
382 &self.0
383 }
384}
385
386impl fmt::Debug for DylibId {
387 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388 write!(f, "DylibId({self})")
389 }
390}
391
392impl fmt::Display for DylibId {
393 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
394 write!(f, "{}", hex::encode(self.0))
395 }
396}
397
398impl FromStr for DylibId {
399 type Err = &'static str;
400
401 fn from_str(s: &str) -> Result<Self, Self::Err> {
402 let bytes = hex::decode(s).map_err(|_| "invalid hex")?;
403 let bytes: [u8; 32] = bytes.try_into().map_err(|_| "expected 32 bytes")?;
404 Ok(Self(bytes))
405 }
406}
407
408impl Serialize for DylibId {
409 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
410 where
411 S: serde::Serializer,
412 {
413 serializer.serialize_str(&hex::encode(self.0))
414 }
415}
416
417impl<'de> Deserialize<'de> for DylibId {
418 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
419 where
420 D: serde::Deserializer<'de>,
421 {
422 struct Visitor;
423
424 impl DeVisitor<'_> for Visitor {
425 type Value = DylibId;
426
427 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
428 write!(f, "a 64-char hex string")
429 }
430
431 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
432 where
433 E: DeError,
434 {
435 let bytes = hex::decode(v).map_err(|_| E::custom("invalid hex"))?;
436 let bytes: [u8; 32] = bytes
437 .try_into()
438 .map_err(|_| E::custom("expected 32 bytes"))?;
439 Ok(DylibId(bytes))
440 }
441 }
442
443 deserializer.deserialize_str(Visitor)
444 }
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize)]
449pub enum DylibSource {
450 Bytes {
454 id: DylibId,
456 bytes: Vec<u8>,
458 },
459 Cached {
461 id: DylibId,
463 },
464 LocalPath {
469 id: DylibId,
471 path: std::path::PathBuf,
473 },
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize)]
478pub enum PreviewRequest {
479 Ping,
483 HasDylib {
485 id: DylibId,
487 },
488 Render {
490 dylib: DylibSource,
492 symbol: String,
494 frame: Size,
496 },
497 Shutdown,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct PreviewOutput {
504 pub png_data: Vec<u8>,
506 pub timings: PreviewRenderTimings,
508}
509
510#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
512pub struct PreviewDylibLoadTimings {
513 pub cache_file_ms: u64,
515 pub load_library_ms: u64,
517 pub initial_dlopen_ms: u64,
519 pub codesign_verify_ms: Option<u64>,
521 pub codesign_ms: Option<u64>,
523 pub reload_after_codesign_ms: Option<u64>,
525}
526
527#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
529pub struct PreviewRenderTimings {
530 pub ensure_dylib_cached_ms: u64,
532 pub dylib_load: Option<PreviewDylibLoadTimings>,
534 pub load_view_ms: u64,
536 pub render_ms: u64,
538 pub png_encode_ms: u64,
540 pub total_ms: u64,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
546pub enum PreviewError {
547 #[error("Unknown dylib id: {0}")]
549 UnknownDylibId(DylibId),
550 #[error("Failed to load dylib: {0}")]
552 DylibLoad(String),
553 #[error("Symbol not found: {0}")]
555 SymbolNotFound(String),
556 #[error("Render failed: {0}")]
558 RenderFailed(String),
559}
560
561#[derive(Debug, Clone, Serialize, Deserialize)]
563pub enum PreviewResponse {
564 Pong {
566 protocol: PreviewProtocolInfo,
568 },
569 HasDylib {
571 present: bool,
573 },
574 Render {
576 result: Result<PreviewOutput, PreviewError>,
578 },
579 Shutdown,
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586
587 #[test]
588 fn dylib_id_roundtrip_hex() {
589 let id = DylibId::from_bytes([0xAB; 32]);
590 let json = serde_json::to_string(&id).unwrap();
591 let de: DylibId = serde_json::from_str(&json).unwrap();
592 assert_eq!(id, de);
593 }
594}