|
@@ -1,8 +1,11 @@
|
|
|
//! Repo file browser behind `/repo/...` — the target of the viewer's toml
|
|
//! Repo file browser behind `/repo/...` — the target of the viewer's toml
|
|
|
//! links: build dirs under `docker-images/` (Dockerfile, versions.env,
|
|
//! links: build dirs under `docker-images/` (Dockerfile, versions.env,
|
|
|
//! profiles, patches), built-in `host-scripts/`, and recipe-local files
|
|
//! profiles, patches), built-in `host-scripts/`, and recipe-local files
|
|
|
-//! under `recipes/`. The root is the process cwd (the launcher runs from the
|
|
|
|
|
-//! repo root); paths are plain names, no traversal, top level restricted.
|
|
|
|
|
|
|
+//! under `recipes/`. Roots: the process cwd (the launcher running from a
|
|
|
|
|
+//! repo root) plus every `docker_image_sources` entry — the synced recipe
|
|
|
|
|
+//! repos (their checkouts carry `docker-images/` + `recipes/`, which is
|
|
|
|
|
+//! how the HA add-on serves them). Paths are plain names, no traversal,
|
|
|
|
|
+//! top level restricted.
|
|
|
//!
|
|
//!
|
|
|
//! `toml_links` scans a recipe.toml for the repo-referencing values and
|
|
//! `toml_links` scans a recipe.toml for the repo-referencing values and
|
|
|
//! builds the `{value, href, title}` list the viewer wraps into anchors.
|
|
//! builds the `{value, href, title}` list the viewer wraps into anchors.
|
|
@@ -15,23 +18,51 @@ fn repo_root_ok() -> Option<std::path::PathBuf> {
|
|
|
(cwd.join("docker-images").is_dir() || cwd.join("host-scripts").is_dir()).then_some(cwd)
|
|
(cwd.join("docker-images").is_dir() || cwd.join("host-scripts").is_dir()).then_some(cwd)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/// Resolution roots: the cwd (dev — the launcher repo itself) plus every
|
|
|
|
|
+/// configured `docker_image_sources` entry (the synced recipe repos).
|
|
|
|
|
+fn repo_roots(config: &crate::config::Config) -> Vec<std::path::PathBuf> {
|
|
|
|
|
+ let mut roots = Vec::new();
|
|
|
|
|
+ if let Some(cwd) = repo_root_ok() {
|
|
|
|
|
+ roots.push(cwd);
|
|
|
|
|
+ }
|
|
|
|
|
+ for s in &config.docker_image_sources {
|
|
|
|
|
+ let p = std::path::PathBuf::from(s);
|
|
|
|
|
+ if !roots.contains(&p) {
|
|
|
|
|
+ roots.push(p);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ roots
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
fn plain_seg(s: &str) -> bool {
|
|
fn plain_seg(s: &str) -> bool {
|
|
|
!s.contains("..")
|
|
!s.contains("..")
|
|
|
&& s.chars()
|
|
&& s.chars()
|
|
|
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
|
|
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-fn repo_resolve(path: &str) -> Option<std::path::PathBuf> {
|
|
|
|
|
- let root = repo_root_ok()?;
|
|
|
|
|
|
|
+/// Resolve a repo path against every root; the first hit wins. `Ok(None)`
|
|
|
|
|
+/// = the path shape is valid but no root has it (→ "file not found");
|
|
|
|
|
+/// `Err(())` = the path shape itself is not allowed (bad top level,
|
|
|
|
|
+/// traversal, exotic characters).
|
|
|
|
|
+fn repo_resolve(
|
|
|
|
|
+ config: &crate::config::Config,
|
|
|
|
|
+ path: &str,
|
|
|
|
|
+) -> Result<Option<std::path::PathBuf>, ()> {
|
|
|
let segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
|
|
let segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
|
|
|
- let first = segs.first().copied()?;
|
|
|
|
|
|
|
+ let first = segs.first().copied().ok_or(())?;
|
|
|
if !matches!(first, "docker-images" | "host-scripts" | "recipes") {
|
|
if !matches!(first, "docker-images" | "host-scripts" | "recipes") {
|
|
|
- return None;
|
|
|
|
|
|
|
+ return Err(());
|
|
|
}
|
|
}
|
|
|
if !segs.iter().all(|s| plain_seg(s)) {
|
|
if !segs.iter().all(|s| plain_seg(s)) {
|
|
|
- return None;
|
|
|
|
|
|
|
+ return Err(());
|
|
|
|
|
+ }
|
|
|
|
|
+ for root in repo_roots(config) {
|
|
|
|
|
+ let p = segs.iter().fold(root, |p, s| p.join(s));
|
|
|
|
|
+ if p.exists() {
|
|
|
|
|
+ return Ok(Some(p));
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- Some(segs.iter().fold(root, |p, s| p.join(s)))
|
|
|
|
|
|
|
+ Ok(None)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
fn dir_listing(base: &str, rel: &str, p: &std::path::Path) -> String {
|
|
fn dir_listing(base: &str, rel: &str, p: &std::path::Path) -> String {
|
|
@@ -92,9 +123,19 @@ pub(crate) async fn repo_route(
|
|
|
) -> Response {
|
|
) -> Response {
|
|
|
use axum::http::StatusCode;
|
|
use axum::http::StatusCode;
|
|
|
use axum::response::IntoResponse;
|
|
use axum::response::IntoResponse;
|
|
|
- let p = match repo_resolve(&path) {
|
|
|
|
|
- Some(p) => p,
|
|
|
|
|
- None => {
|
|
|
|
|
|
|
+ let p = match repo_resolve(&state.config, &path) {
|
|
|
|
|
+ Ok(Some(p)) => p,
|
|
|
|
|
+ Ok(None) => {
|
|
|
|
|
+ // valid shape, but no root has it
|
|
|
|
|
+ return (
|
|
|
|
|
+ StatusCode::NOT_FOUND,
|
|
|
|
|
+ axum::response::Html(
|
|
|
|
|
+ "<!doctype html><meta charset=\"utf-8\">file not found".to_string(),
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+ .into_response();
|
|
|
|
|
+ }
|
|
|
|
|
+ Err(()) => {
|
|
|
return (
|
|
return (
|
|
|
StatusCode::NOT_FOUND,
|
|
StatusCode::NOT_FOUND,
|
|
|
axum::response::Html(
|
|
axum::response::Html(
|
|
@@ -105,12 +146,11 @@ pub(crate) async fn repo_route(
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
if p.is_dir() {
|
|
if p.is_dir() {
|
|
|
- let root = repo_root_ok().unwrap_or_default();
|
|
|
|
|
- let rel = p
|
|
|
|
|
- .strip_prefix(&root)
|
|
|
|
|
- .unwrap_or(&p)
|
|
|
|
|
- .to_string_lossy()
|
|
|
|
|
- .replace(std::path::MAIN_SEPARATOR_STR, "/");
|
|
|
|
|
|
|
+ // list under the root that actually served this path (the first
|
|
|
|
|
+ // existing one) — the rel path for the breadcrumb is the same
|
|
|
|
|
+ // cleaned `path` the client asked for
|
|
|
|
|
+ let rel = path.trim_end_matches('/').to_string();
|
|
|
|
|
+ let rel = rel.strip_prefix('/').unwrap_or(&rel).to_string();
|
|
|
return axum::response::Html(dir_listing(&state.base, &rel, &p)).into_response();
|
|
return axum::response::Html(dir_listing(&state.base, &rel, &p)).into_response();
|
|
|
}
|
|
}
|
|
|
let text = match std::fs::read_to_string(&p) {
|
|
let text = match std::fs::read_to_string(&p) {
|