use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum RegistryError {
#[error("cannot read sim registry {path}: {source}")]
Io {
path: String,
source: std::io::Error,
},
#[error("malformed sim registry {path}: {detail}")]
Malformed {
path: String,
detail: String,
},
#[error(
"unknown device ref {device_ref:?} — pass an explicit UDID or one of \
the recorded aliases: {}",
known.join(", ")
)]
UnknownDevice {
device_ref: String,
known: Vec<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisteredSim {
#[serde(rename = "deviceName")]
pub device_name: String,
pub udid: String,
pub runtime: String,
#[serde(rename = "deviceType")]
pub device_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub locale: Option<String>,
#[serde(
default,
rename = "runnerPort",
skip_serializing_if = "Option::is_none"
)]
pub runner_port: Option<u16>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RegisterOutcome {
Added,
Updated,
}
#[derive(Debug)]
pub struct SimRegistry {
sims: BTreeMap<String, RegisteredSim>,
}
pub fn is_udid(s: &str) -> bool {
let bytes = s.as_bytes();
if bytes.len() != 36 {
return false;
}
for (i, b) in bytes.iter().enumerate() {
match i {
8 | 13 | 18 | 23 => {
if *b != b'-' {
return false;
}
}
_ => {
if !b.is_ascii_hexdigit() {
return false;
}
}
}
}
true
}
pub fn store_dir(path: &Path) -> PathBuf {
smix_dir(path)
}
fn smix_dir(path: &Path) -> PathBuf {
if path.extension().is_some_and(|e| e == "json") {
path.parent().unwrap_or(path).to_path_buf()
} else {
path.to_path_buf()
}
}
fn open_store(path: &Path) -> Result<smix_store::Store, RegistryError> {
let dir = smix_dir(path);
std::fs::create_dir_all(&dir).map_err(|source| RegistryError::Io {
path: dir.display().to_string(),
source,
})?;
let store = smix_store::Store::open(&dir).map_err(|e| RegistryError::Io {
path: dir.display().to_string(),
source: std::io::Error::other(e.to_string()),
})?;
let legacy = dir.join("sims.json");
smix_store::import_legacy_records(&store.sims(), &legacy, "sims").map_err(|e| {
RegistryError::Malformed {
path: legacy.display().to_string(),
detail: e.to_string(),
}
})?;
Ok(store)
}
impl SimRegistry {
pub fn register(
path: &Path,
alias: &str,
sim: RegisteredSim,
) -> Result<RegisterOutcome, RegistryError> {
let store = open_store(path)?;
let existed = store
.sims()
.get(alias)
.map_err(|e| RegistryError::Malformed {
path: path.display().to_string(),
detail: e.to_string(),
})?
.is_some();
store
.sims()
.put_json(alias, &sim)
.map_err(|e| RegistryError::Io {
path: path.display().to_string(),
source: std::io::Error::other(e.to_string()),
})?;
store.sync().map_err(|e| RegistryError::Io {
path: path.display().to_string(),
source: std::io::Error::other(e.to_string()),
})?;
Ok(if existed {
RegisterOutcome::Updated
} else {
RegisterOutcome::Added
})
}
pub fn load(path: &Path) -> Result<Self, RegistryError> {
let store = open_store(path)?;
let mut sims = BTreeMap::new();
for alias in store.sims().list() {
let sim: RegisteredSim = store
.sims()
.get_json(&alias)
.map_err(|e| RegistryError::Malformed {
path: path.display().to_string(),
detail: e.to_string(),
})?
.ok_or_else(|| RegistryError::Malformed {
path: path.display().to_string(),
detail: format!("`{alias}` vanished between listing and reading"),
})?;
sims.insert(alias, sim);
}
Ok(Self { sims })
}
pub fn discover(start: &Path) -> Option<PathBuf> {
let mut dir = Some(start);
while let Some(d) = dir {
let smix = d.join(".smix");
if smix.join("sims.json").is_file() || smix.join("kv").is_dir() {
return Some(smix);
}
dir = d.parent();
}
None
}
pub fn resolve(&self, device_ref: &str) -> Result<String, RegistryError> {
if is_udid(device_ref) {
return Ok(device_ref.to_ascii_uppercase());
}
if let Some(sim) = self.sims.get(device_ref) {
return Ok(sim.udid.to_ascii_uppercase());
}
if let Some(sim) = self.sims.values().find(|s| s.device_name == device_ref) {
return Ok(sim.udid.to_ascii_uppercase());
}
let mut known: Vec<String> = Vec::with_capacity(self.sims.len() * 2);
for (alias, sim) in &self.sims {
known.push(alias.clone());
known.push(sim.device_name.clone());
}
Err(RegistryError::UnknownDevice {
device_ref: device_ref.to_string(),
known,
})
}
pub fn sims(&self) -> &BTreeMap<String, RegisteredSim> {
&self.sims
}
pub fn lookup(&self, device_ref: &str) -> Option<&RegisteredSim> {
if let Some(sim) = self.sims.get(device_ref) {
return Some(sim);
}
self.sims
.values()
.find(|sim| sim.device_name == device_ref || sim.udid.eq_ignore_ascii_case(device_ref))
}
}