hpip 0.1.2

Host These Things Please - a modern async HTTP file server
extern crate base64;

use std::collections::{BTreeMap, BTreeSet};
use std::env;
use std::fs::{self, File};
use std::io::{BufRead, BufReader, Write};
use std::path::Path;

use base64::display::Base64Display;
use base64::engine::general_purpose::STANDARD_NO_PAD as BASE64_STANDARD_NO_PAD;

fn main() {
    htmls();
    extensions();
}

fn assets() -> Vec<(&'static str, String)> {
    let mut assets = Vec::new();
    {
        println!("cargo:rerun-if-changed=Cargo.toml");
        assets.push((
            "generator",
            format!(
                "hpip {}",
                BufReader::new(File::open("Cargo.toml").unwrap())
                    .lines()
                    .map_while(Result::ok)
                    .find(|l| l.starts_with("version = "))
                    .unwrap()["version = ".len()..]
                    .trim_matches('"')
            ),
        ));
    }
    for (key, file) in [
        ("favicon", "assets/favicon.png"),
        ("dir_icon", "assets/icons/directory.gif"),
        ("file_icon", "assets/icons/file.gif"),
        ("file_binary_icon", "assets/icons/file_binary.gif"),
        ("file_image_icon", "assets/icons/file_image.gif"),
        ("file_text_icon", "assets/icons/file_text.gif"),
        ("back_arrow_icon", "assets/icons/back_arrow.gif"),
        ("new_dir_icon", "assets/icons/new_directory.gif"),
        ("delete_file_icon", "assets/icons/delete_file.png"),
        ("rename_icon", "assets/icons/rename.gif"),
        ("confirm_icon", "assets/icons/confirm.gif"),
    ] {
        println!("cargo:rerun-if-changed={}", file);
        assets.push((
            key,
            format!(
                "data:image/{};base64,{}",
                file.split('.').next_back().unwrap(),
                Base64Display::new(&fs::read(file).unwrap()[..], &BASE64_STANDARD_NO_PAD)
            ),
        ));
    }
    let outd = env::var("OUT_DIR").unwrap();
    fs::create_dir_all(format!("{}/{}", outd, "assets")).unwrap();
    for (key, file) in [
        ("manage", "assets/manage.js"),
        ("manage_desktop", "assets/manage_desktop.js"),
        ("upload", "assets/upload.js"),
        ("upload_css", "assets/upload.css"),
        ("adjust_tz", "assets/adjust_tz.js"),
    ] {
        println!("cargo:rerun-if-changed={}", file);
        let data = fs::read_to_string(file).unwrap();
        fs::write(
            format!("{}/{}", outd, file),
            data.lines()
                .flat_map(|l| [l.trim(), "\n"])
                .collect::<String>()
                .as_bytes(),
        )
        .unwrap();
        assets.push((key, data));
    }
    assets
}

fn htmls() {
    let assets = assets();
    for html in [
        "error.html",
        "directory_listing.html",
    ] {
        println!("cargo:rerun-if-changed=assets/{}", html);

        let with_assets = assets
            .iter()
            .fold(
                fs::read_to_string(format!("assets/{}", html)).unwrap(),
                |d, (k, v)| d.replace(&format!("{{{}}}", k), v),
            )
            .lines()
            .flat_map(|l| [l.trim(), "\n"])
            .collect::<String>();

        let mut arguments = BTreeMap::new();
        for i in 0.. {
            let len_pre = arguments.len();
            arguments.extend(
                with_assets
                    .match_indices(&format!("{{{}}}", i))
                    .map(|(start, s)| (start, (s.len(), i))),
            );
            if arguments.len() == len_pre {
                break;
            }
        }

        let mut data = Vec::new();
        let mut argsused = BTreeMap::<u32, u8>::new();
        let mut idx = 0;
        for (start, (len, argi)) in arguments {
            if !with_assets[idx..start].is_empty() {
                data.push(Ok(&with_assets[idx..start]));
            }
            data.push(Err(argi));
            *argsused.entry(argi).or_default() += 1;
            idx = start + len;
        }

        let mut out =
            File::create(Path::new(&env::var("OUT_DIR").unwrap()).join(format!("{}.rs", html)))
                .unwrap();
        write!(&mut out, "pub fn {}<", html.replace('.', "_")).unwrap();
        for (arg, nused) in &argsused {
            if *nused == 1 {
                write!(&mut out, "T{}: HtmlResponseElement, ", arg).unwrap();
            } else {
                write!(&mut out, "T{}: HtmlResponseElement + Copy, ", arg).unwrap();
            }
        }
        write!(&mut out, ">(").unwrap();
        for arg in argsused.keys() {
            write!(&mut out, "a{}: T{}, ", arg, arg).unwrap();
        }
        let raw_bytes = data.iter().fold(0, |sz, dt| match dt {
            Ok(s) => sz + s.len(),
            Err(_) => sz,
        });
        writeln!(
            &mut out,
            r#") -> String {{
    let mut ret = Vec::with_capacity({});  // {}"#,
            if html == "error.html" {
                raw_bytes.next_power_of_two()
            } else {
                32 * 1024
            },
            raw_bytes
        )
        .unwrap();
        for dt in data {
            match dt {
                Ok(s) => writeln!(&mut out, "    ret.extend({:?}.as_bytes());", s).unwrap(),
                Err(i) => writeln!(&mut out, "    a{}.commit(&mut ret);", i).unwrap(),
            }
        }
        writeln!(
            &mut out,
            "    ret.extend({:?}.as_bytes());",
            &with_assets[idx..]
        )
        .unwrap();

        writeln!(
            &mut out,
            r#"
    ret.shrink_to_fit();
    unsafe {{ String::from_utf8_unchecked(ret) }}
}}"#
        )
        .unwrap();
    }

    {
        let file = "assets/directory_listing_achive_inputs.html";
        println!("cargo:rerun-if-changed={}", file);
        fs::write(
            Path::new(&env::var("OUT_DIR").unwrap()).join(file),
            fs::read_to_string(file).unwrap().trim().as_bytes(),
        )
        .unwrap();
    }
}

fn extensions() {
    println!("cargo:rerun-if-changed=assets/encoding_blacklist");
    let mut out =
        File::create(Path::new(&env::var("OUT_DIR").unwrap()).join("extensions.rs")).unwrap();

    let raw = fs::read_to_string("assets/encoding_blacklist").unwrap();
    let mut exts = BTreeMap::new();
    for ext in raw
        .split('\n')
        .map(str::trim)
        .filter(|s| !s.is_empty() && !s.starts_with('#'))
    {
        exts.entry(ext.len()).or_insert(BTreeSet::new()).insert(ext);
    }
    writeln!(
        out,
        "pub fn extension_is_blacklisted(ext: &std::ffi::OsStr) -> bool {{"
    )
    .unwrap();
    writeln!(
        out,
        "#[cfg(not(target_os = \"windows\"))] use std::os::unix::ffi::OsStrExt;"
    )
    .unwrap();

    write!(out, "if !matches!(ext.len(),").unwrap();
    for (i, len) in exts.keys().enumerate() {
        write!(out, " {} {}", if i == 0 { "" } else { "|" }, len).unwrap();
    }
    writeln!(out, ") {{ return false; }}").unwrap();

    let maxlen = exts.keys().max().unwrap();
    writeln!(
        out,
        r#"
let mut buf = [0u8; {}];
#[cfg(not(target_os = "windows"))]
let bytes = ext.as_bytes();
#[cfg(target_os = "windows")]
let bytes = ext.as_encoded_bytes();
for (i, b) in bytes.iter().enumerate() {{
if !b.is_ascii_alphanumeric() {{
    return false;
}}
buf[i] = b.to_ascii_lowercase();
}}
let lcase = &buf[0..ext.len()];
"#,
        maxlen
    )
    .unwrap();

    write!(out, "matches!(lcase,").unwrap();
    for (i, ext) in exts.values().flatten().enumerate() {
        write!(out, " {} b{:?}", if i == 0 { "" } else { "|" }, ext).unwrap();
    }
    writeln!(out, ")").unwrap();

    writeln!(out, "}}").unwrap();
}