a bunch of quality of life + MST viz #8

merged
opened by zzstoatzz.io targeting main from feat/url-validation

bunch of things

+164 -11
Cargo.lock
···
"flate2",
"foldhash",
"futures-core",
-
"h2",
+
"h2 0.3.27",
"http 0.2.12",
"httparse",
"httpdate",
···
"env_logger",
"hickory-resolver",
"log",
+
"reqwest",
"serde",
"serde_json",
"tokio",
···
"miniz_oxide",
"object",
"rustc-demangle",
-
"windows-link",
+
"windows-link 0.2.0",
]
[[package]]
···
"num-traits",
"serde",
"wasm-bindgen",
-
"windows-link",
+
"windows-link 0.2.0",
]
[[package]]
···
"tracing",
+
[[package]]
+
name = "h2"
+
version = "0.4.12"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
+
dependencies = [
+
"atomic-waker",
+
"bytes",
+
"fnv",
+
"futures-core",
+
"futures-sink",
+
"http 1.3.1",
+
"indexmap",
+
"slab",
+
"tokio",
+
"tokio-util",
+
"tracing",
+
]
+
[[package]]
name = "hashbrown"
version = "0.14.5"
···
"bytes",
"futures-channel",
"futures-core",
+
"h2 0.4.12",
"http 1.3.1",
"http-body",
"httparse",
···
"want",
+
[[package]]
+
name = "hyper-rustls"
+
version = "0.27.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+
dependencies = [
+
"http 1.3.1",
+
"hyper",
+
"hyper-util",
+
"rustls",
+
"rustls-pki-types",
+
"tokio",
+
"tokio-rustls",
+
"tower-service",
+
]
+
[[package]]
name = "hyper-tls"
version = "0.6.0"
···
"percent-encoding",
"pin-project-lite",
"socket2 0.6.0",
+
"system-configuration",
"tokio",
"tower-service",
"tracing",
+
"windows-registry",
[[package]]
···
"libc",
"redox_syscall",
"smallvec",
-
"windows-link",
+
"windows-link 0.2.0",
[[package]]
···
"async-compression",
"base64 0.22.1",
"bytes",
+
"encoding_rs",
"futures-core",
"futures-util",
+
"h2 0.4.12",
"http 1.3.1",
"http-body",
"http-body-util",
"hyper",
+
"hyper-rustls",
"hyper-tls",
"hyper-util",
"js-sys",
"log",
+
"mime",
"native-tls",
"percent-encoding",
"pin-project-lite",
···
"subtle",
+
[[package]]
+
name = "ring"
+
version = "0.17.14"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+
dependencies = [
+
"cc",
+
"cfg-if",
+
"getrandom 0.2.16",
+
"libc",
+
"untrusted",
+
"windows-sys 0.52.0",
+
]
+
[[package]]
name = "rustc-demangle"
version = "0.1.26"
···
"windows-sys 0.61.1",
+
[[package]]
+
name = "rustls"
+
version = "0.23.31"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
+
dependencies = [
+
"once_cell",
+
"rustls-pki-types",
+
"rustls-webpki",
+
"subtle",
+
"zeroize",
+
]
+
[[package]]
name = "rustls-pki-types"
version = "1.12.0"
···
"zeroize",
+
[[package]]
+
name = "rustls-webpki"
+
version = "0.103.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
+
dependencies = [
+
"ring",
+
"rustls-pki-types",
+
"untrusted",
+
]
+
[[package]]
name = "rustversion"
version = "1.0.22"
···
"syn 2.0.106",
+
[[package]]
+
name = "system-configuration"
+
version = "0.6.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+
dependencies = [
+
"bitflags",
+
"core-foundation",
+
"system-configuration-sys",
+
]
+
+
[[package]]
+
name = "system-configuration-sys"
+
version = "0.6.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+
dependencies = [
+
"core-foundation-sys",
+
"libc",
+
]
+
[[package]]
name = "tagptr"
version = "0.2.0"
···
"tokio",
+
[[package]]
+
name = "tokio-rustls"
+
version = "0.26.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
+
dependencies = [
+
"rustls",
+
"tokio",
+
]
+
[[package]]
name = "tokio-util"
version = "0.7.16"
···
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06"
+
[[package]]
+
name = "untrusted"
+
version = "0.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
[[package]]
name = "url"
version = "2.5.7"
···
dependencies = [
"windows-implement",
"windows-interface",
-
"windows-link",
-
"windows-result",
-
"windows-strings",
+
"windows-link 0.2.0",
+
"windows-result 0.4.0",
+
"windows-strings 0.5.0",
[[package]]
···
"syn 2.0.106",
+
[[package]]
+
name = "windows-link"
+
version = "0.1.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
[[package]]
name = "windows-link"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
+
[[package]]
+
name = "windows-registry"
+
version = "0.5.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
+
dependencies = [
+
"windows-link 0.1.3",
+
"windows-result 0.3.4",
+
"windows-strings 0.4.2",
+
]
+
+
[[package]]
+
name = "windows-result"
+
version = "0.3.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+
dependencies = [
+
"windows-link 0.1.3",
+
]
+
[[package]]
name = "windows-result"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
dependencies = [
-
"windows-link",
+
"windows-link 0.2.0",
+
]
+
+
[[package]]
+
name = "windows-strings"
+
version = "0.4.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+
dependencies = [
+
"windows-link 0.1.3",
[[package]]
···
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
dependencies = [
-
"windows-link",
+
"windows-link 0.2.0",
[[package]]
···
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f"
dependencies = [
-
"windows-link",
+
"windows-link 0.2.0",
[[package]]
···
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b"
dependencies = [
-
"windows-link",
+
"windows-link 0.2.0",
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
+1
Cargo.toml
···
hickory-resolver = "0.24"
env_logger = "0.11"
log = "0.4"
+
reqwest = { version = "0.12", features = ["json"] }
+164
src/mst.rs
···
+
use serde::{Deserialize, Serialize};
+
use std::collections::HashMap;
+
+
#[derive(Debug, Serialize, Deserialize, Clone)]
+
pub struct Record {
+
pub uri: String,
+
pub cid: String,
+
pub value: serde_json::Value,
+
}
+
+
#[derive(Debug, Serialize, Clone)]
+
#[serde(rename_all = "camelCase")]
+
pub struct MSTNode {
+
pub key: String,
+
pub cid: Option<String>,
+
pub uri: Option<String>,
+
pub value: Option<serde_json::Value>,
+
pub depth: i32,
+
pub children: Vec<MSTNode>,
+
}
+
+
#[derive(Debug, Serialize)]
+
#[serde(rename_all = "camelCase")]
+
pub struct MSTResponse {
+
pub root: MSTNode,
+
pub record_count: usize,
+
}
+
+
pub fn build_mst(records: Vec<Record>) -> MSTResponse {
+
let record_count = records.len();
+
+
// Extract and sort by key
+
let mut nodes: Vec<MSTNode> = records
+
.into_iter()
+
.map(|r| {
+
let key = r.uri.split('/').last().unwrap_or("").to_string();
+
MSTNode {
+
key: key.clone(),
+
cid: Some(r.cid),
+
uri: Some(r.uri),
+
value: Some(r.value),
+
depth: calculate_key_depth(&key),
+
children: vec![],
+
}
+
})
+
.collect();
+
+
nodes.sort_by(|a, b| a.key.cmp(&b.key));
+
+
// Build tree structure
+
let root = build_tree(nodes);
+
+
MSTResponse {
+
root,
+
record_count,
+
}
+
}
+
+
fn calculate_key_depth(key: &str) -> i32 {
+
// Simplified depth calculation based on key hash
+
let mut hash: i32 = 0;
+
for ch in key.chars() {
+
hash = hash.wrapping_shl(5).wrapping_sub(hash).wrapping_add(ch as i32);
+
}
+
+
// Count leading zero bits (approximation)
+
let abs_hash = hash.abs() as u32;
+
let binary = format!("{:032b}", abs_hash);
+
+
let mut depth = 0;
+
let chars: Vec<char> = binary.chars().collect();
+
let mut i = 0;
+
while i < chars.len() - 1 {
+
if chars[i] == '0' && chars[i + 1] == '0' {
+
depth += 1;
+
i += 2;
+
} else {
+
break;
+
}
+
}
+
+
depth.min(5)
+
}
+
+
fn build_tree(nodes: Vec<MSTNode>) -> MSTNode {
+
if nodes.is_empty() {
+
return MSTNode {
+
key: "root".to_string(),
+
cid: None,
+
uri: None,
+
value: None,
+
depth: -1,
+
children: vec![],
+
};
+
}
+
+
// Group by depth
+
let mut by_depth: HashMap<i32, Vec<MSTNode>> = HashMap::new();
+
for node in nodes {
+
by_depth.entry(node.depth).or_insert_with(Vec::new).push(node);
+
}
+
+
let mut depths: Vec<i32> = by_depth.keys().copied().collect();
+
depths.sort();
+
+
// Build tree bottom-up
+
let mut current_level: Vec<MSTNode> = by_depth.remove(&depths[depths.len() - 1]).unwrap_or_default();
+
+
// Work backwards through depths
+
for i in (0..depths.len() - 1).rev() {
+
let depth = depths[i];
+
let mut parent_nodes = by_depth.remove(&depth).unwrap_or_default();
+
+
// Distribute children to parents
+
let children_per_parent = if parent_nodes.is_empty() {
+
0
+
} else {
+
(current_level.len() + parent_nodes.len() - 1) / parent_nodes.len()
+
};
+
+
for (i, parent) in parent_nodes.iter_mut().enumerate() {
+
let start = i * children_per_parent;
+
let end = ((i + 1) * children_per_parent).min(current_level.len());
+
if start < current_level.len() {
+
parent.children = current_level.drain(start..end).collect();
+
}
+
}
+
+
current_level = parent_nodes;
+
}
+
+
// Create root and attach top-level nodes
+
MSTNode {
+
key: "root".to_string(),
+
cid: None,
+
uri: None,
+
value: None,
+
depth: -1,
+
children: current_level,
+
}
+
}
+
+
pub async fn fetch_records(pds: &str, did: &str, collection: &str) -> Result<Vec<Record>, String> {
+
let url = format!(
+
"{}/xrpc/com.atproto.repo.listRecords?repo={}&collection={}&limit=100",
+
pds, did, collection
+
);
+
+
let response = reqwest::get(&url)
+
.await
+
.map_err(|e| format!("Failed to fetch records: {}", e))?;
+
+
#[derive(Deserialize)]
+
struct ListRecordsResponse {
+
records: Vec<Record>,
+
}
+
+
let data: ListRecordsResponse = response
+
.json()
+
.await
+
.map_err(|e| format!("Failed to parse response: {}", e))?;
+
+
Ok(data.records)
+
}
+11
src/templates.rs
···
color: var(--text);
}}
+
.app-name.invalid-link {{
+
color: var(--text-light);
+
opacity: 0.5;
+
cursor: not-allowed;
+
}}
+
+
.app-name.invalid-link:hover {{
+
text-decoration: none;
+
color: var(--text-light);
+
}}
+
.detail-panel {{
position: fixed;
top: 0;
+22 -3
src/routes.rs
···
pub async fn validate_url(query: web::Query<ValidateUrlQuery>) -> HttpResponse {
let url = &query.url;
-
// Try to make a HEAD request with a short timeout
+
// Build client with redirect following and timeout
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(3))
+
.redirect(reqwest::redirect::Policy::limited(5))
.build()
.unwrap();
+
// Try HEAD first, fall back to GET if HEAD doesn't succeed
let is_valid = match client.head(url).send().await {
-
Ok(response) => response.status().is_success() || response.status().is_redirection(),
-
Err(_) => false,
+
Ok(response) => {
+
let status = response.status();
+
if status.is_success() || status.is_redirection() {
+
true
+
} else {
+
// HEAD returned error status (like 405), try GET
+
match client.get(url).send().await {
+
Ok(get_response) => get_response.status().is_success(),
+
Err(_) => false,
+
}
+
}
+
}
+
Err(_) => {
+
// HEAD request failed completely, try GET as fallback
+
match client.get(url).send().await {
+
Ok(response) => response.status().is_success(),
+
Err(_) => false,
+
}
+
}
};
HttpResponse::Ok().json(serde_json::json!({