Skip to main content

frontmatter_gen/
lib.rs

1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![doc(
4    html_favicon_url = "https://kura.pro/frontmatter-gen/images/favicon.ico",
5    html_logo_url = "https://cloudcdn.pro/clients/frontmatter-gen/v1/logos/frontmatter-gen.svg",
6    html_root_url = "https://docs.rs/frontmatter-gen"
7)]
8
9//! # Frontmatter Gen
10//!
11//! `frontmatter-gen` is a fast, secure, and memory-efficient library for working with
12//! frontmatter in multiple formats (YAML, TOML, and JSON).
13//!
14//! ## Overview
15//!
16//! This library provides robust handling of frontmatter with the following key features:
17//!
18//! - **Zero-copy parsing** for optimal memory efficiency
19//! - **Type-safe operations** with comprehensive error handling
20//! - **Multiple format support** (YAML, TOML, JSON)
21//! - **Secure processing** with input validation and size limits
22//! - **Async support** with the `ssg` feature flag
23//!
24//! ## Security Features
25//!
26//! - Input validation to prevent malicious content
27//! - Size limits to prevent denial of service attacks
28//! - Safe string handling to prevent memory corruption
29//! - Secure path handling for file operations
30//!
31//! ## Quick Start
32//!
33//! ```rust
34//! use frontmatter_gen::{extract, Format, Frontmatter, Result};
35//!
36//! let content = r#"---
37//! title: Test Post
38//! date: 2025-09-09
39//! ---
40//! Content here"#;
41//!
42//! let result = extract(content);
43//! assert!(result.is_ok());
44//! let (frontmatter, content) = result.unwrap();
45//! assert_eq!(
46//!     frontmatter.get("title").and_then(|v| v.as_str()),
47//!     Some("Test Post")
48//! );
49//! assert_eq!(content.trim(), "Content here");
50//! # Ok::<(), frontmatter_gen::Error>(())
51//! ```
52//!
53//! ## Feature Flags
54//!
55//! - `default`: Core frontmatter functionality
56//! - `cli`: Command-line interface support
57//! - `ssg`: Static Site Generator functionality (includes CLI)
58//!
59//! ## Error Handling
60//!
61//! All operations return a `Result` type with detailed error information.
62//! Use `frontmatter.get("key")` to access fields and pattern-match on
63//! `Error` variants for granular error handling.
64
65use std::num::NonZeroUsize;
66
67// Re-export core types and traits
68pub use crate::{
69    config::Config,
70    error::Error,
71    extractor::{detect_format, extract_raw_frontmatter},
72    parser::{parse, to_string},
73    types::{Format, Frontmatter, Value},
74};
75
76// Module declarations
77#[cfg(feature = "cli")]
78pub mod cli;
79pub mod config;
80pub mod engine;
81pub mod error;
82pub mod extractor;
83pub mod parser;
84#[cfg(feature = "ssg")]
85pub mod ssg;
86pub mod types;
87pub mod utils;
88
89/// Maximum size allowed for frontmatter content (1 MiB).
90pub const MAX_FRONTMATTER_SIZE: NonZeroUsize =
91    match NonZeroUsize::new(1024 * 1024) {
92        Some(val) => val,
93        None => unreachable!(),
94    };
95
96/// Maximum allowed nesting depth for structured data (32 levels).
97///
98/// Inputs exceeding this nesting ceiling are rejected during validation.
99pub const MAX_NESTING_DEPTH: NonZeroUsize = match NonZeroUsize::new(32)
100{
101    Some(val) => val,
102    None => unreachable!(),
103};
104
105/// A specialized Result type for frontmatter operations.
106///
107/// This type alias provides a consistent error type throughout the crate
108/// and simplifies error handling for library users.
109pub type Result<T> = std::result::Result<T, Error>;
110
111/// Prelude module for convenient imports.
112///
113/// This module provides the most commonly used types and traits.
114/// Import all contents with `use frontmatter_gen::prelude::*`.
115pub mod prelude {
116    pub use crate::{
117        extract, to_format, Config, Error, Format, Frontmatter, Result,
118        Value,
119    };
120}
121
122/// Configuration options for parsing operations.
123///
124/// Provides fine-grained control over parsing behaviour and security limits.
125#[derive(Debug, Clone, Copy)]
126pub struct ParseOptions {
127    /// Maximum allowed content size
128    pub max_size: NonZeroUsize,
129    /// Maximum allowed nesting depth
130    pub max_depth: NonZeroUsize,
131    /// Whether to validate content structure
132    pub validate: bool,
133}
134
135impl Default for ParseOptions {
136    fn default() -> Self {
137        Self {
138            max_size: MAX_FRONTMATTER_SIZE,
139            max_depth: MAX_NESTING_DEPTH,
140            validate: true,
141        }
142    }
143}
144
145const ERR_PATH_TRAVERSAL: &str =
146    "Content contains path traversal patterns";
147const ERR_NULL_BYTES: &str = "Content contains null bytes";
148
149/// Validates input content against security constraints.
150///
151/// # Security
152///
153/// This function helps prevent denial of service attacks by:
154/// - Limiting the maximum size of frontmatter content
155/// - Skipping validation for fenced code blocks
156/// - Checking for malicious patterns
157///
158/// # Examples
159///
160/// ```rust
161/// use frontmatter_gen::{validate_input, ParseOptions};
162///
163/// let content = "---\ntitle: Example\n---\nBody content";
164/// let options = ParseOptions::default();
165/// assert!(validate_input(content, &options).is_ok());
166/// ```
167///
168/// # Errors
169///
170/// Returns [`Error::ValidationError`] on path traversal or null bytes, and
171/// [`Error::ContentTooLarge`] if `content.len()` exceeds `options.max_size`.
172/// Returns `Ok(())` when all checks succeed.
173pub fn validate_input(
174    content: &str,
175    options: &ParseOptions,
176) -> Result<()> {
177    let mut inside_fenced_code = false;
178
179    for line in content.lines() {
180        let trimmed = line.trim_start();
181        if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
182            inside_fenced_code = !inside_fenced_code;
183        } else if !inside_fenced_code {
184            // Path traversal detection
185            if line.contains("../") || line.contains("..\\") {
186                // Log the offending line before returning the validation error.
187                // Both Unix and Windows parent-directory separators are rejected.
188                // Fenced code blocks are excluded above.
189                log::warn!("Potential path traversal detected: {line}");
190                let msg = ERR_PATH_TRAVERSAL.to_string();
191                return Err(Error::ValidationError(msg));
192            }
193
194            // Null byte validation
195            if line.contains('\0') {
196                log::warn!("Null byte detected in content");
197                // Reject embedded NUL bytes
198                let msg = ERR_NULL_BYTES.to_string();
199                return Err(Error::ValidationError(msg));
200            }
201        }
202    }
203
204    // Check size limit
205    let size = content.len();
206    let max = options.max_size.get();
207    if size > max {
208        log::warn!("Content exceeds maximum size: {size} > {max}");
209        return Err(Error::ContentTooLarge { size, max });
210    }
211
212    Ok(())
213}
214
215/// Extracts and parses frontmatter from content with format auto-detection.
216///
217/// This function provides zero-copy extraction of frontmatter where possible,
218/// automatically detecting the format (YAML, TOML, or JSON) and parsing it
219/// into a structured representation.
220///
221/// # Security
222///
223/// This function includes several security measures:
224/// - Input validation and size limits
225/// - Safe string handling
226/// - Protection against malicious content
227///
228/// # Performance
229///
230/// Optimized for performance with:
231/// - Zero-copy operations where possible
232/// - Single-pass parsing
233/// - Minimal allocations
234/// - Pre-allocated buffers
235///
236/// # Examples
237///
238/// ```rust
239/// use frontmatter_gen::extract;
240///
241/// let content = r#"---
242/// title: My Post
243/// date: 2025-09-09
244/// ---
245/// Content here"#;
246///
247/// let (frontmatter, content) = extract(content)?;
248/// assert_eq!(frontmatter.get("title").unwrap().as_str().unwrap(), "My Post");
249/// assert_eq!(content.trim(), "Content here");
250/// # Ok::<(), frontmatter_gen::Error>(())
251/// ```
252///
253/// # Errors
254///
255/// Returns `Error` if:
256/// - Content exceeds size limits
257/// - Content is malformed
258/// - Frontmatter format is invalid
259/// - Parsing fails
260pub fn extract(content: &str) -> Result<(Frontmatter, &str)> {
261    let options = ParseOptions::from_env();
262    validate_input(content, &options)?;
263
264    let (raw_frontmatter, remaining_content) =
265        extract_raw_frontmatter(content)?;
266    let format = detect_format(raw_frontmatter)?;
267    let frontmatter = parse(raw_frontmatter, format)?;
268
269    Ok((frontmatter, remaining_content))
270}
271
272/// Converts frontmatter to a specific format.
273///
274/// # Arguments
275///
276/// * `frontmatter` - The frontmatter to convert
277/// * `format` - Target format for conversion
278///
279/// # Security
280///
281/// This function includes validation of:
282/// - Input size limits
283/// - Format compatibility
284/// - Output safety
285///
286/// # Examples
287///
288/// ```rust
289/// use frontmatter_gen::{Frontmatter, Format, Value, to_format};
290///
291/// let mut frontmatter = Frontmatter::new();
292/// frontmatter.insert("title".to_string(), Value::String("My Post".into()));
293///
294/// let yaml = to_format(&frontmatter, Format::Yaml)?;
295/// assert!(yaml.contains("title: My Post"));
296/// # Ok::<(), frontmatter_gen::Error>(())
297/// ```
298///
299/// # Errors
300///
301/// Returns `Error` if:
302/// - Serialization fails
303/// - Format conversion fails
304/// - Invalid data types are encountered
305pub fn to_format(
306    frontmatter: &Frontmatter,
307    format: Format,
308) -> Result<String> {
309    to_string(frontmatter, format)
310}
311
312impl ParseOptions {
313    /// Load options from environment variables or use defaults.
314    ///
315    /// Reads the following environment variables:
316    /// - `MAX_FRONTMATTER_SIZE`: Maximum size for frontmatter content.
317    /// - `MAX_NESTING_DEPTH`: Maximum allowed nesting depth.
318    /// - `VALIDATE_STRUCTURE`: Enable or disable structure validation (default: `true`).
319    ///
320    /// # Example
321    ///
322    /// ```rust
323    /// use frontmatter_gen::ParseOptions;
324    /// std::env::set_var("MAX_FRONTMATTER_SIZE", "2048");
325    /// std::env::set_var("MAX_NESTING_DEPTH", "64");
326    ///
327    /// let options = ParseOptions::from_env();
328    /// assert_eq!(options.max_size.get(), 2048);
329    /// assert_eq!(options.max_depth.get(), 64);
330    /// assert!(options.validate);
331    /// ```
332    pub fn from_env() -> Self {
333        let max_size = std::env::var("MAX_FRONTMATTER_SIZE")
334            .ok()
335            .and_then(|val| val.parse::<NonZeroUsize>().ok())
336            .unwrap_or(MAX_FRONTMATTER_SIZE);
337
338        let max_depth = std::env::var("MAX_NESTING_DEPTH")
339            .ok()
340            .and_then(|val| val.parse::<NonZeroUsize>().ok())
341            .unwrap_or(MAX_NESTING_DEPTH);
342
343        Self {
344            max_size,
345            max_depth,
346            validate: std::env::var("VALIDATE_STRUCTURE")
347                .map_or(true, |val| val.eq_ignore_ascii_case("true")),
348        }
349    }
350}
351
352#[cfg(test)]
353mod extractor_tests {
354    use crate::Error;
355
356    fn mock_operation(input: Option<&str>) -> Result<String, Error> {
357        match input {
358            Some(value) => Ok(value.to_uppercase()), // Successful operation
359            None => {
360                Err(Error::ParseError("Input is missing".to_string()))
361            }
362        }
363    }
364
365    #[test]
366    fn test_result_type_success() {
367        let input = Some("hello");
368        let result = mock_operation(input);
369        assert!(result.is_ok());
370        assert_eq!(result.unwrap(), "HELLO".to_string());
371    }
372
373    #[test]
374    fn test_result_type_error() {
375        let input = None;
376        let result = mock_operation(input);
377        assert!(matches!(
378            result,
379            Err(Error::ParseError(ref e)) if e == "Input is missing"
380        ));
381    }
382
383    #[test]
384    fn test_result_type_pattern_matching() {
385        let input = Some("world");
386        let result = mock_operation(input);
387        assert_eq!(result.ok(), Some("WORLD".to_string()));
388    }
389
390    #[test]
391    fn test_result_type_unwrap() {
392        let input = Some("rust");
393        let result = mock_operation(input);
394        assert_eq!(result.unwrap(), "RUST".to_string());
395    }
396
397    #[test]
398    fn test_result_type_expect() {
399        let input = Some("test");
400        let result = mock_operation(input);
401        assert_eq!(
402            result.expect("Unexpected error"),
403            "TEST".to_string()
404        );
405    }
406
407    #[test]
408    fn test_result_type_debug_format() {
409        let input = None;
410        let result = mock_operation(input);
411        assert_eq!(
412            format!("{:?}", result),
413            "Err(ParseError(\"Input is missing\"))"
414        );
415    }
416}
417
418#[cfg(test)]
419mod parser_tests {
420    use super::*;
421
422    #[test]
423    fn test_parse_yaml_frontmatter() {
424        let raw = "title: Test Post\npublished: true";
425        let format = Format::Yaml;
426        let parsed = parse(raw, format).unwrap();
427        assert_eq!(
428            parsed.get("title").unwrap().as_str().unwrap(),
429            "Test Post"
430        );
431        assert!(parsed.get("published").unwrap().as_bool().unwrap());
432    }
433
434    #[test]
435    fn test_parse_toml_frontmatter() {
436        let raw = "title = \"Test Post\"\npublished = true";
437        let format = Format::Toml;
438        let parsed = parse(raw, format).unwrap();
439        assert_eq!(
440            parsed.get("title").unwrap().as_str().unwrap(),
441            "Test Post"
442        );
443        assert!(parsed.get("published").unwrap().as_bool().unwrap());
444    }
445
446    #[test]
447    fn test_invalid_yaml_syntax() {
448        let raw = "{ key: value, ";
449        let format = Format::Yaml;
450        let result = parse(raw, format);
451        assert!(result.is_err());
452    }
453
454    #[test]
455    fn test_parse_invalid_toml_syntax() {
456        let raw = "title = \"Unmatched quote";
457        let format = Format::Toml;
458        let result = parse(raw, format);
459        assert!(result.is_err(), "Should fail for invalid TOML syntax");
460    }
461
462    #[test]
463    fn test_parse_invalid_json_syntax() {
464        let raw = "{\"title\": \"Missing closing brace\"";
465        let format = Format::Json;
466        let result = parse(raw, format);
467        assert!(result.is_err(), "Should fail for invalid JSON syntax");
468    }
469
470    #[test]
471    fn test_parse_with_unknown_format() {
472        let raw = "random text";
473        let format = Format::Unsupported;
474        let result = parse(raw, format);
475        assert!(result.is_err(), "Should fail for unsupported formats");
476    }
477
478    #[test]
479    fn test_parse_valid_yaml() {
480        let raw = "title: Valid Post\npublished: true";
481        let format = Format::Yaml;
482        let frontmatter = parse(raw, format).unwrap();
483        assert_eq!(
484            frontmatter.get("title").unwrap().as_str().unwrap(),
485            "Valid Post"
486        );
487        assert!(frontmatter
488            .get("published")
489            .unwrap()
490            .as_bool()
491            .unwrap());
492    }
493
494    #[test]
495    fn test_parse_malformed_yaml() {
496        let raw = "{ unclosed: mapping, ";
497        let format = Format::Yaml;
498        let result = parse(raw, format);
499        assert!(result.is_err(), "Should fail for malformed YAML");
500    }
501
502    #[test]
503    fn test_parse_json() {
504        let raw = r#"{"title": "Valid Post", "draft": false}"#;
505        let format = Format::Json;
506        let frontmatter = parse(raw, format).unwrap();
507        assert_eq!(
508            frontmatter.get("title").unwrap().as_str().unwrap(),
509            "Valid Post"
510        );
511        assert!(!frontmatter.get("draft").unwrap().as_bool().unwrap());
512    }
513}
514
515#[cfg(test)]
516mod format_tests {
517    use super::*;
518
519    #[test]
520    fn test_to_format_yaml() {
521        let mut frontmatter = Frontmatter::new();
522        let _ = frontmatter.insert(
523            "title".to_string(),
524            Value::String("Test Post".to_string()),
525        );
526        let yaml = to_format(&frontmatter, Format::Yaml).unwrap();
527        assert!(yaml.contains("title: Test Post"));
528    }
529
530    #[test]
531    fn test_format_conversion_roundtrip() {
532        let mut frontmatter = Frontmatter::new();
533        let _ = frontmatter.insert(
534            "key".to_string(),
535            Value::String("value".to_string()),
536        );
537        let yaml = to_format(&frontmatter, Format::Yaml).unwrap();
538        let content = format!("---\n{}\n---\nContent", yaml);
539        let (parsed, _) = extract(&content).unwrap();
540        assert_eq!(
541            parsed.get("key").unwrap().as_str().unwrap(),
542            "value"
543        );
544    }
545
546    #[test]
547    fn test_unsupported_format() {
548        let result =
549            to_format(&Frontmatter::new(), Format::Unsupported);
550        assert!(result.is_err());
551    }
552
553    #[test]
554    fn test_convert_to_yaml() {
555        let mut frontmatter = Frontmatter::new();
556        let _ = frontmatter.insert(
557            "title".to_string(),
558            Value::String("Test Post".into()),
559        );
560        let yaml = to_format(&frontmatter, Format::Yaml).unwrap();
561        assert!(yaml.contains("title: Test Post"));
562    }
563
564    #[test]
565    fn test_roundtrip_conversion() {
566        let content = "---\ntitle: Test Post\n---\nContent";
567        let (parsed, _) = extract(content).unwrap();
568        let yaml = to_format(&parsed, Format::Yaml).unwrap();
569        assert!(yaml.contains("title: Test Post"));
570    }
571
572    #[test]
573    fn test_format_invalid_data() {
574        let frontmatter = Frontmatter::new();
575        let result = to_format(&frontmatter, Format::Unsupported);
576        assert!(result.is_err());
577    }
578}
579
580#[cfg(test)]
581mod integration_tests {
582    use super::*;
583
584    #[test]
585    fn test_end_to_end_extraction_and_parsing() {
586        let content = "---\ntitle: Test Post\n---\nContent here";
587        let (frontmatter, content) = extract(content).unwrap();
588        assert_eq!(
589            frontmatter.get("title").unwrap().as_str().unwrap(),
590            "Test Post"
591        );
592        assert_eq!(content.trim(), "Content here");
593    }
594
595    #[test]
596    fn test_roundtrip_conversion() {
597        let content = "---\ntitle: Test Post\n---\nContent";
598        let (frontmatter, _) = extract(content).unwrap();
599        let yaml = to_format(&frontmatter, Format::Yaml).unwrap();
600        assert!(yaml.contains("title: Test Post"));
601    }
602
603    #[test]
604    fn test_complete_workflow() {
605        let content = "---\ntitle: Integration Test\n---\nBody content";
606        let (frontmatter, body) = extract(content).unwrap();
607        assert_eq!(
608            frontmatter.get("title").unwrap().as_str().unwrap(),
609            "Integration Test"
610        );
611        assert_eq!(body.trim(), "Body content");
612    }
613
614    #[test]
615    fn test_end_to_end_error_handling() {
616        let content = "Invalid frontmatter";
617        let result = extract(content);
618        assert!(result.is_err());
619    }
620}
621
622#[cfg(test)]
623mod edge_case_tests {
624    use super::*;
625
626    #[test]
627    fn test_special_characters_handling() {
628        let cases = vec![
629            (
630                "---\ntitle: \"Special: &chars\"\n---\nContent",
631                "Special: &chars",
632            ),
633            (
634                "---\ntitle: \"Another > test\"\n---\nContent",
635                "Another > test",
636            ),
637        ];
638
639        for (content, expected_title) in cases {
640            let (frontmatter, _) = extract(content).unwrap();
641            assert_eq!(
642                frontmatter.get("title").unwrap().as_str().unwrap(),
643                expected_title
644            );
645        }
646    }
647
648    #[cfg(feature = "ssg")]
649    #[tokio::test]
650    async fn test_async_extraction() {
651        let content = "---\ntitle: Async Test\n---\nContent";
652        let (frontmatter, body) = extract(content).unwrap();
653        assert_eq!(
654            frontmatter.get("title").unwrap().as_str().unwrap(),
655            "Async Test"
656        );
657        assert_eq!(body.trim(), "Content");
658    }
659
660    #[test]
661    fn test_large_frontmatter() {
662        let mut large_content = String::from("---\n");
663        for i in 0..1000 {
664            large_content
665                .push_str(&format!("key_{}: value_{}\n", i, i));
666        }
667        large_content.push_str("---\nContent");
668        let (frontmatter, content) = extract(&large_content).unwrap();
669        assert_eq!(frontmatter.len(), 1000);
670        assert_eq!(content.trim(), "Content");
671    }
672
673    #[test]
674    fn test_special_characters() {
675        let content =
676            "---\ntitle: \"Special & <characters>\"\n---\nContent";
677        let (frontmatter, _) = extract(content).unwrap();
678        assert_eq!(
679            frontmatter.get("title").unwrap().as_str().unwrap(),
680            "Special & <characters>"
681        );
682    }
683}
684
685#[cfg(test)]
686mod validate_input_tests {
687    use super::*;
688
689    #[test]
690    fn test_skip_validation_in_fenced_code_blocks() {
691        let options = ParseOptions::default();
692        let content = r#"
693        ---
694        title: Example
695        ---
696        ```
697        ../example/path
698        ```
699        Valid content here.
700        "#;
701
702        let result = validate_input(content, &options);
703        assert!(
704            result.is_ok(),
705            "Validation should skip fenced code blocks."
706        );
707    }
708
709    #[test]
710    fn test_detect_path_traversal_outside_code_blocks() {
711        let options = ParseOptions::default();
712        let content = r#"
713        ---
714        title: Example
715        ---
716        ../malicious/path
717        "#;
718
719        let result = validate_input(content, &options);
720        assert!(result.is_err(), "Validation should detect path traversal outside fenced code blocks.");
721    }
722
723    #[test]
724    fn test_validate_input_null_bytes() {
725        let options = ParseOptions::default();
726        let malicious_content = "title: Valid\0Post";
727        let result = validate_input(malicious_content, &options);
728        assert!(matches!(
729            result,
730            Err(Error::ValidationError(ref e)) if e == "Content contains null bytes"
731        ));
732    }
733
734    #[test]
735    fn test_validate_input_exceeds_max_size() {
736        let options = ParseOptions::default();
737        let oversized_content = "a".repeat(options.max_size.get() + 1);
738        let result = validate_input(&oversized_content, &options);
739        assert!(matches!(result, Err(Error::ContentTooLarge { .. })));
740    }
741
742    #[test]
743    fn test_validate_input_contains_null_bytes() {
744        let options = ParseOptions::default();
745        let malicious_content = "title: Valid\0Post";
746        let result = validate_input(malicious_content, &options);
747        assert!(matches!(
748            result,
749            Err(Error::ValidationError(ref e)) if e == "Content contains null bytes"
750        ));
751    }
752
753    #[test]
754    fn test_validate_input_path_traversal() {
755        let options = ParseOptions::default();
756        let malicious_content = "../malicious/path";
757        let result = validate_input(malicious_content, &options);
758        assert!(matches!(
759            result,
760            Err(Error::ValidationError(ref e)) if e == "Content contains path traversal patterns"
761        ));
762    }
763}
764
765#[cfg(test)]
766mod parse_options_tests {
767    use super::*;
768
769    #[test]
770    fn test_parse_options_default() {
771        let options = ParseOptions::default();
772        assert_eq!(options.max_size.get(), 1024 * 1024);
773        assert_eq!(options.max_depth.get(), 32);
774        assert!(options.validate);
775    }
776
777    #[test]
778    fn test_parse_options_from_env() {
779        std::env::set_var("MAX_FRONTMATTER_SIZE", "524288");
780        std::env::set_var("MAX_NESTING_DEPTH", "20");
781        std::env::set_var("VALIDATE_STRUCTURE", "false");
782
783        let options = ParseOptions::from_env();
784        assert_eq!(options.max_size.get(), 524288);
785        assert_eq!(options.max_depth.get(), 20);
786        assert!(!options.validate);
787
788        std::env::remove_var("MAX_FRONTMATTER_SIZE");
789        std::env::remove_var("MAX_NESTING_DEPTH");
790        std::env::remove_var("VALIDATE_STRUCTURE");
791    }
792}
793
794#[cfg(test)]
795mod size_limit_tests {
796    //! The size guard on `extract_with_options`.
797    //!
798    //! `max_size` is the one limit that applies before any parsing, so
799    //! it is the only thing standing between a caller and an unbounded
800    //! allocation. A guard nothing tests is a guard that can be removed
801    //! by accident.
802
803    use super::*;
804    use std::num::NonZeroUsize;
805
806    #[test]
807    fn content_larger_than_max_size_is_rejected() {
808        log::set_max_level(log::LevelFilter::Trace);
809        let options = ParseOptions {
810            max_size: NonZeroUsize::new(16).expect("non-zero"),
811            ..ParseOptions::default()
812        };
813        let content = "---\ntitle: a very long title indeed\n---\nbody";
814        assert!(content.len() > 16);
815
816        let err = validate_input(content, &options)
817            .expect_err("oversized content is rejected");
818        assert!(
819            matches!(err, Error::ContentTooLarge { size, max } if size == content.len() && max == 16),
820            "the error names both the size and the limit: {err:?}"
821        );
822    }
823
824    #[test]
825    fn content_exactly_at_max_size_is_accepted() {
826        let content = "---\ntitle: T\n---\nbody";
827        let options = ParseOptions {
828            max_size: NonZeroUsize::new(content.len())
829                .expect("non-zero"),
830            ..ParseOptions::default()
831        };
832        validate_input(content, &options)
833            .expect("content exactly at the limit is accepted");
834    }
835}
836
837#[cfg(test)]
838extern crate alloc;
839
840#[cfg(test)]
841mod miri_and_core_symbiosis_tests {
842    mod core_kernel {
843        use crate::{
844            extractor::{detect_format, extract_raw_frontmatter},
845            parser::{
846                parse, parse_with_options, to_string,
847                validate_frontmatter, ParseOptions as ParserOptions,
848            },
849            Frontmatter, Value,
850        };
851        use alloc::{boxed::Box, string::String, vec::Vec};
852        use core::{fmt::Write as _, str};
853
854        pub(super) fn verify_core_pipeline(
855            input: &[u8],
856            max_depth: usize,
857            max_keys: usize,
858        ) -> crate::Result<Frontmatter> {
859            let content = str::from_utf8(input)
860                .map_err(|_| crate::Error::InvalidFormat)?;
861            let (raw, body) = extract_raw_frontmatter(content)?;
862            let doc_range = content.as_bytes().as_ptr_range();
863            assert!(doc_range.contains(&raw.as_bytes().as_ptr()));
864            assert!(doc_range.contains(&body.as_bytes().as_ptr()));
865            assert!(content.ends_with(body));
866
867            let fmt = detect_format(raw)?;
868            let opts = ParserOptions {
869                max_depth,
870                max_keys,
871                validate: true,
872            };
873            let fm: Frontmatter =
874                parse_with_options(raw, fmt, Some(opts))?;
875            validate_frontmatter(&fm, max_depth, max_keys)?;
876
877            let roundtrip: String = to_string(&fm, fmt)?;
878            let mut sink = String::new();
879            let _ = write!(&mut sink, "{roundtrip}");
880            assert!(!sink.is_empty());
881
882            let reparsed: Frontmatter = parse(&roundtrip, fmt)?;
883            let boxed: Box<Frontmatter> = Box::new(reparsed);
884            let keys: Vec<&String> =
885                boxed.iter().map(|(k, _)| k).collect();
886            assert_eq!(keys.len(), fm.len());
887            for (k, v) in fm.iter() {
888                let got = boxed.get(k);
889                assert_eq!(v.as_str(), got.and_then(Value::as_str));
890                assert_eq!(v.as_bool(), got.and_then(Value::as_bool));
891            }
892            Ok(*boxed)
893        }
894    }
895
896    use super::*;
897    use crate::error::{Context, EngineError};
898    use std::sync::Arc;
899    use std::thread;
900
901    #[test]
902    fn miri_zero_copy_provenance_and_core_alloc_symbiosis() {
903        let cases: [&[u8]; 3] = [
904            "---\ntitle: \"🦀 Rust Ω\"\nflag: true\n---\nBody 東京."
905                .as_bytes(),
906            "+++\ntitle = \"🦀 Rust Ω\"\nflag = true\n+++\nBody 東京."
907                .as_bytes(),
908            "{\n  \"title\": \"🦀 Rust Ω\",\n  \"flag\": true\n}\nBody 東京."
909                .as_bytes(),
910        ];
911
912        for doc in cases {
913            let fm =
914                core_kernel::verify_core_pipeline(doc, 4, 8).unwrap();
915            assert_eq!(
916                fm.get("title").and_then(Value::as_str),
917                Some("🦀 Rust Ω")
918            );
919            assert!(
920                core_kernel::verify_core_pipeline(doc, 4, 1).is_err()
921            );
922        }
923
924        assert!(core_kernel::verify_core_pipeline(&[0xFF, 0xFE], 4, 8)
925            .is_err());
926    }
927
928    #[test]
929    fn miri_concurrent_arc_error_clones_and_depth_boundaries() {
930        let deep_yaml = "---\na:\n  b:\n    c: val\n---\nbody";
931        assert!(core_kernel::verify_core_pipeline(
932            deep_yaml.as_bytes(),
933            3,
934            8
935        )
936        .is_ok());
937        assert!(core_kernel::verify_core_pipeline(
938            deep_yaml.as_bytes(),
939            2,
940            8
941        )
942        .is_err());
943
944        let yaml_err =
945            parse("invalid: [unclosed", Format::Yaml).unwrap_err();
946        let json_err =
947            parse("{\"unclosed\": ", Format::Json).unwrap_err();
948        let shared = Arc::new((yaml_err, json_err));
949
950        let mut handles = Vec::with_capacity(2);
951        for idx in 0..2 {
952            let pair = Arc::clone(&shared);
953            handles.push(thread::spawn(move || {
954                let ctx = Context {
955                    line: Some(idx + 1),
956                    column: Some(4),
957                    snippet: Some("unclosed".to_owned()),
958                };
959                let y = pair.0.clone().with_context(&ctx);
960                let j = pair.1.clone().with_context(&ctx);
961                let engine_err = EngineError::FileSystemError {
962                    source: std::io::Error::other("miri"),
963                    context: y.to_string(),
964                };
965                let converted: Error = engine_err.clone().into();
966                assert!(!j.to_string().is_empty());
967                assert!(!converted.to_string().is_empty());
968            }));
969        }
970        for handle in handles {
971            handle.join().unwrap();
972        }
973    }
974}