Skip to main content

mant_loader/
manual_input.rs

1//! Product input policy: prepare bounded bytes, then invoke the pure roff codec.
2
3mod error;
4mod source;
5
6use crate::ManualPage;
7use libmandoc_rs::ParseReport;
8use mant_codec::{parse_roff_bytes, parse_roff_bytes_with_report, redirect_target};
9use mant_ir::Document;
10use source::{load_manual_source, resolve_manual_redirects};
11use std::path::Path;
12
13pub use error::{ManualError, ManualErrorKind};
14pub use source::MAX_MANUAL_BYTES;
15
16/// Parse and normalize one standalone man or mdoc source file.
17///
18/// This safe convenience entry point does not expand `.so` redirects because
19/// no caller-approved manual hierarchy accompanies a bare path. `ManT`'s indexed
20/// query path uses [`parse_manual_page`] instead.
21///
22/// # Errors
23///
24/// Returns [`ManualError`] when the source cannot be opened, decoded, or parsed.
25pub fn parse_manual_source(path: &Path) -> Result<Document, ManualError> {
26    parse_manual_source_with_report(path).map(|(document, _)| document)
27}
28
29/// Parse through the production file pipeline, retaining its native witness.
30///
31/// Both results describe the same bounded, decompressed and control-masked
32/// input, with the same deny-include policy as [`parse_manual_source`]. This
33/// avoids a second parser invocation when consumers audit native-to-IR facts.
34/// The native report is fully owned and no source file is read during lowering.
35///
36/// # Errors
37///
38/// Returns [`ManualError`] on source, decompression, redirect or parser failure.
39pub fn parse_manual_source_with_report(
40    path: &Path,
41) -> Result<(Document, ParseReport), ManualError> {
42    let loaded = load_manual_source(path)?;
43    reject_standalone_redirect(path, &loaded.source)?;
44    parse_roff_bytes_with_report(path, &loaded.source).map_err(ManualError::from)
45}
46
47/// Parse one already bounded, uncompressed standalone roff input.
48///
49/// This is the standard-input counterpart of [`parse_manual_source`]. It does
50/// not expand `.so` redirects and never reads another file.
51///
52/// # Errors
53///
54/// Returns [`ManualError`] when libmandoc rejects the input.
55pub fn parse_manual_bytes(path: &Path, source: &[u8]) -> Result<Document, ManualError> {
56    reject_standalone_redirect(path, source)?;
57    parse_roff_bytes(path, source).map_err(ManualError::from)
58}
59
60fn reject_standalone_redirect(path: &Path, source: &[u8]) -> Result<(), ManualError> {
61    if redirect_target(source)
62        .map_err(|error| ManualError::redirect(path, error.to_string()))?
63        .is_some()
64    {
65        return Err(ManualError::redirect(
66            path,
67            "standalone .so redirects require MANPATH discovery and cannot be followed by --input",
68        ));
69    }
70    Ok(())
71}
72
73/// Parse an indexed manual, resolving `.so` redirects against its discovered
74/// manual hierarchy without falling back to the process working directory.
75///
76/// # Errors
77///
78/// Returns [`ManualError`] when the source cannot be opened, decoded, or parsed.
79pub fn parse_manual_page(page: &ManualPage) -> Result<Document, ManualError> {
80    let resolved = resolve_manual_redirects(page)?;
81    let mut document = parse_roff_bytes(&page.path, &resolved.source)?;
82    if let Some(alias_target) = resolved.alias_target {
83        document.meta.alias_target = Some(alias_target);
84    }
85    Ok(document)
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn standalone_inputs_reject_redirect_only_so_pages() {
94        let path = Path::new("stdin");
95        let error = parse_manual_bytes(path, b".so man1/target.1\n")
96            .expect_err("standalone input must not follow another file");
97        assert_eq!(error.kind(), ManualErrorKind::Redirect);
98        assert_eq!(error.path(), path);
99        assert!(error.to_string().contains("require MANPATH discovery"));
100    }
101
102    #[test]
103    fn standalone_malformed_redirect_keeps_original_path_and_failure_kind() {
104        for source in [
105            b".so\n".as_slice(),
106            b".so first second\n",
107            b".so bad\0name\n",
108        ] {
109            let path = Path::new("original display name.1");
110            let error = parse_manual_bytes(path, source).expect_err("reject malformed alias");
111            assert_eq!(error.kind(), ManualErrorKind::Redirect);
112            assert_eq!(error.path(), path);
113            assert_eq!(
114                error.message(),
115                "manual .so redirect must contain exactly one target path"
116            );
117        }
118    }
119}