java_properties/
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 <https://docs.oracle.com/javase/7/docs/api/java/util/Properties.html>.
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
67use encoding_rs::CoderResult;
68use encoding_rs::Decoder;
69use encoding_rs::Encoder;
70use encoding_rs::EncoderResult;
71use encoding_rs::Encoding;
72use encoding_rs::WINDOWS_1252;
73use lazy_static::lazy_static;
74use regex::Regex;
75use std::collections::HashMap;
76use std::collections::VecDeque;
77use std::convert::From;
78use std::error::Error;
79use std::fmt;
80use std::fmt::Display;
81use std::fmt::Formatter;
82use std::io;
83use std::io::Read;
84use std::io::Write;
85use std::iter::Peekable;
86use std::ops::Deref;
87
88/////////////////////
89
90/// The error type for reading and writing properties files.
91#[derive(Debug)]
92pub struct PropertiesError {
93    description: String,
94    cause: Option<Box<dyn Error + 'static + Send + Sync>>,
95    line_number: Option<usize>,
96}
97
98impl PropertiesError {
99    fn new<S: Into<String>>(
100        description: S,
101        cause: Option<Box<dyn Error + 'static + Send + Sync>>,
102        line_number: Option<usize>,
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<usize> {
113        self.line_number
114    }
115}
116
117impl 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
132impl From<io::Error> for PropertiesError {
133    fn from(e: io::Error) -> Self {
134        PropertiesError::new("I/O error", Some(Box::new(e)), None)
135    }
136}
137
138impl 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
150struct DecodeIter<R: Read> {
151    decoder: Decoder,
152    reader: R,
153    input_buffer: Vec<u8>,
154    output_buffer: String,
155    chars: VecDeque<char>,
156}
157
158impl<R: Read> DecodeIter<R> {
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
172impl<R: Read> Iterator for DecodeIter<R> {
173    type Item = Result<char, io::Error>;
174
175    fn next(&mut self) -> Option<Self::Item> {
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)]
217struct NaturalLine(usize, String);
218
219// We can't use BufRead.lines() because it doesn't use the proper line endings
220struct NaturalLines<R: Read> {
221    chars: Peekable<DecodeIter<R>>,
222    eof: bool,
223    line_count: usize,
224}
225
226impl<R: Read> NaturalLines<R> {
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
236const LF: char = '\n';
237const CR: char = '\r';
238
239impl<R: Read> Iterator for NaturalLines<R> {
240    type Item = Result<NaturalLine, PropertiesError>;
241
242    fn next(&mut self) -> Option<Self::Item> {
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)]
281struct LogicalLine(usize, String);
282
283struct LogicalLines<I: Iterator<Item = Result<NaturalLine, PropertiesError>>> {
284    physical_lines: I,
285    eof: bool,
286}
287
288impl<I: Iterator<Item = Result<NaturalLine, PropertiesError>>> LogicalLines<I> {
289    fn new(physical_lines: I) -> Self {
290        LogicalLines {
291            physical_lines,
292            eof: false,
293        }
294    }
295}
296
297fn 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
309impl<I: Iterator<Item = Result<NaturalLine, PropertiesError>>> Iterator for LogicalLines<I> {
310    type Item = Result<LogicalLine, PropertiesError>;
311
312    fn next(&mut self) -> Option<Self::Item> {
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)]
357enum 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)]
364pub struct Line {
365    line_number: usize,
366    data: LineContent,
367}
368
369impl 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
400impl 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)]
412pub 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
420impl 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
429impl From<Line> for LineContent {
430    fn from(line: Line) -> LineContent {
431        line.data
432    }
433}
434
435/////////////////////
436
437fn unescape(s: &str, line_number: usize) -> Result<String, PropertiesError> {
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
507lazy_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
538fn parse_line(line: &str) -> Option<ParsedLine> {
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.
564pub struct PropertiesIter<R: Read> {
565    lines: LogicalLines<NaturalLines<R>>,
566}
567
568impl<R: Read> PropertiesIter<R> {
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<F: FnMut(String, String)>(&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<Line, PropertiesError> {
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.
618impl<R: Read> Iterator for PropertiesIter<R> {
619    type Item = Result<Line, PropertiesError>;
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<Self::Item> {
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)]
643pub 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
654impl 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
664struct EncodingWriter<W: Write> {
665    writer: W,
666    lines_written: usize,
667    encoder: Encoder,
668    buffer: Vec<u8>,
669}
670
671impl<W: Write> EncodingWriter<W> {
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.
760pub struct PropertiesWriter<W: Write> {
761    comment_prefix: String,
762    kv_separator: String,
763    line_ending: LineEnding,
764    writer: EncodingWriter<W>,
765}
766
767impl<W: Write> PropertiesWriter<W> {
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`.
902pub fn write<W: Write>(writer: W, map: &HashMap<String, String>) -> 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`.
914pub fn read<R: Read>(input: R) -> Result<HashMap<String, String>, 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)]
926mod 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<usize> {
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}