pub mod bench;
pub mod hydrolysis;
use serde::de::{Error as DeError, Visitor as DeVisitor};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
pub const PREVIEW_PROTOCOL_COMMIT: &str = env!("WATERUI_PREVIEW_PROTOCOL_COMMIT");
#[must_use]
pub fn protocol_info(waterui_core_fingerprint: impl Into<String>) -> PreviewProtocolInfo {
PreviewProtocolInfo {
build_commit: PREVIEW_PROTOCOL_COMMIT.to_string(),
waterui_core_fingerprint: waterui_core_fingerprint.into(),
platform: PreviewRuntimePlatform::current(),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreviewProtocolInfo {
pub build_commit: String,
pub waterui_core_fingerprint: String,
pub platform: PreviewRuntimePlatform,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PreviewRuntimePlatform {
Macos,
IosSimulator,
Ios,
Android,
Other,
}
impl PreviewRuntimePlatform {
#[must_use]
pub const fn current() -> Self {
if cfg!(target_os = "macos") {
Self::Macos
} else if cfg!(target_os = "ios") && cfg!(target_abi = "sim") {
Self::IosSimulator
} else if cfg!(target_os = "ios") {
Self::Ios
} else if cfg!(target_os = "android") {
Self::Android
} else {
Self::Other
}
}
}
pub mod registry {
use std::net::IpAddr;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use super::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreviewAppInstance {
pub pid: u32,
pub host: IpAddr,
pub port: u16,
pub waterui_core_fingerprint: String,
pub registered_at_unix_ms: u64,
}
impl PreviewAppInstance {
#[must_use]
pub fn new(
pid: u32,
host: IpAddr,
port: u16,
waterui_core_fingerprint: impl Into<String>,
) -> Self {
Self {
pid,
host,
port,
waterui_core_fingerprint: waterui_core_fingerprint.into(),
registered_at_unix_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock must not be earlier than the Unix epoch")
.as_millis()
.try_into()
.expect("preview registration timestamp must fit into u64"),
}
}
}
fn water_cache_dir() -> PathBuf {
if let Some(cache_dir) = std::env::var_os("WATER_CACHE_DIR") {
return PathBuf::from(cache_dir);
}
if let Some(cache_dir) = dirs::cache_dir() {
return cache_dir.join("waterui");
}
std::env::temp_dir().join("waterui-cache")
}
#[must_use]
pub fn preview_cache_root_dir() -> PathBuf {
water_cache_dir().join("preview")
}
#[must_use]
pub fn preview_instance_registry_dir() -> PathBuf {
preview_cache_root_dir().join("instances")
}
#[must_use]
pub fn preview_instance_registry_path(instance: &PreviewAppInstance) -> PathBuf {
preview_instance_registry_dir().join(format!("{}-{}.json", instance.pid, instance.port))
}
}
pub mod transport {
use std::io;
use futures_lite::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use serde::Serialize;
use serde::de::DeserializeOwned;
pub const LEN_PREFIX_BYTES: usize = 4;
#[must_use]
pub fn max_frame_bytes() -> usize {
const DEFAULT: usize = 128 * 1024 * 1024;
match std::env::var("WATERUI_PREVIEW_MAX_FRAME_BYTES") {
Ok(value) => value.parse::<usize>().unwrap_or_else(|error| {
panic!("invalid WATERUI_PREVIEW_MAX_FRAME_BYTES value `{value}`: {error}")
}),
Err(std::env::VarError::NotPresent) => DEFAULT,
Err(std::env::VarError::NotUnicode(_)) => {
panic!("WATERUI_PREVIEW_MAX_FRAME_BYTES must be valid UTF-8")
}
}
}
pub async fn read_frame<R, T>(reader: &mut R) -> io::Result<T>
where
R: AsyncRead + Unpin + Send,
T: DeserializeOwned,
{
let mut len_buf = [0u8; LEN_PREFIX_BYTES];
reader.read_exact(&mut len_buf).await?;
let len = u32::from_be_bytes(len_buf) as usize;
let max = max_frame_bytes();
if len > max {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("preview frame too large: {len} bytes (max {max})"),
));
}
let mut buf = vec![0u8; len];
reader.read_exact(&mut buf).await?;
let config = bincode::config::standard();
let (value, bytes_read): (T, usize) = bincode::serde::decode_from_slice(&buf, config)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
if bytes_read != buf.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"trailing bytes after preview frame payload",
));
}
Ok(value)
}
pub async fn write_frame<W, T>(writer: &mut W, value: &T) -> io::Result<()>
where
W: AsyncWrite + Unpin + Send,
T: Serialize + Sync,
{
let config = bincode::config::standard();
let data = bincode::serde::encode_to_vec(value, config)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let len: u32 = data.len().try_into().map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"preview frame too large for u32 length",
)
})?;
writer.write_all(&len.to_be_bytes()).await?;
writer.write_all(&data).await?;
writer.flush().await?;
Ok(())
}
}
pub mod tcp {
use std::net::{IpAddr, Ipv4Addr};
use std::ops::RangeInclusive;
use thiserror::Error;
pub const DEFAULT_HOST: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
pub const DEFAULT_PORT_START: u16 = 2106;
pub const DEFAULT_PORT_RANGE: u16 = 50;
#[derive(Debug, Clone, Copy)]
pub struct PreviewTcpConfig {
pub host: IpAddr,
pub port_start: u16,
pub port_range: u16,
}
impl PreviewTcpConfig {
#[must_use]
pub const fn default_localhost() -> Self {
Self {
host: DEFAULT_HOST,
port_start: DEFAULT_PORT_START,
port_range: DEFAULT_PORT_RANGE,
}
}
pub fn from_env() -> Result<Self, ConfigError> {
let mut cfg = Self::default_localhost();
if let Ok(host) = std::env::var("WATERUI_PREVIEW_HOST") {
cfg.host = host.parse().map_err(|_| ConfigError::InvalidHost)?;
}
if let Ok(port_start) = std::env::var("WATERUI_PREVIEW_PORT_START") {
cfg.port_start = port_start
.parse()
.map_err(|_| ConfigError::InvalidPortStart)?;
}
if let Ok(port_range) = std::env::var("WATERUI_PREVIEW_PORT_RANGE") {
cfg.port_range = port_range
.parse()
.map_err(|_| ConfigError::InvalidPortRange)?;
}
Ok(cfg)
}
#[must_use]
pub const fn ports(&self) -> RangeInclusive<u16> {
let end = self
.port_start
.saturating_add(self.port_range.saturating_sub(1));
self.port_start..=end
}
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("invalid WATERUI_PREVIEW_HOST")]
InvalidHost,
#[error("invalid WATERUI_PREVIEW_PORT_START")]
InvalidPortStart,
#[error("invalid WATERUI_PREVIEW_PORT_RANGE")]
InvalidPortRange,
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Size {
pub width: f32,
pub height: f32,
}
impl Size {
#[must_use]
pub const fn new(width: f32, height: f32) -> Self {
Self { width, height }
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct DylibId([u8; 32]);
impl DylibId {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl fmt::Debug for DylibId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DylibId({self})")
}
}
impl fmt::Display for DylibId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", hex::encode(self.0))
}
}
impl FromStr for DylibId {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = hex::decode(s).map_err(|_| "invalid hex")?;
let bytes: [u8; 32] = bytes.try_into().map_err(|_| "expected 32 bytes")?;
Ok(Self(bytes))
}
}
impl Serialize for DylibId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&hex::encode(self.0))
}
}
impl<'de> Deserialize<'de> for DylibId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct Visitor;
impl DeVisitor<'_> for Visitor {
type Value = DylibId;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "a 64-char hex string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: DeError,
{
let bytes = hex::decode(v).map_err(|_| E::custom("invalid hex"))?;
let bytes: [u8; 32] = bytes
.try_into()
.map_err(|_| E::custom("expected 32 bytes"))?;
Ok(DylibId(bytes))
}
}
deserializer.deserialize_str(Visitor)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DylibSource {
Bytes {
id: DylibId,
bytes: Vec<u8>,
},
Cached {
id: DylibId,
},
LocalPath {
id: DylibId,
path: std::path::PathBuf,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PreviewRequest {
Ping,
HasDylib {
id: DylibId,
},
Render {
dylib: DylibSource,
symbol: String,
frame: Size,
},
Shutdown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreviewOutput {
pub png_data: Vec<u8>,
pub timings: PreviewRenderTimings,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct PreviewDylibLoadTimings {
pub cache_file_ms: u64,
pub load_library_ms: u64,
pub initial_dlopen_ms: u64,
pub codesign_verify_ms: Option<u64>,
pub codesign_ms: Option<u64>,
pub reload_after_codesign_ms: Option<u64>,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct PreviewRenderTimings {
pub ensure_dylib_cached_ms: u64,
pub dylib_load: Option<PreviewDylibLoadTimings>,
pub load_view_ms: u64,
pub render_ms: u64,
pub png_encode_ms: u64,
pub total_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
pub enum PreviewError {
#[error("Unknown dylib id: {0}")]
UnknownDylibId(DylibId),
#[error("Failed to load dylib: {0}")]
DylibLoad(String),
#[error("Symbol not found: {0}")]
SymbolNotFound(String),
#[error("Render failed: {0}")]
RenderFailed(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PreviewResponse {
Pong {
protocol: PreviewProtocolInfo,
},
HasDylib {
present: bool,
},
Render {
result: Result<PreviewOutput, PreviewError>,
},
Shutdown,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dylib_id_roundtrip_hex() {
let id = DylibId::from_bytes([0xAB; 32]);
let json = serde_json::to_string(&id).unwrap();
let de: DylibId = serde_json::from_str(&json).unwrap();
assert_eq!(id, de);
}
}