use std::io::Write;
use std::path::{Path, PathBuf};
static BUNDLE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/zsh_docs.zst"));
include!(concat!(env!("OUT_DIR"), "/zsh_docs_id.rs"));
const STAMP: &str = ".zshrs-docs-version";
fn stamp_value() -> String {
format!("{}-{}", env!("CARGO_PKG_VERSION"), DOCS_ID)
}
fn base_dir() -> Option<PathBuf> {
let home = std::env::var_os("HOME")?;
Some(PathBuf::from(home).join(".zshrs"))
}
pub fn man_dir() -> Option<PathBuf> {
Some(base_dir()?.join("man"))
}
pub fn info_dir() -> Option<PathBuf> {
Some(base_dir()?.join("info"))
}
pub fn help_dir() -> Option<PathBuf> {
Some(base_dir()?.join("help"))
}
fn stamp_version(stamp: &str) -> Option<(u64, u64, u64)> {
let version = stamp.rsplit_once('-')?.0;
let mut parts = version.split('.').map(|p| p.parse::<u64>().ok());
Some((parts.next()??, parts.next()??, parts.next()??))
}
fn needs_write(base: &Path) -> bool {
let Ok(on_disk) = std::fs::read_to_string(base.join(STAMP)) else {
return true;
};
let on_disk = on_disk.trim();
let ours = stamp_value();
if on_disk == ours {
return false;
}
match (stamp_version(on_disk), stamp_version(&ours)) {
(Some(theirs), Some(mine)) => theirs <= mine,
_ => true,
}
}
pub fn ensure_installed() -> Option<usize> {
let base = base_dir()?;
if !needs_write(&base) {
return Some(0);
}
let raw = zstd::decode_all(BUNDLE).ok()?;
let mut n = 0usize;
let mut i = 0usize;
while i + 4 <= raw.len() {
let nl = u32::from_le_bytes(raw[i..i + 4].try_into().ok()?) as usize;
i += 4;
if i + nl + 4 > raw.len() {
break;
}
let name = String::from_utf8_lossy(&raw[i..i + nl]).into_owned();
i += nl;
let bl = u32::from_le_bytes(raw[i..i + 4].try_into().ok()?) as usize;
i += 4;
if i + bl > raw.len() {
break;
}
let body = &raw[i..i + bl];
i += bl;
if name.is_empty() || name.starts_with('/') || name.contains("..") {
continue;
}
let dest = base.join(&name);
let Some(parent) = dest.parent() else { continue };
if std::fs::create_dir_all(parent).is_err() {
continue;
}
if std::fs::write(&dest, body).is_ok() {
n += 1;
}
}
if let Ok(mut f) = std::fs::File::create(base.join(STAMP)) {
let _ = f.write_all(stamp_value().as_bytes());
}
tracing::info!(target: "bundled_docs", written = n, dir = %base.display(),
"materialised bundled zsh man/info pages");
Some(n)
}
fn prepend_search_path(var: &str, dir: &Path) {
let dir = dir.to_string_lossy().into_owned();
let next = match std::env::var(var) {
Ok(cur) if !cur.is_empty() => {
if cur.split(':').any(|e| e == dir) {
return;
}
format!("{dir}:{cur}")
}
_ => format!("{dir}:"),
};
unsafe { std::env::set_var(var, next) };
}
fn published_dirs() -> Vec<(&'static str, PathBuf)> {
let mut out = Vec::new();
if let Some(d) = man_dir() {
if d.is_dir() {
out.push(("MANPATH", d));
}
}
if let Some(d) = info_dir() {
if d.is_dir() {
out.push(("INFOPATH", d));
}
}
out
}
fn inherited_home() -> Option<String> {
match crate::ported::params::environ.get() {
Some(env) => env
.iter()
.find(|(k, _)| k == "HOME")
.map(|(_, v)| v.clone()),
None => std::env::var("HOME").ok(),
}
.filter(|h| !h.is_empty())
}
fn helpdir_value() -> Option<String> {
let d = PathBuf::from(inherited_home()?).join(".zshrs").join("help");
d.is_dir().then(|| d.to_string_lossy().into_owned())
}
pub fn seed_helpdir_param() {
if crate::dash_mode::zsh_dropin() {
return; }
if crate::ported::params::getsparam("HELPDIR").is_some_and(|v| !v.is_empty()) {
return; }
if let Some(v) = helpdir_value() {
crate::ported::params::setsparam("HELPDIR", &v);
}
}
pub fn install_and_publish() {
if crate::dash_mode::zsh_dropin() {
return; }
let _ = ensure_installed();
for (var, dir) in published_dirs() {
prepend_search_path(var, &dir);
}
}
pub fn publish_into(env: &mut Vec<(String, String)>) {
if crate::dash_mode::zsh_dropin() {
return; }
let _ = ensure_installed();
for (var, dir) in published_dirs() {
let dir = dir.to_string_lossy().into_owned();
match env.iter_mut().find(|(k, _)| k == var) {
Some((_, v)) if !v.is_empty() => {
if !v.split(':').any(|e| e == dir) {
*v = format!("{dir}:{v}");
}
}
Some((_, v)) => *v = format!("{dir}:"),
None => env.push((var.to_string(), format!("{dir}:"))),
}
let val = env
.iter()
.find(|(k, _)| k == var)
.map(|(_, v)| v.clone())
.unwrap_or_default();
unsafe { std::env::set_var(var, val) };
}
}
#[cfg(test)]
mod tests {
use super::stamp_version;
#[test]
fn stamp_version_parses_version_and_ignores_hash() {
assert_eq!(stamp_version("0.13.19-ed24bc013fc546e4"), Some((0, 13, 19)));
assert!(stamp_version("0.13.9-aa") < stamp_version("0.13.10-bb"));
}
#[test]
fn stamp_version_rejects_malformed_stamps() {
assert_eq!(stamp_version(""), None);
assert_eq!(stamp_version("garbage"), None);
assert_eq!(stamp_version("0.13-ed24"), None);
}
}