retex-lua 1.3.0-rc.1

Embedded Lua and the LuaTeX Lua API for retex
Documentation
//! The `lfs` library (LuaFileSystem API, plus LuaTeX's additions
//! `isdir`, `isfile`, `mkdirp`, `readlink`, `shortname`).

use std::path::Path;

use mlua::{Lua, MultiValue, Table, Value};

/// Records that Lua code looked at file `p`, for the pass driver: a
/// later pass must rerun if the file changes (see `Engine::record_probe`).
fn record(lua: &Lua, p: &str) {
    let path = Path::new(p);
    let found = path.is_file().then_some(path);
    let _ = crate::with_engine(lua, |e| e.record_probe(p, found));
}

/// Makes `io.open` for reading record the file (see [`record`]).
fn wrap_io_open(lua: &Lua) -> mlua::Result<()> {
    let rec = lua.create_function(|lua, p: String| {
        record(lua, &p);
        Ok(())
    })?;
    lua.load(
        r#"
        local record = ...
        local open = io.open
        io.open = function(name, mode)
          if type(name) == "string" and (mode == nil or string.sub(mode, 1, 1) == "r") then
            record(name)
          end
          return open(name, mode)
        end
        "#,
    )
    .set_name("=[retex io.open]")
    .call::<()>(rec)
}

/// Installs `lfs` (also available as `require("lfs")`).
pub fn install(lua: &Lua) -> mlua::Result<()> {
    wrap_io_open(lua)?;
    let t = lua.create_table()?;
    t.set(
        "attributes",
        lua.create_function(|lua, (p, req): (String, Option<String>)| {
            record(lua, &p);
            attributes(lua, &p, req, true)
        })?,
    )?;
    t.set(
        "symlinkattributes",
        lua.create_function(|lua, (p, req): (String, Option<String>)| {
            attributes(lua, &p, req, false)
        })?,
    )?;
    t.set(
        "isdir",
        lua.create_function(|_, p: String| Ok(Path::new(&p).is_dir()))?,
    )?;
    t.set(
        "isfile",
        lua.create_function(|lua, p: String| {
            record(lua, &p);
            Ok(Path::new(&p).is_file())
        })?,
    )?;
    t.set(
        "currentdir",
        lua.create_function(|_, ()| {
            Ok(std::env::current_dir()
                .map(|d| d.to_string_lossy().into_owned())
                .ok())
        })?,
    )?;
    t.set(
        "chdir",
        lua.create_function(|lua, p: String| result(lua, std::env::set_current_dir(&p)))?,
    )?;
    t.set(
        "mkdir",
        lua.create_function(|lua, p: String| result(lua, std::fs::create_dir(&p)))?,
    )?;
    t.set(
        "mkdirp",
        lua.create_function(|lua, p: String| result(lua, std::fs::create_dir_all(&p)))?,
    )?;
    t.set(
        "rmdir",
        lua.create_function(|lua, p: String| result(lua, std::fs::remove_dir(&p)))?,
    )?;
    t.set(
        "touch",
        lua.create_function(|lua, (p, _a, _m): (String, Option<f64>, Option<f64>)| {
            let r = std::fs::OpenOptions::new()
                .create(true)
                .append(true)
                .open(&p)
                .map(|_| ());
            result(lua, r)
        })?,
    )?;
    t.set(
        "readlink",
        lua.create_function(|_, p: String| {
            Ok(std::fs::read_link(&p)
                .map(|d| d.to_string_lossy().into_owned())
                .ok())
        })?,
    )?;
    t.set("shortname", lua.create_function(|_, p: String| Ok(p))?)?;
    t.set(
        "dir",
        lua.create_function(|lua, p: String| {
            let mut names: Vec<String> = vec![".".into(), "..".into()];
            if let Ok(rd) = std::fs::read_dir(&p) {
                for e in rd.flatten() {
                    names.push(e.file_name().to_string_lossy().into_owned());
                }
            }
            let state = lua.create_table()?;
            for (i, n) in names.into_iter().enumerate() {
                state.set(i + 1, n)?;
            }
            state.set("i", 0)?;
            let iter = lua.create_function(|_, st: Table| {
                let i: i64 = st.get("i")?;
                st.set("i", i + 1)?;
                st.get::<Value>(i + 1)
            })?;
            Ok((iter, state))
        })?,
    )?;
    t.set("lock", lua.create_function(|_, _a: MultiValue| Ok(true))?)?;
    t.set("unlock", lua.create_function(|_, _a: MultiValue| Ok(true))?)?;
    t.set(
        "setmode",
        lua.create_function(|_, _a: MultiValue| Ok(true))?,
    )?;
    lua.globals().set("lfs", t.clone())?;
    let package: Table = lua.globals().get("package")?;
    let loaded: Table = package.get("loaded")?;
    loaded.set("lfs", t)?;
    Ok(())
}

fn result(lua: &Lua, r: std::io::Result<()>) -> mlua::Result<MultiValue> {
    match r {
        Ok(()) => Ok(MultiValue::from_iter([Value::Boolean(true)])),
        Err(e) => Ok(MultiValue::from_iter([
            Value::Nil,
            Value::String(lua.create_string(e.to_string())?),
        ])),
    }
}

/// The permission bits (`0o777`) and the numeric fields of `lfs.attributes`.
#[cfg(unix)]
fn unix_fields(md: &std::fs::Metadata) -> (u32, [(&'static str, i64); 12]) {
    use std::os::unix::fs::{MetadataExt, PermissionsExt};
    (
        md.permissions().mode(),
        [
            ("size", md.size() as i64),
            ("modification", md.mtime()),
            ("access", md.atime()),
            ("change", md.ctime()),
            ("dev", md.dev() as i64),
            ("ino", md.ino() as i64),
            ("nlink", md.nlink() as i64),
            ("uid", md.uid().into()),
            ("gid", md.gid().into()),
            ("rdev", md.rdev() as i64),
            ("blocks", md.blocks() as i64),
            ("blksize", md.blksize() as i64),
        ],
    )
}

/// Elsewhere: the times and the size, read-only files without `w`
/// (as LuaFileSystem does on Windows), the Unix numbers 0.
#[cfg(not(unix))]
fn unix_fields(md: &std::fs::Metadata) -> (u32, [(&'static str, i64); 12]) {
    let secs = |t: std::io::Result<std::time::SystemTime>| {
        t.ok()
            .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
            .map_or(0, |d| d.as_secs() as i64)
    };
    let perm = if md.permissions().readonly() {
        0o444
    } else {
        0o666
    };
    (
        perm,
        [
            ("size", md.len() as i64),
            ("modification", secs(md.modified())),
            ("access", secs(md.accessed())),
            ("change", secs(md.created())),
            ("dev", 0),
            ("ino", 0),
            ("nlink", 1),
            ("uid", 0),
            ("gid", 0),
            ("rdev", 0),
            ("blocks", 0),
            ("blksize", 0),
        ],
    )
}

fn attributes(lua: &Lua, p: &str, req: Option<String>, follow: bool) -> mlua::Result<MultiValue> {
    let md = if follow {
        std::fs::metadata(p)
    } else {
        std::fs::symlink_metadata(p)
    };
    let md = match md {
        Ok(m) => m,
        Err(e) => {
            return Ok(MultiValue::from_iter([
                Value::Nil,
                Value::String(lua.create_string(format!("{p}: {e}"))?),
            ]));
        }
    };
    let ft = md.file_type();
    let mode = if ft.is_dir() {
        "directory"
    } else if ft.is_symlink() {
        "link"
    } else if ft.is_file() {
        "file"
    } else {
        "other"
    };
    let (perm, fields) = unix_fields(&md);
    let mut ps = String::new();
    for (bit, c) in [
        (0o400, 'r'),
        (0o200, 'w'),
        (0o100, 'x'),
        (0o040, 'r'),
        (0o020, 'w'),
        (0o010, 'x'),
        (0o004, 'r'),
        (0o002, 'w'),
        (0o001, 'x'),
    ] {
        ps.push(if perm & bit != 0 { c } else { '-' });
    }
    let t = lua.create_table()?;
    t.set("mode", mode)?;
    for (k, v) in fields {
        t.set(k, v)?;
    }
    t.set("permissions", ps)?;
    match req {
        Some(r) => Ok(MultiValue::from_iter([t.get::<Value>(r)?])),
        None => Ok(MultiValue::from_iter([Value::Table(t)])),
    }
}