├── Cargo.toml ├── LICENSE ├── README.md ├── git-hooks └── pre-commit ├── init-git-hooks └── src └── lib.rs /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "java-properties" 3 | version = "2.0.0" 4 | authors = ["Adam Crume "] 5 | description = "A library for reading and writing Java properties files in Rust." 6 | keywords = ["java", "properties"] 7 | readme = "README.md" 8 | repository = "https://github.com/adamcrume/java-properties" 9 | license = "MIT" 10 | documentation = "https://adamcrume.github.io/java-properties" 11 | edition = "2018" 12 | 13 | [dependencies] 14 | encoding_rs = "0.8.32" 15 | lazy_static = "1.4.0" 16 | regex = "1.5.5" 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Adam Crume 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java properties for Rust 2 | 3 | This is a library for reading and writing Java properties files in Rust. 4 | The specification is taken from the [Properties](https://docs.oracle.com/javase/7/docs/api/java/util/Properties.html) documentation. 5 | Where the documentation is ambiguous or incomplete, behavior is based on the behavior of `java.util.Properties`. 6 | 7 | ## Example 8 | ```rust 9 | use std::collections::HashMap; 10 | use std::env::temp_dir; 11 | use std::fs::File; 12 | use std::io::BufReader; 13 | use std::io::BufWriter; 14 | use std::io::prelude::*; 15 | 16 | let mut file_name = temp_dir(); 17 | file_name.push("java-properties-test.properties"); 18 | 19 | // Writing 20 | let mut map1 = HashMap::new(); 21 | map1.insert("a".to_string(), "b".to_string()); 22 | let mut f = File::create(&file_name)?; 23 | write(BufWriter::new(f), &map1)?; 24 | 25 | // Reading 26 | let mut f = File::open(&file_name)?; 27 | let map2 = read(BufReader::new(f))?; 28 | assert_eq!(src_map1, dst_map1); 29 | ``` 30 | -------------------------------------------------------------------------------- /git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if git-rev-parse --verify HEAD >/dev/null 2>&1 6 | then 7 | against=HEAD 8 | else 9 | # Initial commit: diff against an empty tree object 10 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 11 | fi 12 | 13 | function grep-check { 14 | test="$1" 15 | ignore="$2" 16 | msg="$3" 17 | if (git diff --cached | egrep -i "$test" | grep -v IGNORE:"$ignore"); then 18 | echo "Error: $msg (This message can be suppressed by adding the string IGNORE:$ignore to the same line.)" 19 | exit 1 20 | fi 21 | } 22 | 23 | function grep-check-case-sensitive { 24 | test="$1" 25 | ignore="$2" 26 | msg="$3" 27 | if (git diff --cached | egrep "$test" | grep -v IGNORE:"$ignore"); then 28 | echo "Error: $msg (This message can be suppressed by adding the string IGNORE:$ignore to the same line.)" 29 | exit 1 30 | fi 31 | } 32 | 33 | grep-check-case-sensitive \ 34 | NOCOMMIT `#IGNORE:NOCOMMIT` \ 35 | NOCOMMIT `#IGNORE:NOCOMMIT` \ 36 | "Found a line tagged with NOCOMMIT." # IGNORE:NOCOMMIT 37 | 38 | cargo fmt --all -- --check 39 | cargo build --all-targets 40 | cargo test --release 41 | 42 | # Check for trailing whitespace 43 | exec git diff-index --check --cached $against -- 44 | -------------------------------------------------------------------------------- /init-git-hooks: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mydir="$(dirname "$(readlink -f "$0")")" 4 | ln -s ../../git-hooks/pre-commit "$mydir"/.git/hooks/pre-commit 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // -*- indent-tabs-mode:nil; tab-width:4; -*- 2 | //! Utilities for reading and writing Java properties files 3 | //! 4 | //! The specification is taken from . 5 | //! Where the documentation is ambiguous or incomplete, behavior is based on the behavior of java.util.Properties. 6 | //! 7 | //! # Examples 8 | //! 9 | //! ``` 10 | //! use java_properties::PropertiesIter; 11 | //! use java_properties::PropertiesWriter; 12 | //! use java_properties::read; 13 | //! use java_properties::write; 14 | //! use std::collections::HashMap; 15 | //! use std::env::temp_dir; 16 | //! use std::fs::File; 17 | //! use std::io::BufReader; 18 | //! use std::io::BufWriter; 19 | //! use std::io::prelude::*; 20 | //! 21 | //! # fn main() -> std::result::Result<(), java_properties::PropertiesError> { 22 | //! let mut file_name = temp_dir(); 23 | //! file_name.push("java-properties-test.properties"); 24 | //! 25 | //! // Writing simple 26 | //! let mut src_map1 = HashMap::new(); 27 | //! src_map1.insert("a".to_string(), "b".to_string()); 28 | //! let mut f = File::create(&file_name)?; 29 | //! write(BufWriter::new(f), &src_map1)?; 30 | //! 31 | //! // Writing advanced 32 | //! let mut src_map2 = HashMap::new(); 33 | //! src_map2.insert("a".to_string(), "b".to_string()); 34 | //! let mut f = File::create(&file_name)?; 35 | //! let mut writer = PropertiesWriter::new(BufWriter::new(f)); 36 | //! for (k, v) in &src_map2 { 37 | //! writer.write(&k, &v)?; 38 | //! } 39 | //! writer.finish(); 40 | //! 41 | //! // Reading simple 42 | //! let mut f2 = File::open(&file_name)?; 43 | //! let dst_map1 = read(BufReader::new(f2))?; 44 | //! assert_eq!(src_map1, dst_map1); 45 | //! 46 | //! // Reading advanced 47 | //! let mut f = File::open(&file_name)?; 48 | //! let mut dst_map2 = HashMap::new(); 49 | //! PropertiesIter::new(BufReader::new(f)).read_into(|k, v| { 50 | //! dst_map2.insert(k, v); 51 | //! })?; 52 | //! assert_eq!(src_map2, dst_map2); 53 | //! # Ok(()) 54 | //! # } 55 | //! ``` 56 | 57 | #![deny(rustdoc::broken_intra_doc_links)] 58 | #![deny(rustdoc::invalid_codeblock_attributes)] 59 | #![deny(rustdoc::invalid_rust_codeblocks)] 60 | #![deny(rustdoc::private_intra_doc_links)] 61 | #![deny(unused_must_use)] 62 | #![doc(test(attr(allow(unused_extern_crates))))] 63 | #![doc(test(attr(deny(unused_must_use))))] 64 | #![doc(test(attr(warn(unused))))] 65 | #![warn(missing_docs)] 66 | 67 | use encoding_rs::CoderResult; 68 | use encoding_rs::Decoder; 69 | use encoding_rs::Encoder; 70 | use encoding_rs::EncoderResult; 71 | use encoding_rs::Encoding; 72 | use encoding_rs::WINDOWS_1252; 73 | use lazy_static::lazy_static; 74 | use regex::Regex; 75 | use std::collections::HashMap; 76 | use std::collections::VecDeque; 77 | use std::convert::From; 78 | use std::error::Error; 79 | use std::fmt; 80 | use std::fmt::Display; 81 | use std::fmt::Formatter; 82 | use std::io; 83 | use std::io::Read; 84 | use std::io::Write; 85 | use std::iter::Peekable; 86 | use std::ops::Deref; 87 | 88 | ///////////////////// 89 | 90 | /// The error type for reading and writing properties files. 91 | #[derive(Debug)] 92 | pub struct PropertiesError { 93 | description: String, 94 | cause: Option>, 95 | line_number: Option, 96 | } 97 | 98 | impl PropertiesError { 99 | fn new>( 100 | description: S, 101 | cause: Option>, 102 | line_number: Option, 103 | ) -> Self { 104 | PropertiesError { 105 | description: description.into(), 106 | cause, 107 | line_number, 108 | } 109 | } 110 | 111 | /// Returns the 1-based line number associated with the error, if available. 112 | pub fn line_number(&self) -> Option { 113 | self.line_number 114 | } 115 | } 116 | 117 | impl Error for PropertiesError { 118 | fn description(&self) -> &str { 119 | &self.description 120 | } 121 | 122 | // The "readable" version is less readable, especially since it requires manual type assertions. 123 | #[allow(clippy::manual_map)] 124 | fn source(&self) -> Option<&(dyn Error + 'static)> { 125 | match self.cause { 126 | Some(ref c) => Some(c.deref()), 127 | None => None, 128 | } 129 | } 130 | } 131 | 132 | impl From for PropertiesError { 133 | fn from(e: io::Error) -> Self { 134 | PropertiesError::new("I/O error", Some(Box::new(e)), None) 135 | } 136 | } 137 | 138 | impl Display for PropertiesError { 139 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 140 | write!(f, "{}", &self.description)?; 141 | match self.line_number { 142 | Some(n) => write!(f, " (line_number = {})", n), 143 | None => write!(f, " (line_number = unknown)"), 144 | } 145 | } 146 | } 147 | 148 | ///////////////////// 149 | 150 | struct DecodeIter { 151 | decoder: Decoder, 152 | reader: R, 153 | input_buffer: Vec, 154 | output_buffer: String, 155 | chars: VecDeque, 156 | } 157 | 158 | impl DecodeIter { 159 | fn new(reader: R, encoding: &'static Encoding) -> Self { 160 | Self { 161 | decoder: encoding.new_decoder(), 162 | reader, 163 | // must have a non-zero capacity since we double it as needed 164 | input_buffer: Vec::with_capacity(64), 165 | // must have a non-zero capacity since we double it as needed 166 | output_buffer: String::with_capacity(64), 167 | chars: VecDeque::new(), 168 | } 169 | } 170 | } 171 | 172 | impl Iterator for DecodeIter { 173 | type Item = Result; 174 | 175 | fn next(&mut self) -> Option { 176 | loop { 177 | if let Some(c) = self.chars.pop_front() { 178 | return Some(Ok(c)); 179 | } 180 | let reader_eof = if self.input_buffer.is_empty() { 181 | self.input_buffer.resize(self.input_buffer.capacity(), 0); 182 | let bytes_read = match self.reader.read(&mut self.input_buffer) { 183 | Ok(x) => x, 184 | Err(e) => { 185 | self.input_buffer.clear(); 186 | return Some(Err(e)); 187 | } 188 | }; 189 | self.input_buffer.truncate(bytes_read); 190 | bytes_read == 0 191 | } else { 192 | false 193 | }; 194 | let (result, bytes_read, _) = self.decoder.decode_to_string( 195 | &self.input_buffer, 196 | &mut self.output_buffer, 197 | reader_eof, 198 | ); 199 | self.input_buffer.drain(..bytes_read); 200 | match result { 201 | CoderResult::InputEmpty => (), 202 | CoderResult::OutputFull => { 203 | self.output_buffer.reserve(self.output_buffer.capacity()); 204 | } 205 | }; 206 | self.chars.extend(self.output_buffer.drain(..)); 207 | if self.chars.is_empty() && reader_eof { 208 | return None; 209 | } 210 | } 211 | } 212 | } 213 | 214 | ///////////////////// 215 | 216 | #[derive(PartialEq, Eq, Debug)] 217 | struct NaturalLine(usize, String); 218 | 219 | // We can't use BufRead.lines() because it doesn't use the proper line endings 220 | struct NaturalLines { 221 | chars: Peekable>, 222 | eof: bool, 223 | line_count: usize, 224 | } 225 | 226 | impl NaturalLines { 227 | fn new(reader: R, encoding: &'static Encoding) -> Self { 228 | NaturalLines { 229 | chars: DecodeIter::new(reader, encoding).peekable(), 230 | eof: false, 231 | line_count: 0, 232 | } 233 | } 234 | } 235 | 236 | const LF: char = '\n'; 237 | const CR: char = '\r'; 238 | 239 | impl Iterator for NaturalLines { 240 | type Item = Result; 241 | 242 | fn next(&mut self) -> Option { 243 | if self.eof { 244 | return None; 245 | } 246 | let mut buf = String::new(); 247 | loop { 248 | match self.chars.next() { 249 | Some(Ok(CR)) => { 250 | if let Some(&Ok(LF)) = self.chars.peek() { 251 | self.chars.next(); 252 | } 253 | self.line_count += 1; 254 | return Some(Ok(NaturalLine(self.line_count, buf))); 255 | } 256 | Some(Ok(LF)) => { 257 | self.line_count += 1; 258 | return Some(Ok(NaturalLine(self.line_count, buf))); 259 | } 260 | Some(Ok(c)) => buf.push(c), 261 | Some(Err(e)) => { 262 | return Some(Err(PropertiesError::new( 263 | "I/O error", 264 | Some(Box::new(e)), 265 | Some(self.line_count + 1), 266 | ))) 267 | } 268 | None => { 269 | self.eof = true; 270 | self.line_count += 1; 271 | return Some(Ok(NaturalLine(self.line_count, buf))); 272 | } 273 | } 274 | } 275 | } 276 | } 277 | 278 | ///////////////////// 279 | 280 | #[derive(PartialEq, Eq, Debug)] 281 | struct LogicalLine(usize, String); 282 | 283 | struct LogicalLines>> { 284 | physical_lines: I, 285 | eof: bool, 286 | } 287 | 288 | impl>> LogicalLines { 289 | fn new(physical_lines: I) -> Self { 290 | LogicalLines { 291 | physical_lines, 292 | eof: false, 293 | } 294 | } 295 | } 296 | 297 | fn count_ending_backslashes(s: &str) -> usize { 298 | let mut n = 0; 299 | for c in s.chars() { 300 | if c == '\\' { 301 | n += 1; 302 | } else { 303 | n = 0; 304 | } 305 | } 306 | n 307 | } 308 | 309 | impl>> Iterator for LogicalLines { 310 | type Item = Result; 311 | 312 | fn next(&mut self) -> Option { 313 | if self.eof { 314 | return None; 315 | } 316 | let mut buf = String::new(); 317 | let mut first = true; 318 | let mut line_number = 0; 319 | loop { 320 | match self.physical_lines.next() { 321 | Some(Err(e)) => return Some(Err(e)), 322 | Some(Ok(NaturalLine(line_no, line))) => { 323 | if first { 324 | line_number = line_no; 325 | } 326 | buf.push_str(if first { &line } else { line.trim_start() }); 327 | lazy_static! { 328 | static ref COMMENT_RE: Regex = Regex::new("^[ \t\r\n\x0c]*[#!]").unwrap(); 329 | } 330 | if first && COMMENT_RE.is_match(&line) { 331 | // This format is terrible. We can't throw out comment lines before joining natural lines, because "a\\\n#b" should be joined into "a#b". 332 | // On the other hand, we can't join natural lines before processing comments, because "#a\\\nb" should stay as two lines, "#a\\" and "b". 333 | // Processing line joins and comments are inextricably linked. 334 | assert!(line_number != 0); 335 | return Some(Ok(LogicalLine(line_number, buf))); 336 | } 337 | if count_ending_backslashes(&line) % 2 == 1 { 338 | buf.pop(); 339 | } else { 340 | assert!(line_number != 0); 341 | return Some(Ok(LogicalLine(line_number, buf))); 342 | } 343 | } 344 | None => { 345 | self.eof = true; 346 | return None; 347 | } 348 | } 349 | first = false; 350 | } 351 | } 352 | } 353 | 354 | ///////////////////// 355 | 356 | #[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] 357 | enum ParsedLine<'a> { 358 | Comment(&'a str), 359 | KVPair(&'a str, &'a str), 360 | } 361 | 362 | /// A line read from a properties file. 363 | #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Hash)] 364 | pub struct Line { 365 | line_number: usize, 366 | data: LineContent, 367 | } 368 | 369 | impl Line { 370 | /// Returns the 1-based line number. 371 | pub fn line_number(&self) -> usize { 372 | self.line_number 373 | } 374 | 375 | /// Returns the content of the line. 376 | pub fn content(&self) -> &LineContent { 377 | &self.data 378 | } 379 | 380 | /// Returns the content of the line, consuming it in the process. 381 | pub fn consume_content(self) -> LineContent { 382 | self.data 383 | } 384 | 385 | fn mk_pair(line_number: usize, key: String, value: String) -> Line { 386 | Line { 387 | line_number, 388 | data: LineContent::KVPair(key, value), 389 | } 390 | } 391 | 392 | fn mk_comment(line_number: usize, text: String) -> Line { 393 | Line { 394 | line_number, 395 | data: LineContent::Comment(text), 396 | } 397 | } 398 | } 399 | 400 | impl Display for Line { 401 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 402 | write!( 403 | f, 404 | "Line {{line_number: {}, content: {}}}", 405 | self.line_number, self.data 406 | ) 407 | } 408 | } 409 | 410 | /// Parsed content of the line. 411 | #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Hash)] 412 | pub enum LineContent { 413 | /// Content of a comment line. 414 | Comment(String), 415 | 416 | /// Content of a key/value line. 417 | KVPair(String, String), 418 | } 419 | 420 | impl Display for LineContent { 421 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 422 | match *self { 423 | LineContent::Comment(ref s) => write!(f, "Comment({:?})", s), 424 | LineContent::KVPair(ref k, ref v) => write!(f, "KVPair({:?}, {:?})", k, v), 425 | } 426 | } 427 | } 428 | 429 | impl From for LineContent { 430 | fn from(line: Line) -> LineContent { 431 | line.data 432 | } 433 | } 434 | 435 | ///////////////////// 436 | 437 | fn unescape(s: &str, line_number: usize) -> Result { 438 | let mut buf = String::new(); 439 | let mut iter = s.chars(); 440 | loop { 441 | match iter.next() { 442 | None => break, 443 | Some(c) => { 444 | if c == '\\' { 445 | match iter.next() { 446 | Some(c) => { 447 | match c { 448 | // \b is specifically blacklisted by the documentation. Why? Who knows. 449 | 't' => buf.push('\t'), 450 | 'n' => buf.push('\n'), 451 | 'f' => buf.push('\x0c'), 452 | 'r' => buf.push('\r'), 453 | 'u' => { 454 | let mut tmp = String::new(); 455 | for _ in 0..4 { 456 | match iter.next() { 457 | Some(c) => tmp.push(c), 458 | None => return Err(PropertiesError::new( 459 | "Malformed \\uxxxx encoding: not enough digits.", 460 | None, 461 | Some(line_number), 462 | )), 463 | } 464 | } 465 | let val = match u16::from_str_radix(&tmp, 16) { 466 | Ok(x) => x, 467 | Err(e) => { 468 | return Err(PropertiesError::new( 469 | "Malformed \\uxxxx encoding: not hex.", 470 | Some(Box::new(e)), 471 | Some(line_number), 472 | )) 473 | } 474 | }; 475 | match std::char::from_u32(val as u32) { 476 | Some(c) => buf.push(c), 477 | None => { 478 | return Err(PropertiesError::new( 479 | "Malformed \\uxxxx encoding: invalid character.", 480 | None, 481 | Some(line_number), 482 | )) 483 | } 484 | } 485 | } 486 | _ => buf.push(c), 487 | } 488 | } 489 | None => { 490 | // The Java implementation replaces a dangling backslash with a NUL byte (\0). 491 | // Is this "correct"? Probably not. 492 | // It's never documented, so assume it's undefined behavior. 493 | // Let's do what Java does, though. 494 | buf.push('\x00'); 495 | break; 496 | } 497 | } 498 | } else { 499 | buf.push(c); 500 | } 501 | } 502 | } 503 | } 504 | Ok(buf) 505 | } 506 | 507 | lazy_static! { 508 | // Note that we have to use \x20 to match a space and \x23 to match a pound character since we're ignoring whitespace and comments 509 | static ref LINE_RE: Regex = Regex::new(r"(?x) # allow whitespace and comments 510 | ^ 511 | [\x20\t\r\n\x0c]* # ignorable whitespace 512 | (?: 513 | [\x23!] # start of comment (# or !) 514 | [\x20\t\r\n\x0c]* # ignorable whitespace 515 | (.*?) # comment text 516 | [\x20\t\r\n\x0c]* # ignorable whitespace 517 | | 518 | ( 519 | (?:[^\\:=\x20\t\r\n\x0c]|\\.)* # key 520 | (?:\\$)? # end of line backslash, can't show up in real input because it's caught by LogicalLines 521 | ) 522 | (?: 523 | (?: 524 | [\x20\t\r\n\x0c]*[:=][\x20\t\r\n\x0c]* # try matching an actual separator (: or =) 525 | | 526 | [\x20\t\r\n\x0c]+ # try matching whitespace only 527 | ) 528 | ( 529 | (?:[^\\]|\\.)*? # value 530 | (?:\\$)? # end of line backslash, can't show up in real input because it's caught by LogicalLines 531 | ) 532 | )? 533 | ) 534 | $ 535 | ").unwrap(); 536 | } 537 | 538 | fn parse_line(line: &str) -> Option { 539 | if let Some(c) = LINE_RE.captures(line) { 540 | if let Some(comment_match) = c.get(1) { 541 | Some(ParsedLine::Comment(comment_match.as_str())) 542 | } else if let Some(key_match) = c.get(2) { 543 | let key = key_match.as_str(); 544 | if let Some(value_match) = c.get(3) { 545 | Some(ParsedLine::KVPair(key, value_match.as_str())) 546 | } else if !key.is_empty() { 547 | Some(ParsedLine::KVPair(key, "")) 548 | } else { 549 | None 550 | } 551 | } else { 552 | panic!("Failed to get any groups out of the regular expression.") 553 | } 554 | } else { 555 | // This should never happen. The pattern should match all strings. 556 | panic!("Failed to match on {:?}", line); 557 | } 558 | } 559 | 560 | /// Parses a properties file and iterates over its contents. 561 | /// 562 | /// For basic usage, see the crate-level documentation. 563 | /// Note that once `next` returns an error, the result of further calls is undefined. 564 | pub struct PropertiesIter { 565 | lines: LogicalLines>, 566 | } 567 | 568 | impl PropertiesIter { 569 | /// Parses properties from the given `Read` stream. 570 | pub fn new(input: R) -> Self { 571 | Self::new_with_encoding(input, WINDOWS_1252) 572 | } 573 | 574 | /// Parses properties from the given `Read` stream in the given encoding. 575 | /// Note that the Java properties specification specifies ISO-8859-1 encoding 576 | /// (a.k.a. windows-1252) for properties files; in most cases, `new` should be 577 | /// called instead. 578 | pub fn new_with_encoding(input: R, encoding: &'static Encoding) -> Self { 579 | PropertiesIter { 580 | lines: LogicalLines::new(NaturalLines::new(input, encoding)), 581 | } 582 | } 583 | 584 | /// Calls `f` for each key/value pair. 585 | /// 586 | /// Line numbers and comments are ignored. 587 | /// On the first error, the error is returned. 588 | /// Note that `f` may have already been called at this point. 589 | pub fn read_into(&mut self, mut f: F) -> Result<(), PropertiesError> { 590 | for line in self { 591 | if let LineContent::KVPair(key, value) = line?.data { 592 | f(key, value); 593 | } 594 | } 595 | Ok(()) 596 | } 597 | 598 | fn parsed_line_to_line( 599 | &self, 600 | parsed_line: ParsedLine<'_>, 601 | line_number: usize, 602 | ) -> Result { 603 | Ok(match parsed_line { 604 | ParsedLine::Comment(c) => { 605 | let comment = unescape(c, line_number)?; 606 | Line::mk_comment(line_number, comment) 607 | } 608 | ParsedLine::KVPair(k, v) => { 609 | let key = unescape(k, line_number)?; 610 | let value = unescape(v, line_number)?; 611 | Line::mk_pair(line_number, key, value) 612 | } 613 | }) 614 | } 615 | } 616 | 617 | /// Note that once `next` returns an error, the result of further calls is undefined. 618 | impl Iterator for PropertiesIter { 619 | type Item = Result; 620 | 621 | /// Returns the next line. 622 | /// 623 | /// Once this returns an error, the result of further calls is undefined. 624 | fn next(&mut self) -> Option { 625 | loop { 626 | match self.lines.next() { 627 | Some(Ok(LogicalLine(line_no, line))) => { 628 | if let Some(parsed_line) = parse_line(&line) { 629 | return Some(self.parsed_line_to_line(parsed_line, line_no)); 630 | } 631 | } 632 | Some(Err(e)) => return Some(Err(e)), 633 | None => return None, 634 | } 635 | } 636 | } 637 | } 638 | 639 | ///////////////////// 640 | 641 | /// A line ending style allowed in a Java properties file. 642 | #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Copy, Clone, Hash)] 643 | pub enum LineEnding { 644 | /// Carriage return alone. 645 | CR, 646 | /// Line feed alone. 647 | LF, 648 | /// Carriage return followed by line feed. 649 | // The name can't be changed without breaking backward compatibility. 650 | #[allow(clippy::upper_case_acronyms)] 651 | CRLF, 652 | } 653 | 654 | impl Display for LineEnding { 655 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 656 | f.write_str(match *self { 657 | LineEnding::CR => "LineEnding::CR", 658 | LineEnding::LF => "LineEnding::LF", 659 | LineEnding::CRLF => "LineEnding::CRLF", 660 | }) 661 | } 662 | } 663 | 664 | struct EncodingWriter { 665 | writer: W, 666 | lines_written: usize, 667 | encoder: Encoder, 668 | buffer: Vec, 669 | } 670 | 671 | impl EncodingWriter { 672 | fn write(&mut self, mut data: &str) -> Result<(), PropertiesError> { 673 | while !data.is_empty() { 674 | let (result, bytes_read) = self.encoder.encode_from_utf8_to_vec_without_replacement( 675 | data, 676 | &mut self.buffer, 677 | false, 678 | ); 679 | data = &data[bytes_read..]; 680 | match result { 681 | EncoderResult::InputEmpty => (), 682 | EncoderResult::OutputFull => { 683 | // encoding_rs won't just append to vectors if it would require allocation. 684 | self.buffer.reserve(self.buffer.capacity() * 2); 685 | } 686 | EncoderResult::Unmappable(c) => { 687 | let escaped = format!("\\u{:x}", c as isize); 688 | let (result2, _) = self.encoder.encode_from_utf8_to_vec_without_replacement( 689 | &escaped, 690 | &mut self.buffer, 691 | false, 692 | ); 693 | match result2 { 694 | EncoderResult::InputEmpty => (), 695 | EncoderResult::OutputFull => { 696 | // encoding_rs won't just append to vectors if it would require allocation. 697 | self.buffer.reserve(self.buffer.capacity() * 2); 698 | } 699 | EncoderResult::Unmappable(_) => { 700 | return Err(PropertiesError::new( 701 | format!( 702 | "Encoding error: unable to write UTF-8 escaping {:?} for {:?}", 703 | escaped, c 704 | ), 705 | None, 706 | Some(self.lines_written), 707 | )) 708 | } 709 | } 710 | } 711 | } 712 | } 713 | self.flush_buffer()?; 714 | Ok(()) 715 | } 716 | 717 | fn flush_buffer(&mut self) -> Result<(), PropertiesError> { 718 | self.writer.write_all(&self.buffer).map_err(|e| { 719 | PropertiesError::new("I/O error", Some(Box::new(e)), Some(self.lines_written)) 720 | })?; 721 | self.buffer.clear(); 722 | Ok(()) 723 | } 724 | 725 | fn flush(&mut self) -> Result<(), PropertiesError> { 726 | self.flush_buffer()?; 727 | self.writer.flush()?; 728 | Ok(()) 729 | } 730 | 731 | fn finish(&mut self) -> Result<(), PropertiesError> { 732 | let (result, _) = 733 | self.encoder 734 | .encode_from_utf8_to_vec_without_replacement("", &mut self.buffer, true); 735 | match result { 736 | EncoderResult::InputEmpty => (), 737 | EncoderResult::OutputFull => { 738 | return Err(PropertiesError::new( 739 | "Encoding error: output full", 740 | None, 741 | Some(self.lines_written), 742 | )) 743 | } 744 | EncoderResult::Unmappable(c) => { 745 | return Err(PropertiesError::new( 746 | format!("Encoding error: unmappable character {:?}", c), 747 | None, 748 | Some(self.lines_written), 749 | )) 750 | } 751 | } 752 | self.flush()?; 753 | Ok(()) 754 | } 755 | } 756 | 757 | /// Writes to a properties file. 758 | /// 759 | /// `finish()` *must* be called after writing all data. 760 | pub struct PropertiesWriter { 761 | comment_prefix: String, 762 | kv_separator: String, 763 | line_ending: LineEnding, 764 | writer: EncodingWriter, 765 | } 766 | 767 | impl PropertiesWriter { 768 | /// Writes to the given `Write` stream. 769 | pub fn new(writer: W) -> Self { 770 | Self::new_with_encoding(writer, WINDOWS_1252) 771 | } 772 | 773 | /// Writes to the given `Write` stream in the given encoding. 774 | /// Note that the Java properties specification specifies ISO-8859-1 encoding 775 | /// for properties files; in most cases, `new` should be called instead. 776 | pub fn new_with_encoding(writer: W, encoding: &'static Encoding) -> Self { 777 | PropertiesWriter { 778 | comment_prefix: "# ".to_string(), 779 | kv_separator: "=".to_string(), 780 | line_ending: LineEnding::LF, 781 | writer: EncodingWriter { 782 | writer, 783 | lines_written: 0, 784 | encoder: encoding.new_encoder(), 785 | // It's important that we start with a non-zero capacity, since we double it as needed. 786 | buffer: Vec::with_capacity(256), 787 | }, 788 | } 789 | } 790 | 791 | fn write_eol(&mut self) -> Result<(), PropertiesError> { 792 | self.writer.write(match self.line_ending { 793 | LineEnding::CR => "\r", 794 | LineEnding::LF => "\n", 795 | LineEnding::CRLF => "\r\n", 796 | })?; 797 | Ok(()) 798 | } 799 | 800 | /// Writes a comment to the file. 801 | pub fn write_comment(&mut self, comment: &str) -> Result<(), PropertiesError> { 802 | self.writer.lines_written += 1; 803 | self.writer.write(&self.comment_prefix)?; 804 | self.writer.write(comment)?; 805 | self.write_eol()?; 806 | Ok(()) 807 | } 808 | 809 | fn write_escaped(&mut self, s: &str) -> Result<(), PropertiesError> { 810 | self.writer.lines_written += 1; 811 | let mut escaped = String::new(); 812 | for c in s.chars() { 813 | match c { 814 | '\\' => escaped.push_str("\\\\"), 815 | ' ' => escaped.push_str("\\ "), 816 | '\t' => escaped.push_str("\\t"), 817 | '\r' => escaped.push_str("\\r"), 818 | '\n' => escaped.push_str("\\n"), 819 | '\x0c' => escaped.push_str("\\f"), 820 | ':' => escaped.push_str("\\:"), 821 | '=' => escaped.push_str("\\="), 822 | '!' => escaped.push_str("\\!"), 823 | '#' => escaped.push_str("\\#"), 824 | _ if c < ' ' => escaped.push_str(&format!("\\u{:x}", c as u16)), 825 | _ => escaped.push(c), // We don't worry about other characters, since they're taken care of below. 826 | } 827 | } 828 | self.writer.write(&escaped)?; 829 | Ok(()) 830 | } 831 | 832 | /// Writes a key/value pair to the file. 833 | pub fn write(&mut self, key: &str, value: &str) -> Result<(), PropertiesError> { 834 | self.write_escaped(key)?; 835 | self.writer.write(&self.kv_separator)?; 836 | self.write_escaped(value)?; 837 | self.write_eol()?; 838 | Ok(()) 839 | } 840 | 841 | /// Flushes the underlying stream. 842 | pub fn flush(&mut self) -> Result<(), PropertiesError> { 843 | self.writer.flush()?; 844 | Ok(()) 845 | } 846 | 847 | /// Sets the comment prefix. 848 | /// 849 | /// The prefix must contain a '#' or a '!', may only contain spaces, tabs, or form feeds before the comment character, 850 | /// and may not contain any carriage returns or line feeds ('\r' or '\n'). 851 | pub fn set_comment_prefix(&mut self, prefix: &str) -> Result<(), PropertiesError> { 852 | lazy_static! { 853 | static ref RE: Regex = Regex::new(r"^[ \t\x0c]*[#!][^\r\n]*$").unwrap(); 854 | } 855 | if !RE.is_match(prefix) { 856 | return Err(PropertiesError::new( 857 | &format!("Bad comment prefix: {:?}", prefix), 858 | None, 859 | None, 860 | )); 861 | } 862 | self.comment_prefix = prefix.to_string(); 863 | Ok(()) 864 | } 865 | 866 | /// Sets the key/value separator. 867 | /// 868 | /// The separator may be non-empty whitespace, or a colon with optional whitespace on either side, 869 | /// or an equals sign with optional whitespace on either side. (Whitespace here means ' ', '\t', or '\f'.) 870 | pub fn set_kv_separator(&mut self, separator: &str) -> Result<(), PropertiesError> { 871 | lazy_static! { 872 | static ref RE: Regex = Regex::new(r"^([ \t\x0c]*[:=][ \t\x0c]*|[ \t\x0c]+)$").unwrap(); 873 | } 874 | if !RE.is_match(separator) { 875 | return Err(PropertiesError::new( 876 | &format!("Bad key/value separator: {:?}", separator), 877 | None, 878 | None, 879 | )); 880 | } 881 | self.kv_separator = separator.to_string(); 882 | Ok(()) 883 | } 884 | 885 | /// Sets the line ending. 886 | pub fn set_line_ending(&mut self, line_ending: LineEnding) { 887 | self.line_ending = line_ending; 888 | } 889 | 890 | /// Finishes the encoding. 891 | pub fn finish(&mut self) -> Result<(), PropertiesError> { 892 | self.writer.finish()?; 893 | Ok(()) 894 | } 895 | } 896 | 897 | ///////////////////// 898 | 899 | /// Writes a hash map to a properties file. 900 | /// 901 | /// For more advanced use cases, use `PropertiesWriter`. 902 | pub fn write(writer: W, map: &HashMap) -> Result<(), PropertiesError> { 903 | let mut writer = PropertiesWriter::new(writer); 904 | for (k, v) in map { 905 | writer.write(k, v)?; 906 | } 907 | writer.finish()?; 908 | Ok(()) 909 | } 910 | 911 | /// Reads a properties file into a hash map. 912 | /// 913 | /// For more advanced use cases, use `PropertiesIter`. 914 | pub fn read(input: R) -> Result, PropertiesError> { 915 | let mut p = PropertiesIter::new(input); 916 | let mut map = HashMap::new(); 917 | p.read_into(|k, v| { 918 | map.insert(k, v); 919 | })?; 920 | Ok(map) 921 | } 922 | 923 | ///////////////////// 924 | 925 | #[cfg(test)] 926 | mod tests { 927 | use super::Line; 928 | use super::LineEnding; 929 | use super::LogicalLine; 930 | use super::LogicalLines; 931 | use super::NaturalLine; 932 | use super::NaturalLines; 933 | use super::ParsedLine; 934 | use super::PropertiesError; 935 | use super::PropertiesIter; 936 | use super::PropertiesWriter; 937 | use encoding_rs::UTF_8; 938 | use encoding_rs::WINDOWS_1252; 939 | use std::io; 940 | use std::io::ErrorKind; 941 | use std::io::Read; 942 | 943 | const LF: u8 = b'\n'; 944 | const CR: u8 = b'\r'; 945 | const SP: u8 = b' '; // space 946 | 947 | #[test] 948 | fn natural_lines() { 949 | let data = [ 950 | (vec![], vec![""]), 951 | (vec![SP], vec![" "]), 952 | (vec![SP, CR], vec![" ", ""]), 953 | (vec![SP, LF], vec![" ", ""]), 954 | (vec![SP, CR, LF], vec![" ", ""]), 955 | (vec![SP, CR, SP], vec![" ", " "]), 956 | (vec![SP, LF, SP], vec![" ", " "]), 957 | (vec![SP, CR, LF, SP], vec![" ", " "]), 958 | (vec![CR], vec!["", ""]), 959 | (vec![LF], vec!["", ""]), 960 | (vec![CR, LF], vec!["", ""]), 961 | (vec![CR, SP], vec!["", " "]), 962 | (vec![LF, SP], vec!["", " "]), 963 | (vec![CR, LF, SP], vec!["", " "]), 964 | ]; 965 | for &(ref bytes, ref lines) in &data { 966 | let reader = &bytes as &[u8]; 967 | let mut iter = NaturalLines::new(reader, WINDOWS_1252); 968 | let mut count = 1; 969 | for line in lines { 970 | match (line.to_string(), iter.next()) { 971 | (ref e, Some(Ok(NaturalLine(a_ln, ref a)))) => { 972 | if (count, e) != (a_ln, a) { 973 | panic!("Failure while processing {:?}. Expected Some(Ok({:?})), but was {:?}", bytes, (count, e), (a_ln, a)); 974 | } 975 | } 976 | (e, a) => panic!( 977 | "Failure while processing {:?}. Expected Some(Ok({:?})), but was {:?}", 978 | bytes, 979 | (count, e), 980 | a 981 | ), 982 | } 983 | count += 1; 984 | } 985 | match iter.next() { 986 | None => (), 987 | a => panic!( 988 | "Failure while processing {:?}. Expected None, but was {:?}", 989 | bytes, a 990 | ), 991 | } 992 | } 993 | } 994 | 995 | #[test] 996 | fn logical_lines() { 997 | let data = [ 998 | (vec![], vec![]), 999 | (vec!["foo"], vec!["foo"]), 1000 | (vec!["foo", "bar"], vec!["foo", "bar"]), 1001 | (vec!["foo\\", "bar"], vec!["foobar"]), 1002 | (vec!["foo\\\\", "bar"], vec!["foo\\\\", "bar"]), 1003 | (vec!["foo\\\\\\", "bar"], vec!["foo\\\\bar"]), 1004 | (vec!["foo\\", " bar"], vec!["foobar"]), 1005 | (vec!["#foo\\", " bar"], vec!["#foo\\", " bar"]), 1006 | (vec!["foo\\", "# bar"], vec!["foo# bar"]), 1007 | (vec!["\u{1F41E}\\", "\u{1F41E}"], vec!["\u{1F41E}\u{1F41E}"]), 1008 | ( 1009 | vec!["\u{1F41E}\\", " \u{1F41E}"], 1010 | vec!["\u{1F41E}\u{1F41E}"], 1011 | ), 1012 | ]; 1013 | for &(ref input_lines, ref lines) in &data { 1014 | let mut count = 0; 1015 | let mut iter = LogicalLines::new(input_lines.iter().map(|x| { 1016 | count += 1; 1017 | Ok(NaturalLine(count, x.to_string())) 1018 | })); 1019 | let mut e_ln = 0; 1020 | for line in lines { 1021 | e_ln += 1; 1022 | match (line.to_string(), iter.next()) { 1023 | (ref e, Some(Ok(LogicalLine(a_ln, ref a)))) => { 1024 | if (e_ln, e) != (a_ln, a) { 1025 | panic!("Failure while processing {:?}. Expected Some(Ok({:?})), but was {:?}", input_lines, (e_ln, e), (a_ln, a)); 1026 | } 1027 | } 1028 | (e, a) => panic!( 1029 | "Failure while processing {:?}. Expected Some(Ok({:?})), but was {:?}", 1030 | input_lines, 1031 | (e_ln, e), 1032 | a 1033 | ), 1034 | } 1035 | } 1036 | match iter.next() { 1037 | None => (), 1038 | a => panic!( 1039 | "Failure while processing {:?}. Expected None, but was {:?}", 1040 | input_lines, a 1041 | ), 1042 | } 1043 | } 1044 | } 1045 | 1046 | #[test] 1047 | fn count_ending_backslashes() { 1048 | assert_eq!(0, super::count_ending_backslashes("")); 1049 | 1050 | assert_eq!(0, super::count_ending_backslashes("x")); 1051 | assert_eq!(1, super::count_ending_backslashes("\\")); 1052 | 1053 | assert_eq!(0, super::count_ending_backslashes("xx")); 1054 | assert_eq!(0, super::count_ending_backslashes("\\x")); 1055 | assert_eq!(1, super::count_ending_backslashes("x\\")); 1056 | assert_eq!(2, super::count_ending_backslashes("\\\\")); 1057 | 1058 | assert_eq!(0, super::count_ending_backslashes("xxx")); 1059 | assert_eq!(0, super::count_ending_backslashes("\\xx")); 1060 | assert_eq!(0, super::count_ending_backslashes("x\\x")); 1061 | assert_eq!(0, super::count_ending_backslashes("\\\\x")); 1062 | assert_eq!(1, super::count_ending_backslashes("xx\\")); 1063 | assert_eq!(1, super::count_ending_backslashes("\\x\\")); 1064 | assert_eq!(2, super::count_ending_backslashes("x\\\\")); 1065 | assert_eq!(3, super::count_ending_backslashes("\\\\\\")); 1066 | 1067 | assert_eq!(0, super::count_ending_backslashes("x\u{1F41E}")); 1068 | assert_eq!(0, super::count_ending_backslashes("\\\u{1F41E}")); 1069 | assert_eq!(0, super::count_ending_backslashes("\u{1F41E}x")); 1070 | assert_eq!(1, super::count_ending_backslashes("\u{1F41E}\\")); 1071 | } 1072 | 1073 | #[test] 1074 | fn parse_line() { 1075 | let data = [ 1076 | ("", None), 1077 | (" ", None), 1078 | ("\\", Some(ParsedLine::KVPair("\\", ""))), 1079 | ("a=\\", Some(ParsedLine::KVPair("a", "\\"))), 1080 | ("\\ ", Some(ParsedLine::KVPair("\\ ", ""))), 1081 | ("# foo", Some(ParsedLine::Comment("foo"))), 1082 | (" # foo", Some(ParsedLine::Comment("foo"))), 1083 | ("a # foo", Some(ParsedLine::KVPair("a", "# foo"))), 1084 | ("a", Some(ParsedLine::KVPair("a", ""))), 1085 | ("a = b", Some(ParsedLine::KVPair("a", "b"))), 1086 | ("a : b", Some(ParsedLine::KVPair("a", "b"))), 1087 | ("a b", Some(ParsedLine::KVPair("a", "b"))), 1088 | (" a = b ", Some(ParsedLine::KVPair("a", "b "))), 1089 | (" a : b", Some(ParsedLine::KVPair("a", "b"))), 1090 | (" a b", Some(ParsedLine::KVPair("a", "b"))), 1091 | ("a:=b", Some(ParsedLine::KVPair("a", "=b"))), 1092 | ("a=:b", Some(ParsedLine::KVPair("a", ":b"))), 1093 | ("a b:c", Some(ParsedLine::KVPair("a", "b:c"))), 1094 | ( 1095 | "a\\ \\:\\=b c", 1096 | Some(ParsedLine::KVPair("a\\ \\:\\=b", "c")), 1097 | ), 1098 | ( 1099 | "a\\ \\:\\=b=c", 1100 | Some(ParsedLine::KVPair("a\\ \\:\\=b", "c")), 1101 | ), 1102 | ( 1103 | "a\\\\ \\\\:\\\\=b c", 1104 | Some(ParsedLine::KVPair("a\\\\", "\\\\:\\\\=b c")), 1105 | ), 1106 | ("\\ b", Some(ParsedLine::KVPair("\\ ", "b"))), 1107 | ("=", Some(ParsedLine::KVPair("", ""))), 1108 | ("=x", Some(ParsedLine::KVPair("", "x"))), 1109 | ("x=", Some(ParsedLine::KVPair("x", ""))), 1110 | ("\\=x", Some(ParsedLine::KVPair("\\=x", ""))), 1111 | ( 1112 | "\u{1F41E}=\u{1F41E}", 1113 | Some(ParsedLine::KVPair("\u{1F41E}", "\u{1F41E}")), 1114 | ), 1115 | ]; 1116 | for &(line, ref expected) in &data { 1117 | let actual = super::parse_line(line); 1118 | if expected != &actual { 1119 | panic!( 1120 | "Failed when splitting {:?}. Expected {:?} but got {:?}", 1121 | line, expected, actual 1122 | ); 1123 | } 1124 | } 1125 | } 1126 | 1127 | #[test] 1128 | fn unescape() { 1129 | let data = [ 1130 | (r"", Some("")), 1131 | (r"x", Some("x")), 1132 | (r"\\", Some("\\")), 1133 | (r"\#", Some("#")), 1134 | (r"\!", Some("!")), 1135 | (r"\\\n\r\t\f\u0001\b", Some("\\\n\r\t\x0c\u{0001}b")), 1136 | (r"\", Some("\x00")), 1137 | (r"\u", None), 1138 | (r"\uasfd", None), 1139 | ]; 1140 | for &(input, expected) in &data { 1141 | let actual = &super::unescape(input, 1); 1142 | let is_match = match (expected, actual) { 1143 | (Some(e), &Ok(ref a)) => e == a, 1144 | (None, &Err(_)) => true, 1145 | _ => false, 1146 | }; 1147 | if !is_match { 1148 | panic!( 1149 | "Failed when unescaping {:?}. Expected {:?} but got {:?}", 1150 | input, expected, actual 1151 | ); 1152 | } 1153 | } 1154 | } 1155 | 1156 | #[test] 1157 | fn properties_iter() { 1158 | fn mk_comment(line_no: usize, text: &str) -> Line { 1159 | Line::mk_comment(line_no, text.to_string()) 1160 | } 1161 | fn mk_pair(line_no: usize, key: &str, value: &str) -> Line { 1162 | Line::mk_pair(line_no, key.to_string(), value.to_string()) 1163 | } 1164 | let data = vec![ 1165 | ( 1166 | WINDOWS_1252, 1167 | vec![ 1168 | ("", vec![]), 1169 | ("a=b", vec![mk_pair(1, "a", "b")]), 1170 | ("a=\\#b", vec![mk_pair(1, "a", "#b")]), 1171 | ("\\!a=b", vec![mk_pair(1, "!a", "b")]), 1172 | ("a=b\nc=d\\\ne=f\ng=h\r#comment1\r\n#comment2\\\ni=j\\\n#comment3\n \n#comment4", vec![ 1173 | mk_pair(1, "a", "b"), 1174 | mk_pair(2, "c", "de=f"), 1175 | mk_pair(4, "g", "h"), 1176 | mk_comment(5, "comment1"), 1177 | mk_comment(6, "comment2\u{0}"), 1178 | mk_pair(7, "i", "j#comment3"), 1179 | mk_comment(10, "comment4"), 1180 | ]), 1181 | ("a = b\\\n c, d ", vec![mk_pair(1, "a", "bc, d ")]), 1182 | ("x=\\\\\\\nty", vec![mk_pair(1, "x", "\\ty")]), 1183 | ], 1184 | ), 1185 | ( 1186 | UTF_8, 1187 | vec![( 1188 | "a=日本語\nb=Français", 1189 | vec![mk_pair(1, "a", "日本語"), mk_pair(2, "b", "Français")], 1190 | )], 1191 | ), 1192 | ]; 1193 | for &(encoding, ref dataset) in &data { 1194 | for &(input, ref lines) in dataset { 1195 | let mut iter = PropertiesIter::new_with_encoding(input.as_bytes(), encoding); 1196 | for line in lines { 1197 | match (line, iter.next()) { 1198 | (ref e, Some(Ok(ref a))) => { 1199 | if e != &a { 1200 | panic!("Failure while processing {:?}. Expected Some(Ok({:?})), but was {:?}", input, e, a); 1201 | } 1202 | } 1203 | (e, a) => panic!( 1204 | "Failure while processing {:?}. Expected Some(Ok({:?})), but was {:?}", 1205 | input, e, a 1206 | ), 1207 | } 1208 | } 1209 | match iter.next() { 1210 | None => (), 1211 | a => panic!( 1212 | "Failure while processing {:?}. Expected None, but was {:?}", 1213 | input, a 1214 | ), 1215 | } 1216 | } 1217 | } 1218 | } 1219 | 1220 | #[test] 1221 | fn properties_writer_kv() { 1222 | let data = [ 1223 | ("", "", "=\n"), 1224 | ("a", "b", "a=b\n"), 1225 | (" :=", " :=", "\\ \\:\\==\\ \\:\\=\n"), 1226 | ("!", "#", "\\!=\\#\n"), 1227 | ("\u{1F41E}", "\u{1F41E}", "\\u1f41e=\\u1f41e\n"), 1228 | ]; 1229 | for &(key, value, expected) in &data { 1230 | let mut buf = Vec::new(); 1231 | { 1232 | let mut writer = PropertiesWriter::new(&mut buf); 1233 | writer.write(key, value).unwrap(); 1234 | writer.finish().unwrap(); 1235 | } 1236 | let actual = WINDOWS_1252.decode(&buf).0; 1237 | if expected != actual { 1238 | panic!("Failure while processing key {:?} and value {:?}. Expected {:?}, but was {:?}", key, value, expected, actual); 1239 | } 1240 | } 1241 | } 1242 | 1243 | #[test] 1244 | fn properties_writer_kv_custom_encoding() { 1245 | let data = [ 1246 | ("", "", "=\n"), 1247 | ("a", "b", "a=b\n"), 1248 | (" :=", " :=", "\\ \\:\\==\\ \\:\\=\n"), 1249 | ("!", "#", "\\!=\\#\n"), 1250 | ("\u{1F41E}", "\u{1F41E}", "\u{1F41E}=\u{1F41E}\n"), 1251 | ]; 1252 | for &(key, value, expected) in &data { 1253 | let mut buf = Vec::new(); 1254 | { 1255 | let mut writer = PropertiesWriter::new_with_encoding(&mut buf, UTF_8); 1256 | writer.write(key, value).unwrap(); 1257 | writer.finish().unwrap(); 1258 | } 1259 | let actual = UTF_8.decode(&buf).0; 1260 | if expected != actual { 1261 | panic!("Failure while processing key {:?} and value {:?}. Expected {:?}, but was {:?}", key, value, expected, actual); 1262 | } 1263 | } 1264 | } 1265 | 1266 | #[test] 1267 | fn properties_writer_comment() { 1268 | let data = [ 1269 | ("", "# \n"), 1270 | ("a", "# a\n"), 1271 | (" :=", "# :=\n"), 1272 | ("\u{1F41E}", "# \\u1f41e\n"), 1273 | ]; 1274 | for &(comment, expected) in &data { 1275 | let mut buf = Vec::new(); 1276 | { 1277 | let mut writer = PropertiesWriter::new(&mut buf); 1278 | writer.write_comment(comment).unwrap(); 1279 | writer.finish().unwrap(); 1280 | } 1281 | let actual = UTF_8.decode(&buf).0; 1282 | if expected != actual { 1283 | panic!( 1284 | "Failure while processing {:?}. Expected {:?}, but was {:?}", 1285 | comment, expected, actual 1286 | ); 1287 | } 1288 | } 1289 | } 1290 | 1291 | #[test] 1292 | fn properties_writer_good_comment_prefix() { 1293 | let prefixes = ["#", "!", " #", " !", "#x", "!x", "\x0c#"]; 1294 | let mut buf = Vec::new(); 1295 | for prefix in &prefixes { 1296 | let mut writer = PropertiesWriter::new(&mut buf); 1297 | writer.set_comment_prefix(prefix).unwrap(); 1298 | } 1299 | } 1300 | 1301 | #[test] 1302 | fn properties_writer_bad_comment_prefix() { 1303 | let prefixes = ["", " ", "x", "\n#", "#\n", "#\r"]; 1304 | let mut buf = Vec::new(); 1305 | for prefix in &prefixes { 1306 | let mut writer = PropertiesWriter::new(&mut buf); 1307 | match writer.set_comment_prefix(prefix) { 1308 | Ok(_) => panic!("Unexpectedly succeded with prefix {:?}", prefix), 1309 | Err(_) => (), 1310 | } 1311 | } 1312 | } 1313 | 1314 | #[test] 1315 | fn properties_writer_custom_comment_prefix() { 1316 | let data = [ 1317 | ("", " !\n"), 1318 | ("a", " !a\n"), 1319 | (" :=", " ! :=\n"), 1320 | ("\u{1F41E}", " !\\u1f41e\n"), 1321 | ]; 1322 | for &(comment, expected) in &data { 1323 | let mut buf = Vec::new(); 1324 | { 1325 | let mut writer = PropertiesWriter::new(&mut buf); 1326 | writer.set_comment_prefix(" !").unwrap(); 1327 | writer.write_comment(comment).unwrap(); 1328 | writer.finish().unwrap(); 1329 | } 1330 | let actual = WINDOWS_1252.decode(&buf).0; 1331 | if expected != actual { 1332 | panic!( 1333 | "Failure while processing {:?}. Expected {:?}, but was {:?}", 1334 | comment, expected, actual 1335 | ); 1336 | } 1337 | } 1338 | } 1339 | 1340 | #[test] 1341 | fn properties_writer_good_kv_separator() { 1342 | let separators = [":", "=", " ", " :", ": ", " =", "= ", "\x0c", "\t"]; 1343 | let mut buf = Vec::new(); 1344 | for separator in &separators { 1345 | let mut writer = PropertiesWriter::new(&mut buf); 1346 | writer.set_kv_separator(separator).unwrap(); 1347 | } 1348 | } 1349 | 1350 | #[test] 1351 | fn properties_writer_bad_kv_separator() { 1352 | let separators = ["", "x", ":=", "=:", "\n", "\r"]; 1353 | let mut buf = Vec::new(); 1354 | for separator in &separators { 1355 | let mut writer = PropertiesWriter::new(&mut buf); 1356 | match writer.set_kv_separator(separator) { 1357 | Ok(_) => panic!("Unexpectedly succeded with separator {:?}", separator), 1358 | Err(_) => (), 1359 | } 1360 | } 1361 | } 1362 | 1363 | #[test] 1364 | fn properties_writer_custom_kv_separator() { 1365 | let data = [ 1366 | (":", "x:y\n"), 1367 | ("=", "x=y\n"), 1368 | (" ", "x y\n"), 1369 | (" :", "x :y\n"), 1370 | (": ", "x: y\n"), 1371 | (" =", "x =y\n"), 1372 | ("= ", "x= y\n"), 1373 | ("\x0c", "x\x0cy\n"), 1374 | ("\t", "x\ty\n"), 1375 | ]; 1376 | for &(separator, expected) in &data { 1377 | let mut buf = Vec::new(); 1378 | { 1379 | let mut writer = PropertiesWriter::new(&mut buf); 1380 | writer.set_kv_separator(separator).unwrap(); 1381 | writer.write("x", "y").unwrap(); 1382 | writer.finish().unwrap(); 1383 | } 1384 | let actual = WINDOWS_1252.decode(&buf).0; 1385 | if expected != actual { 1386 | panic!( 1387 | "Failure while processing {:?}. Expected {:?}, but was {:?}", 1388 | separator, expected, actual 1389 | ); 1390 | } 1391 | } 1392 | } 1393 | 1394 | #[test] 1395 | fn properties_writer_custom_line_ending() { 1396 | let data = [ 1397 | (LineEnding::CR, "# foo\rx=y\r"), 1398 | (LineEnding::LF, "# foo\nx=y\n"), 1399 | (LineEnding::CRLF, "# foo\r\nx=y\r\n"), 1400 | ]; 1401 | for &(line_ending, expected) in &data { 1402 | let mut buf = Vec::new(); 1403 | { 1404 | let mut writer = PropertiesWriter::new(&mut buf); 1405 | writer.set_line_ending(line_ending); 1406 | writer.write_comment("foo").unwrap(); 1407 | writer.write("x", "y").unwrap(); 1408 | writer.finish().unwrap(); 1409 | } 1410 | let actual = WINDOWS_1252.decode(&buf).0; 1411 | if expected != actual { 1412 | panic!( 1413 | "Failure while processing {:?}. Expected {:?}, but was {:?}", 1414 | line_ending, expected, actual 1415 | ); 1416 | } 1417 | } 1418 | } 1419 | 1420 | struct ErrorReader; 1421 | 1422 | impl Read for ErrorReader { 1423 | fn read(&mut self, _: &mut [u8]) -> io::Result { 1424 | Err(io::Error::new(ErrorKind::InvalidData, "dummy error")) 1425 | } 1426 | } 1427 | 1428 | #[test] 1429 | fn properties_error_line_number() { 1430 | let data = [ 1431 | ("", 1), 1432 | ("\n", 2), 1433 | ("\r", 2), 1434 | ("\r\n", 2), 1435 | ("\\uxxxx", 1), 1436 | ("\n\\uxxxx", 2), 1437 | ("a\\\nb\n\\uxxxx", 3), 1438 | ]; 1439 | for &(input, line_number) in &data { 1440 | let iter = PropertiesIter::new(input.as_bytes().chain(ErrorReader)); 1441 | let mut got_error = false; 1442 | for line in iter { 1443 | if let Err(e) = line { 1444 | assert_eq!(e.line_number(), Some(line_number)); 1445 | got_error = true; 1446 | break; 1447 | } 1448 | } 1449 | assert!(got_error); 1450 | } 1451 | } 1452 | 1453 | #[test] 1454 | fn properties_error_display() { 1455 | assert_eq!( 1456 | format!("{}", PropertiesError::new("foo", None, None)), 1457 | "foo (line_number = unknown)" 1458 | ); 1459 | assert_eq!( 1460 | format!("{}", PropertiesError::new("foo", None, Some(1))), 1461 | "foo (line_number = 1)" 1462 | ); 1463 | } 1464 | 1465 | #[test] 1466 | fn line_display() { 1467 | assert_eq!( 1468 | format!("{}", Line::mk_pair(1, "foo".to_string(), "bar".to_string())), 1469 | "Line {line_number: 1, content: KVPair(\"foo\", \"bar\")}" 1470 | ); 1471 | assert_eq!( 1472 | format!("{}", Line::mk_comment(1, "baz".to_string())), 1473 | "Line {line_number: 1, content: Comment(\"baz\")}" 1474 | ); 1475 | } 1476 | } 1477 | --------------------------------------------------------------------------------