• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

qubit-ltd / rust-config / 84ab02af-da5f-49b6-aa4e-20ed6b5f3fd3

09 Apr 2026 04:48PM UTC coverage: 97.136% (+0.01%) from 97.122%
84ab02af-da5f-49b6-aa4e-20ed6b5f3fd3

push

circleci

Haixing-Hu
chore: bump version to 0.7.0

1221 of 1257 relevant lines covered (97.14%)

45.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.14
/src/source/properties_config_source.rs
1
/*******************************************************************************
2
 *
3
 *    Copyright (c) 2025 - 2026.
4
 *    Haixing Hu, Qubit Co. Ltd.
5
 *
6
 *    All rights reserved.
7
 *
8
 ******************************************************************************/
9
//! # Properties File Configuration Source
10
//!
11
//! Loads configuration from Java `.properties` format files.
12
//!
13
//! # Format
14
//!
15
//! The `.properties` format supports:
16
//! - `key=value` assignments
17
//! - `key: value` assignments (colon separator)
18
//! - `# comment` and `! comment` lines
19
//! - Blank lines (ignored)
20
//! - Line continuation with `\` at end of line
21
//! - Unicode escape sequences (`\uXXXX`)
22
//!
23
//! # Author
24
//!
25
//! Haixing Hu
26

27
use std::path::{Path, PathBuf};
28

29
use crate::{Config, ConfigError, ConfigResult};
30

31
use super::ConfigSource;
32

33
/// Configuration source that loads from Java `.properties` format files
34
///
35
/// # Examples
36
///
37
/// ```rust,ignore
38
/// use qubit_config::source::{PropertiesConfigSource, ConfigSource};
39
/// use qubit_config::Config;
40
///
41
/// let source = PropertiesConfigSource::from_file("config.properties");
42
/// let mut config = Config::new();
43
/// source.load(&mut config).unwrap();
44
/// ```
45
///
46
/// # Author
47
///
48
/// Haixing Hu
49
#[derive(Debug, Clone)]
50
pub struct PropertiesConfigSource {
51
    path: PathBuf,
52
}
53

54
impl PropertiesConfigSource {
55
    /// Creates a new `PropertiesConfigSource` from a file path
56
    ///
57
    /// # Parameters
58
    ///
59
    /// * `path` - Path to the `.properties` file
60
    pub fn from_file<P: AsRef<Path>>(path: P) -> Self {
5✔
61
        Self {
5✔
62
            path: path.as_ref().to_path_buf(),
5✔
63
        }
5✔
64
    }
5✔
65

66
    /// Parses a `.properties` format string into key-value pairs
67
    ///
68
    /// # Parameters
69
    ///
70
    /// * `content` - The content of the `.properties` file
71
    ///
72
    /// # Returns
73
    ///
74
    /// Returns a vector of `(key, value)` pairs
75
    pub fn parse_content(content: &str) -> Vec<(String, String)> {
25✔
76
        let mut result = Vec::new();
25✔
77
        let mut lines = content.lines().peekable();
25✔
78

79
        while let Some(line) = lines.next() {
79✔
80
            let trimmed = line.trim();
54✔
81

82
            // Skip blank lines and comments
83
            if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with('!') {
54✔
84
                continue;
15✔
85
            }
39✔
86

87
            // Handle line continuation
88
            let mut full_line = trimmed.to_string();
39✔
89
            while full_line.ends_with('\\') {
44✔
90
                full_line.pop(); // remove trailing backslash
6✔
91
                if let Some(next) = lines.next() {
6✔
92
                    full_line.push_str(next.trim());
5✔
93
                } else {
5✔
94
                    break;
1✔
95
                }
96
            }
97

98
            // Parse key=value or key: value
99
            if let Some((key, value)) = parse_key_value(&full_line) {
39✔
100
                let key = unescape_unicode(key.trim());
39✔
101
                let value = unescape_unicode(value.trim());
39✔
102
                result.push((key, value));
39✔
103
            }
39✔
104
        }
105

106
        result
25✔
107
    }
25✔
108
}
109

110
/// Parses a single `key=value` or `key: value` line
111
fn parse_key_value(line: &str) -> Option<(&str, &str)> {
39✔
112
    // Find the first '=' or ':' that is not preceded by '\'
113
    let chars = line.char_indices();
39✔
114
    for (i, ch) in chars {
261✔
115
        if ch == '=' || ch == ':' {
261✔
116
            // Separator is escaped only if there is an odd number of trailing backslashes.
117
            if !is_escaped_separator(line, i) {
39✔
118
                return Some((&line[..i], &line[i + ch.len_utf8()..]));
38✔
119
            }
1✔
120
        }
222✔
121
    }
122
    // No separator found - treat the whole line as a key with empty value
123
    if !line.is_empty() {
1✔
124
        Some((line, ""))
1✔
125
    } else {
126
        None
×
127
    }
128
}
39✔
129

130
/// Returns true if the separator at `sep_pos` is escaped by a preceding odd
131
/// number of backslashes.
132
///
133
/// # Parameters
134
///
135
/// * `line` - Full properties line being parsed.
136
/// * `sep_pos` - Byte index of `=` or `:` in `line`.
137
///
138
/// # Returns
139
///
140
/// `true` when the separator is escaped and must not split the key/value.
141
fn is_escaped_separator(line: &str, sep_pos: usize) -> bool {
39✔
142
    let slash_count = line.as_bytes()[..sep_pos]
39✔
143
        .iter()
39✔
144
        .rev()
39✔
145
        .take_while(|&&b| b == b'\\')
42✔
146
        .count();
39✔
147
    slash_count % 2 == 1
39✔
148
}
39✔
149

150
/// Processes Unicode escape sequences (`\uXXXX`) in a string
151
fn unescape_unicode(s: &str) -> String {
78✔
152
    let mut result = String::with_capacity(s.len());
78✔
153
    let mut chars = s.chars().peekable();
78✔
154

155
    while let Some(ch) = chars.next() {
566✔
156
        if ch == '\\' {
488✔
157
            match chars.peek() {
15✔
158
                Some('u') => {
159
                    chars.next(); // consume 'u'
7✔
160
                    let hex: String = chars.by_ref().take(4).collect();
7✔
161
                    if hex.len() == 4 {
7✔
162
                        if let Ok(code) = u32::from_str_radix(&hex, 16) {
6✔
163
                            if let Some(unicode_char) = char::from_u32(code) {
6✔
164
                                result.push(unicode_char);
6✔
165
                                continue;
6✔
166
                            }
×
167
                        }
×
168
                    }
1✔
169
                    // If parsing fails, keep original
170
                    result.push('\\');
1✔
171
                    result.push('u');
1✔
172
                    result.push_str(&hex);
1✔
173
                }
174
                Some('n') => {
1✔
175
                    chars.next();
1✔
176
                    result.push('\n');
1✔
177
                }
1✔
178
                Some('t') => {
1✔
179
                    chars.next();
1✔
180
                    result.push('\t');
1✔
181
                }
1✔
182
                Some('r') => {
1✔
183
                    chars.next();
1✔
184
                    result.push('\r');
1✔
185
                }
1✔
186
                Some('\\') => {
3✔
187
                    chars.next();
3✔
188
                    result.push('\\');
3✔
189
                }
3✔
190
                _ => {
2✔
191
                    result.push(ch);
2✔
192
                }
2✔
193
            }
194
        } else {
473✔
195
            result.push(ch);
473✔
196
        }
473✔
197
    }
198

199
    result
78✔
200
}
78✔
201

202
impl ConfigSource for PropertiesConfigSource {
203
    fn load(&self, config: &mut Config) -> ConfigResult<()> {
4✔
204
        let content = std::fs::read_to_string(&self.path).map_err(|e| {
4✔
205
            ConfigError::IoError(std::io::Error::new(
1✔
206
                e.kind(),
1✔
207
                format!(
1✔
208
                    "Failed to read properties file '{}': {}",
1✔
209
                    self.path.display(),
1✔
210
                    e
1✔
211
                ),
1✔
212
            ))
1✔
213
        })?;
1✔
214

215
        for (key, value) in Self::parse_content(&content) {
17✔
216
            config.set(&key, value)?;
17✔
217
        }
218

219
        Ok(())
3✔
220
    }
4✔
221
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc