pax-compiler 0.39.0

Compiler APIs for parsing and building Pax projects into application executables
Documentation
use crate::helpers::PUBLIC_DIR_NAME;
use color_eyre::eyre::{eyre, Report, WrapErr};
use std::fs;
use std::path::{Component, Path, PathBuf};

const RESERVED_TOP_LEVEL_PATHS: &[&str] = &["assets", "__reloads__", "snippets"];

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PublicEntryKind {
    Directory,
    File,
}

#[derive(Debug)]
struct PublicEntry {
    source: PathBuf,
    relative: PathBuf,
    kind: PublicEntryKind,
}

pub(crate) fn project_public_dir(project_root: &Path) -> PathBuf {
    project_root.join(PUBLIC_DIR_NAME)
}

pub(crate) fn existing_project_public_dir(project_root: &Path) -> Option<PathBuf> {
    let public_dir = project_public_dir(project_root);
    public_dir.is_dir().then_some(public_dir)
}

pub(crate) fn validate_public_dir(public_dir: &Path, output_root: &Path) -> Result<(), Report> {
    build_copy_plan(public_dir, output_root).map(|_| ())
}

pub(crate) fn materialize_public_dir(public_dir: &Path, output_root: &Path) -> Result<(), Report> {
    let entries = build_copy_plan(public_dir, output_root)?;
    for entry in entries {
        let destination = output_root.join(&entry.relative);
        match entry.kind {
            PublicEntryKind::Directory => fs::create_dir_all(&destination).wrap_err_with(|| {
                format!(
                    "Failed to create public output directory `{}`",
                    destination.display()
                )
            })?,
            PublicEntryKind::File => {
                if let Some(parent) = destination.parent() {
                    fs::create_dir_all(parent).wrap_err_with(|| {
                        format!(
                            "Failed to create public output directory `{}`",
                            parent.display()
                        )
                    })?;
                }
                fs::copy(&entry.source, &destination).wrap_err_with(|| {
                    format!(
                        "Failed to copy public file `{}` to `{}`",
                        entry.source.display(),
                        destination.display()
                    )
                })?;
            }
        }
    }
    Ok(())
}

pub(crate) fn is_servable_public_path(public_dir: &Path, relative: &Path) -> bool {
    let Ok(public_metadata) = fs::symlink_metadata(public_dir) else {
        return false;
    };
    if !public_metadata.is_dir()
        || public_metadata.file_type().is_symlink()
        || relative.as_os_str().is_empty()
        || relative.is_absolute()
        || relative
            .components()
            .any(|component| !matches!(component, Component::Normal(_)))
        || is_reserved_public_path(relative)
    {
        return false;
    }

    let mut candidate = public_dir.to_path_buf();
    for component in relative.components() {
        let Component::Normal(component) = component else {
            return false;
        };
        candidate.push(component);
        let Ok(metadata) = fs::symlink_metadata(&candidate) else {
            return false;
        };
        if metadata.file_type().is_symlink() {
            return false;
        }
    }

    let Ok(metadata) = fs::metadata(&candidate) else {
        return false;
    };
    if metadata.is_file() {
        return true;
    }
    if !metadata.is_dir() {
        return false;
    }

    let index_path = candidate.join("index.html");
    match fs::symlink_metadata(index_path) {
        Ok(index_metadata) => index_metadata.is_file() && !index_metadata.file_type().is_symlink(),
        Err(_) => false,
    }
}

fn build_copy_plan(public_dir: &Path, output_root: &Path) -> Result<Vec<PublicEntry>, Report> {
    let public_metadata = match fs::symlink_metadata(public_dir) {
        Ok(metadata) => metadata,
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
        Err(err) => {
            return Err(err).wrap_err_with(|| {
                format!(
                    "Failed to inspect public directory `{}`",
                    public_dir.display()
                )
            })
        }
    };
    if public_metadata.file_type().is_symlink() {
        return Err(eyre!(
            "Pax web public directory `{}` must not be a symbolic link",
            public_dir.display()
        ));
    }
    if !public_metadata.is_dir() {
        return Err(eyre!(
            "Pax web public path `{}` must be a directory",
            public_dir.display()
        ));
    }

    let mut entries = Vec::new();
    collect_entries(public_dir, public_dir, &mut entries)?;
    entries.sort_by(|left, right| left.relative.cmp(&right.relative));

    for entry in &entries {
        if is_reserved_public_path(&entry.relative) {
            return Err(eyre!(
                "Public path `{}` is reserved by the Pax web runtime",
                entry.relative.display()
            ));
        }

        let destination = output_root.join(&entry.relative);
        let destination_metadata = match fs::symlink_metadata(&destination) {
            Ok(metadata) => Some(metadata),
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
            Err(err) => {
                return Err(err).wrap_err_with(|| {
                    format!(
                        "Failed to inspect generated web output `{}`",
                        destination.display()
                    )
                })
            }
        };

        match (entry.kind, destination_metadata) {
            (PublicEntryKind::Directory, Some(metadata)) if metadata.is_dir() => {}
            (_, Some(_)) => {
                return Err(eyre!(
                    "Public path `{}` conflicts with generated or custom web output at `{}`",
                    entry.relative.display(),
                    destination.display()
                ));
            }
            (_, None) => {}
        }
    }

    Ok(entries)
}

fn collect_entries(
    public_root: &Path,
    directory: &Path,
    entries: &mut Vec<PublicEntry>,
) -> Result<(), Report> {
    let read_dir = fs::read_dir(directory).wrap_err_with(|| {
        format!(
            "Failed to read Pax web public directory `{}`",
            directory.display()
        )
    })?;

    for child in read_dir {
        let child = child.wrap_err_with(|| {
            format!(
                "Failed to read an entry in Pax web public directory `{}`",
                directory.display()
            )
        })?;
        let source = child.path();
        let relative = source
            .strip_prefix(public_root)
            .map_err(|_| {
                eyre!(
                    "Public path escaped its source root: `{}`",
                    source.display()
                )
            })?
            .to_path_buf();
        let metadata = fs::symlink_metadata(&source)
            .wrap_err_with(|| format!("Failed to inspect public path `{}`", source.display()))?;

        if metadata.file_type().is_symlink() {
            return Err(eyre!(
                "Public path `{}` must not be a symbolic link",
                relative.display()
            ));
        }
        if metadata.is_dir() {
            entries.push(PublicEntry {
                source: source.clone(),
                relative,
                kind: PublicEntryKind::Directory,
            });
            collect_entries(public_root, &source, entries)?;
        } else if metadata.is_file() {
            entries.push(PublicEntry {
                source,
                relative,
                kind: PublicEntryKind::File,
            });
        } else {
            return Err(eyre!(
                "Public path `{}` is not a regular file or directory",
                relative.display()
            ));
        }
    }

    Ok(())
}

fn is_reserved_public_path(relative: &Path) -> bool {
    let Some(first) = relative.components().next() else {
        return false;
    };
    let Component::Normal(first) = first else {
        return true;
    };
    let Some(first) = first.to_str() else {
        return true;
    };
    RESERVED_TOP_LEVEL_PATHS.contains(&first)
}

#[cfg(test)]
mod tests {
    use super::{is_servable_public_path, materialize_public_dir, validate_public_dir};
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn materializes_nested_public_files_without_transforming_bytes() {
        let dir = tempdir().unwrap();
        let public_dir = dir.path().join("public");
        let output_dir = dir.path().join("output");
        fs::create_dir_all(public_dir.join(".well-known")).unwrap();
        fs::create_dir_all(&output_dir).unwrap();
        fs::write(public_dir.join("ai.md"), b"# Pax\n\0raw").unwrap();
        fs::write(public_dir.join(".well-known/agent.txt"), b"pax").unwrap();

        materialize_public_dir(&public_dir, &output_dir).unwrap();

        assert_eq!(fs::read(output_dir.join("ai.md")).unwrap(), b"# Pax\n\0raw");
        assert_eq!(
            fs::read(output_dir.join(".well-known/agent.txt")).unwrap(),
            b"pax"
        );
    }

    #[test]
    fn absent_public_directory_is_a_no_op() {
        let dir = tempdir().unwrap();
        let output_dir = dir.path().join("output");
        fs::create_dir_all(&output_dir).unwrap();

        materialize_public_dir(&dir.path().join("public"), &output_dir).unwrap();

        assert!(fs::read_dir(output_dir).unwrap().next().is_none());
    }

    #[test]
    fn collision_fails_before_copying_any_public_file() {
        let dir = tempdir().unwrap();
        let public_dir = dir.path().join("public");
        let output_dir = dir.path().join("output");
        fs::create_dir_all(&public_dir).unwrap();
        fs::create_dir_all(&output_dir).unwrap();
        fs::write(public_dir.join("a-safe.txt"), b"safe").unwrap();
        fs::write(public_dir.join("index.html"), b"public index").unwrap();
        fs::write(output_dir.join("index.html"), b"generated index").unwrap();

        let err = materialize_public_dir(&public_dir, &output_dir).unwrap_err();

        assert!(err.to_string().contains("index.html"));
        assert!(!output_dir.join("a-safe.txt").exists());
        assert_eq!(
            fs::read(output_dir.join("index.html")).unwrap(),
            b"generated index"
        );
    }

    #[test]
    fn public_directories_can_merge_without_overwriting_files() {
        let dir = tempdir().unwrap();
        let public_dir = dir.path().join("public");
        let output_dir = dir.path().join("output");
        fs::create_dir_all(public_dir.join("docs")).unwrap();
        fs::create_dir_all(output_dir.join("docs")).unwrap();
        fs::write(public_dir.join("docs/ai.md"), b"ai").unwrap();
        fs::write(output_dir.join("docs/index.html"), b"index").unwrap();

        materialize_public_dir(&public_dir, &output_dir).unwrap();

        assert_eq!(fs::read(output_dir.join("docs/ai.md")).unwrap(), b"ai");
        assert_eq!(
            fs::read(output_dir.join("docs/index.html")).unwrap(),
            b"index"
        );
    }

    #[test]
    fn reserved_runtime_directories_are_rejected() {
        let dir = tempdir().unwrap();
        let public_dir = dir.path().join("public");
        let output_dir = dir.path().join("output");
        fs::create_dir_all(public_dir.join("__reloads__")).unwrap();
        fs::create_dir_all(&output_dir).unwrap();

        let err = validate_public_dir(&public_dir, &output_dir).unwrap_err();

        assert!(err.to_string().contains("reserved"));
        assert!(err.to_string().contains("__reloads__"));
    }

    #[test]
    fn live_public_filter_tracks_file_creation_edits_and_deletion() {
        let dir = tempdir().unwrap();
        let public_dir = dir.path().join("public");
        fs::create_dir_all(&public_dir).unwrap();
        let relative = std::path::Path::new("ai.md");

        assert!(!is_servable_public_path(&public_dir, relative));
        fs::write(public_dir.join(relative), b"one").unwrap();
        assert!(is_servable_public_path(&public_dir, relative));
        fs::write(public_dir.join(relative), b"two").unwrap();
        assert!(is_servable_public_path(&public_dir, relative));
        fs::remove_file(public_dir.join(relative)).unwrap();
        assert!(!is_servable_public_path(&public_dir, relative));
    }

    #[cfg(unix)]
    #[test]
    fn symbolic_links_are_rejected_for_builds_and_live_serving() {
        use std::os::unix::fs::symlink;

        let dir = tempdir().unwrap();
        let public_dir = dir.path().join("public");
        let output_dir = dir.path().join("output");
        fs::create_dir_all(&public_dir).unwrap();
        fs::create_dir_all(&output_dir).unwrap();
        fs::write(dir.path().join("secret.txt"), b"secret").unwrap();
        symlink(dir.path().join("secret.txt"), public_dir.join("ai.md")).unwrap();

        let err = validate_public_dir(&public_dir, &output_dir).unwrap_err();

        assert!(err.to_string().contains("symbolic link"));
        assert!(!is_servable_public_path(
            &public_dir,
            std::path::Path::new("ai.md")
        ));
    }
}