Skip to main content

use_php_ini/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7macro_rules! ini_text_newtype {
8    ($name:ident) => {
9        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10        pub struct $name(String);
11
12        impl $name {
13            pub fn new(input: &str) -> Result<Self, PhpIniError> {
14                let trimmed = input.trim();
15                if trimmed.is_empty() {
16                    Err(PhpIniError::Empty)
17                } else {
18                    Ok(Self(trimmed.to_string()))
19                }
20            }
21
22            pub fn as_str(&self) -> &str {
23                &self.0
24            }
25        }
26
27        impl fmt::Display for $name {
28            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29                formatter.write_str(self.as_str())
30            }
31        }
32
33        impl FromStr for $name {
34            type Err = PhpIniError;
35
36            fn from_str(input: &str) -> Result<Self, Self::Err> {
37                Self::new(input)
38            }
39        }
40    };
41}
42
43ini_text_newtype!(PhpIniSectionName);
44ini_text_newtype!(PhpIniDirectiveName);
45
46/// PHP INI environment label metadata.
47#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
48pub enum PhpIniEnvironment {
49    Production,
50    Development,
51    Testing,
52    Cli,
53    Unknown,
54}
55
56impl PhpIniEnvironment {
57    pub const fn as_str(self) -> &'static str {
58        match self {
59            Self::Production => "production",
60            Self::Development => "development",
61            Self::Testing => "testing",
62            Self::Cli => "cli",
63            Self::Unknown => "unknown",
64        }
65    }
66}
67
68impl fmt::Display for PhpIniEnvironment {
69    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70        formatter.write_str(self.as_str())
71    }
72}
73
74/// Scalar PHP INI value metadata.
75#[derive(Clone, Debug, PartialEq)]
76pub enum PhpIniValue {
77    String(String),
78    Integer(i64),
79    Float(f64),
80    Boolean(bool),
81    Null,
82}
83
84impl fmt::Display for PhpIniValue {
85    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self {
87            Self::String(value) => formatter.write_str(value),
88            Self::Integer(value) => write!(formatter, "{value}"),
89            Self::Float(value) => write!(formatter, "{value}"),
90            Self::Boolean(value) => formatter.write_str(if *value { "true" } else { "false" }),
91            Self::Null => formatter.write_str("null"),
92        }
93    }
94}
95
96impl FromStr for PhpIniValue {
97    type Err = PhpIniError;
98
99    fn from_str(input: &str) -> Result<Self, Self::Err> {
100        let trimmed = input.trim().trim_matches('"');
101        if trimmed.is_empty() {
102            return Ok(Self::String(String::new()));
103        }
104        match trimmed.to_ascii_lowercase().as_str() {
105            "true" | "on" | "yes" => Ok(Self::Boolean(true)),
106            "false" | "off" | "no" => Ok(Self::Boolean(false)),
107            "null" | "none" => Ok(Self::Null),
108            _ => trimmed
109                .parse::<i64>()
110                .map(Self::Integer)
111                .or_else(|_| trimmed.parse::<f64>().map(Self::Float))
112                .or_else(|_| Ok(Self::String(trimmed.to_string()))),
113        }
114    }
115}
116
117/// PHP INI directive metadata.
118#[derive(Clone, Debug, PartialEq)]
119pub struct PhpIniDirective {
120    section: Option<PhpIniSectionName>,
121    name: PhpIniDirectiveName,
122    value: PhpIniValue,
123}
124
125impl PhpIniDirective {
126    pub const fn new(name: PhpIniDirectiveName, value: PhpIniValue) -> Self {
127        Self {
128            section: None,
129            name,
130            value,
131        }
132    }
133
134    pub fn with_section(mut self, section: PhpIniSectionName) -> Self {
135        self.section = Some(section);
136        self
137    }
138
139    pub const fn section(&self) -> Option<&PhpIniSectionName> {
140        self.section.as_ref()
141    }
142
143    pub const fn name(&self) -> &PhpIniDirectiveName {
144        &self.name
145    }
146
147    pub const fn value(&self) -> &PhpIniValue {
148        &self.value
149    }
150}
151
152impl fmt::Display for PhpIniDirective {
153    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
154        write!(formatter, "{} = {}", self.name, self.value)
155    }
156}
157
158/// Error returned when PHP INI metadata is invalid.
159#[derive(Clone, Copy, Debug, Eq, PartialEq)]
160pub enum PhpIniError {
161    Empty,
162    MissingEquals,
163}
164
165impl fmt::Display for PhpIniError {
166    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
167        match self {
168            Self::Empty => formatter.write_str("PHP INI metadata cannot be empty"),
169            Self::MissingEquals => formatter.write_str("PHP INI directive line must contain '='"),
170        }
171    }
172}
173
174impl Error for PhpIniError {}
175
176pub fn parse_ini_directive_line(input: &str) -> Result<PhpIniDirective, PhpIniError> {
177    let trimmed = input.trim();
178    if trimmed.is_empty() {
179        return Err(PhpIniError::Empty);
180    }
181    let Some((name, value)) = trimmed.split_once('=') else {
182        return Err(PhpIniError::MissingEquals);
183    };
184    Ok(PhpIniDirective::new(
185        PhpIniDirectiveName::new(name)?,
186        value.parse()?,
187    ))
188}
189
190#[cfg(test)]
191mod tests {
192    use super::{
193        PhpIniDirective, PhpIniDirectiveName, PhpIniEnvironment, PhpIniError, PhpIniValue,
194        parse_ini_directive_line,
195    };
196
197    #[test]
198    fn formats_and_parses_directives() -> Result<(), PhpIniError> {
199        let directive = PhpIniDirective::new(
200            PhpIniDirectiveName::new("memory_limit")?,
201            PhpIniValue::String("128M".to_string()),
202        );
203        let parsed = parse_ini_directive_line("display_errors = On")?;
204
205        assert_eq!(directive.to_string(), "memory_limit = 128M");
206        assert_eq!(parsed.value(), &PhpIniValue::Boolean(true));
207        assert_eq!(PhpIniEnvironment::Development.to_string(), "development");
208        Ok(())
209    }
210}