magicpak 1.4.0

Build minimal docker images without static linking
Documentation
use std::ffi::{OsStr, OsString};
use std::io;
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};

use crate::base::{Error, Result};

#[derive(Hash, Default, Debug)]
pub struct SearchPaths {
    rpath: Option<Vec<PathBuf>>,
    runpath: Option<Vec<PathBuf>>,
    ld_library_path: Option<Vec<PathBuf>>,
    platform: OsString,
    origin: OsString,
}

impl SearchPaths {
    pub fn new(origin: OsString) -> Result<Self> {
        Ok(SearchPaths {
            rpath: None,
            runpath: None,
            ld_library_path: None,
            platform: auxv_platform()?,
            origin,
        })
    }

    pub fn rpath(&self) -> Option<&Vec<PathBuf>> {
        self.rpath.as_ref()
    }

    pub fn runpath(&self) -> Option<&Vec<PathBuf>> {
        self.runpath.as_ref()
    }

    pub fn iter_rpaths(&self) -> impl Iterator<Item = &Path> {
        self.rpath.iter().flat_map(|v| v.iter().map(AsRef::as_ref))
    }

    pub fn iter_runpaths(&self) -> impl Iterator<Item = &Path> {
        self.runpath
            .iter()
            .flat_map(|v| v.iter().map(AsRef::as_ref))
    }

    pub fn iter_ld_library_paths(&self) -> impl Iterator<Item = &Path> {
        self.ld_library_path
            .iter()
            .flat_map(|v| v.iter().map(AsRef::as_ref))
    }

    fn append<I, S>(paths: &mut Option<Vec<PathBuf>>, other: I, origin: &OsStr, platform: &OsStr)
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        let inner = paths.get_or_insert(Vec::new());
        inner.extend(
            other
                .into_iter()
                .map(|x| expand_tokens(x, origin, platform)),
        )
    }

    pub fn append_rpath<I, S>(&mut self, rpath: I)
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        let SearchPaths {
            origin, platform, ..
        } = self;

        Self::append(&mut self.rpath, rpath, origin, platform)
    }

    pub fn append_runpath<I, S>(&mut self, runpath: I)
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        let SearchPaths {
            origin, platform, ..
        } = self;

        Self::append(&mut self.runpath, runpath, origin, platform)
    }

    pub fn append_ld_library_path<I, S>(&mut self, ld_library_path: I)
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        let SearchPaths {
            origin, platform, ..
        } = self;

        Self::append(&mut self.ld_library_path, ld_library_path, origin, platform)
    }
}

fn expand_tokens<S, T, U>(input: S, origin: T, platform: U) -> PathBuf
where
    S: AsRef<OsStr>,
    T: AsRef<OsStr>,
    U: AsRef<OsStr>,
{
    let input = input.as_ref();

    let mut result = OsString::new();
    result.reserve(input.len());

    let mut buffer = Vec::new();
    buffer.reserve(16);

    enum ParseState {
        Standby,
        Scanning,
        ScanningBraced,
    }

    let state = input
        .as_bytes()
        .iter()
        .fold(ParseState::Standby, |state, b| match (state, b) {
            (ParseState::Standby, b'$') => {
                result.push(OsStr::from_bytes(&buffer));
                buffer.clear();
                ParseState::Scanning
            }
            (ParseState::Scanning, b'{') if buffer.is_empty() => ParseState::ScanningBraced,
            (ParseState::Scanning, b'$') => {
                result.push(substitute(&buffer, &origin, &platform));
                buffer.clear();

                ParseState::Scanning
            }
            (ParseState::Scanning, b'/') => {
                result.push(substitute(&buffer, &origin, &platform));
                buffer.clear();

                buffer.push(*b);
                ParseState::Standby
            }
            (ParseState::ScanningBraced, b'}') => {
                result.push(substitute(&buffer, &origin, &platform));
                buffer.clear();
                ParseState::Standby
            }
            (s, b) => {
                buffer.push(*b);
                s
            }
        });

    match state {
        ParseState::ScanningBraced => {
            tracing::warn!(
                token = ?String::from_utf8_lossy(&buffer),
                "search_paths: unterminated braced token",
            );
            result.push(OsStr::from_bytes(&buffer));
        }
        ParseState::Scanning => {
            result.push(substitute(&buffer, &origin, &platform));
        }
        ParseState::Standby => {
            result.push(OsStr::from_bytes(&buffer));
        }
    }

    if input != result {
        tracing::info!(
            input = %input.to_string_lossy(),
            result = %result.to_string_lossy(),
            "search_paths: expand",
        );
    }

    result.into()
}

fn substitute<S, T>(s: &[u8], origin: S, platform: T) -> OsString
where
    S: AsRef<OsStr>,
    T: AsRef<OsStr>,
{
    match s {
        b"ORIGIN" => origin.as_ref().to_owned(),
        b"LIB" => match is_64bit(&platform) {
            Some(true) => OsStr::new("lib64").to_owned(),
            Some(false) => OsStr::new("lib").to_owned(),
            None => {
                tracing::warn!(
                    platform = %platform.as_ref().to_string_lossy(),
                    "search_paths: assuming 32-bit platform",
                );
                OsStr::new("lib").to_owned()
            }
        },
        b"PLATFORM" => platform.as_ref().to_owned(),
        _ => {
            tracing::warn!(
                token = %format!("${}", String::from_utf8_lossy(s)),
                "search_paths: unknown dynamic string token",
            );
            OsString::from_vec([&[b'$'], s].concat().to_vec())
        }
    }
}

fn is_64bit<S>(platform: S) -> Option<bool>
where
    S: AsRef<OsStr>,
{
    match platform.as_ref().to_string_lossy().as_ref() {
        "x86_64" | "amd64" | "aarch64" => Some(true),
        "i386" | "i686" | "x86" | "arm" => Some(false),
        _ => None,
    }
}

fn auxv_platform() -> Result<OsString> {
    let mut reader = crt0stack::Reader::from_environ().done();
    let platform = reader
        .find_map(|entry| {
            if let crt0stack::Entry::Platform(platform) = entry {
                Some(platform)
            } else {
                None
            }
        })
        .ok_or_else(|| {
            Error::IO(io::Error::new(
                io::ErrorKind::NotFound,
                "could not find AT_PLATFORM auxval",
            ))
        })?;
    tracing::debug!(%platform, "search_paths: read platform from auxv");
    Ok(platform.into())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_substitute() {
        let origin = "/home/user/";
        let platform = "x86_64";
        assert_eq!(substitute(b"ORIGIN", origin, platform), origin);
        assert_eq!(substitute(b"PLATFORM", origin, platform), platform);

        assert_eq!(substitute(b"LIB", origin, "x86_64"), "lib64");
        assert_eq!(substitute(b"LIB", origin, "x86"), "lib");

        assert_eq!(substitute(b"WTF", origin, platform), "$WTF");
    }

    #[test]
    fn test_expand_tokens() {
        let origin = "/home/user/";
        let platform = "x86_64";
        assert_eq!(
            expand_tokens("$ORIGIN/$LIB", origin, platform),
            PathBuf::from(format!("{}/lib64", origin))
        );
        assert_eq!(
            expand_tokens("${ORIGIN}/${LIB}", origin, platform),
            PathBuf::from(format!("{}/lib64", origin))
        );
        assert_eq!(
            expand_tokens("/$ORIGIN$LIB", origin, platform),
            PathBuf::from(format!("/{}lib64", origin))
        );
        assert_eq!(
            expand_tokens("/lib/$PLATFORM", origin, platform),
            PathBuf::from(format!("/lib/{}", platform))
        );
        assert_eq!(
            expand_tokens("${PLATFORM}${ORIGIN}", origin, platform),
            PathBuf::from(format!("{}{}", platform, origin))
        );
    }
}