Repository: BurntSushi/rust-csv (language: rust)

Issue:
Automatically escape fields that contain the comment character
Currently, if data is written with QuoteStyle::Necessary, and the first
field of a row happens to start with a comment character, the row will be
ignored as a comment when later reading it back in.

This change adds a `comment` property to Writer, and automatically
quotes fields that have the provided comment character in them, so they
round-trip correctly.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 91a6d2a5..d9a2b03f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,29 +18,29 @@ jobs:
         build: [pinned, stable, beta, nightly, macos, win-msvc, win-gnu]
         include:
         - build: pinned
-          os: ubuntu-22.04
-          rust: 1.60.0
+          os: ubuntu-latest
+          rust: 1.61.0
         - build: stable
-          os: ubuntu-22.04
+          os: ubuntu-latest
           rust: stable
         - build: beta
-          os: ubuntu-22.04
+          os: ubuntu-latest
           rust: beta
         - build: nightly
-          os: ubuntu-22.04
+          os: ubuntu-latest
           rust: nightly
         - build: macos
-          os: macos-12
+          os: macos-latest
           rust: stable
         - build: win-msvc
-          os: windows-2022
+          os: windows-latest
           rust: stable
         - build: win-gnu
-          os: windows-2022
+          os: windows-latest
           rust: stable-x86_64-gnu
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
     - name: Install Rust
       uses: dtolnay/rust-toolchain@master
       with:
@@ -53,12 +53,12 @@ jobs:
     - if: matrix.build == 'nightly'
       run: cargo bench --verbose --no-run
 
-  rustfmt:
+  check-doc-sync:
     name: check tutorial and cookbook examples
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
     - name: Install Rust
       uses: dtolnay/rust-toolchain@master
       with:
@@ -69,26 +69,25 @@ jobs:
     - name: check that tutorial examples are up to date and in sync
       run: ci/check-copy tutorial
 
-  check-doc-sync:
-    name: rustfmt
-    runs-on: ubuntu-22.04
+  rustfmt:
+    runs-on: ubuntu-latest
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
     - name: Install Rust
       uses: dtolnay/rust-toolchain@master
       with:
         toolchain: stable
         components: rustfmt
     - name: Check formatting
-      run: cargo fmt --check
+      run: cargo fmt --all --check
 
   miri:
     name: miri
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-latest
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
     - name: Install Rust
       uses: dtolnay/rust-toolchain@miri
     - run: cargo miri test --lib --verbose
diff --git a/Cargo.toml b/Cargo.toml
index e47b77dc..46dd9ed3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,8 +12,7 @@ license = "Unlicense/MIT"
 categories = ["encoding", "parser-implementations"]
 exclude = ["/.github", "/ci/*", "/scripts/*"]
 edition = "2021"
-resolver = "2"
-rust-version = "1.60"
+rust-version = "1.61"
 
 [workspace]
 members = ["csv-core", "csv-index"]
diff --git a/README.md b/README.md
index e1d84430..938928f5 100644
--- a/README.md
+++ b/README.md
@@ -13,18 +13,15 @@ Dual-licensed under MIT or the [UNLICENSE](http://unlicense.org).
 https://docs.rs/csv
 
 If you're new to Rust, the
-[tutorial](https://docs.rs/csv/1.0.0/csv/tutorial/index.html)
+[tutorial](https://docs.rs/csv/1.*/csv/tutorial/index.html)
 is a good place to start.
 
 
 ### Usage
 
-Add this to your `Cargo.toml`:
+To bring this crate into your repository, either add `csv` to your
+`Cargo.toml`, or run `cargo add csv`.
 
-```toml
-[dependencies]
-csv = "1.2"
-```
 
 ### Example
 
@@ -32,7 +29,7 @@ This example shows how to read CSV data from stdin and print each record to
 stdout.
 
 There are more examples in the
-[cookbook](https://docs.rs/csv/1.0.0/csv/cookbook/index.html).
+[cookbook](https://docs.rs/csv/1.*/csv/cookbook/index.html).
 
 ```rust
 use std::{error::Error, io, process};
@@ -103,7 +100,7 @@ fn main() {
 
 The above example can be run like so:
 
-```text
+```
 $ git clone git://github.com/BurntSushi/rust-csv
 $ cd rust-csv
 $ cargo run --example cookbook-read-serde < examples/data/smallpop.csv
diff --git a/csv-core/src/writer.rs b/csv-core/src/writer.rs
index 4f943017..ba196b63 100644
--- a/csv-core/src/writer.rs
+++ b/csv-core/src/writer.rs
@@ -26,6 +26,7 @@ impl WriterBuilder {
             quote: b'"',
             escape: b'\\',
             double_quote: true,
+            comment: None,
         };
         WriterBuilder { wtr: wtr }
     }
@@ -56,6 +57,13 @@ impl WriterBuilder {
             }
             _ => unreachable!(),
         }
+        // If the first field of a row starts with a comment character,
+        // it needs to be quoted, or the row will not be readable later.
+        // As requires_quotes is calculated in advance, we force quotes
+        // when a comment character is encountered anywhere in the field.
+        if let Some(comment) = self.wtr.comment {
+            wtr.requires_quotes[comment as usize] = true;
+        }
         wtr
     }
 
@@ -119,6 +127,17 @@ impl WriterBuilder {
         self.wtr.double_quote = yes;
         self
     }
+
+    /// The comment character that will be used when later reading the file.
+    ///
+    /// If `quote_style` is set to `QuoteStyle::Necessary`, a field will
+    /// be quoted if the comment character is detected anywhere in the field.
+    ///
+    /// The default value is None.
+    pub fn comment(&mut self, comment: Option<u8>) -> &mut WriterBuilder {
+        self.wtr.comment = comment;
+        self
+    }
 }
 
 impl Default for WriterBuilder {
@@ -166,6 +185,7 @@ pub struct Writer {
     quote: u8,
     escape: u8,
     double_quote: bool,
+    comment: Option<u8>,
 }
 
 impl Clone for Writer {
@@ -183,6 +203,7 @@ impl Clone for Writer {
             quote: self.quote,
             escape: self.escape,
             double_quote: self.double_quote,
+            comment: self.comment,
         }
     }
 }
@@ -1044,4 +1065,21 @@ mod tests {
         inp = &inp[1..];
         assert_quote!(inp, out, 1, 2, InputEmpty, r#""""#);
     }
+
+    #[test]
+    fn comment_char_is_automatically_quoted() {
+        let mut wtr = WriterBuilder::new().comment(Some(b'#')).build();
+        let out = &mut [0; 1024];
+
+        assert_field!(
+            wtr,
+            b("# abc"),
+            &mut out[..],
+            5,
+            6,
+            InputEmpty,
+            "\"# abc"
+        );
+        assert_write!(wtr, finish, &mut out[..], 1, InputEmpty, "\"");
+    }
 }
diff --git a/src/writer.rs b/src/writer.rs
index 392c2073..9fef1f16 100644
--- a/src/writer.rs
+++ b/src/writer.rs
@@ -470,6 +470,34 @@ impl WriterBuilder {
         self
     }
 
+    /// The comment character that will be used when later reading the file.
+    ///
+    /// If `quote_style` is set to `QuoteStyle::Necessary`, a field will
+    /// be quoted if the comment character is detected anywhere in the field.
+    ///
+    /// The default value is None.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use std::error::Error;
+    /// use csv::WriterBuilder;
+    ///
+    /// # fn main() { example().unwrap(); }
+    /// fn example() -> Result<(), Box<dyn Error>> {
+    ///     let mut wtr =
+    ///         WriterBuilder::new().comment(Some(b'#')).from_writer(Vec::new());
+    ///     wtr.write_record(&["# comment", "another"]).unwrap();
+    ///     let buf = wtr.into_inner().unwrap();
+    ///     assert_eq!(String::from_utf8(buf).unwrap(), "\"# comment\",another\n");
+    ///     Ok(())
+    /// }
+    /// ```
+    pub fn comment(&mut self, comment: Option<u8>) -> &mut WriterBuilder {
+        self.builder.comment(comment);
+        self
+    }
+
     /// Set the capacity (in bytes) of the internal buffer used in the CSV
     /// writer. This defaults to a reasonable setting.
     pub fn buffer_capacity(&mut self, capacity: usize) -> &mut WriterBuilder {
@@ -1414,4 +1442,13 @@ mod tests {
         wtr.serialize((true, 1.3, "hi")).unwrap();
         assert_eq!(wtr_as_string(wtr), "true,1.3,hi\n");
     }
+
+    #[test]
+    fn comment_char_is_automatically_quoted() {
+        let mut wtr =
+            WriterBuilder::new().comment(Some(b'#')).from_writer(Vec::new());
+        wtr.write_record(&["# comment", "another"]).unwrap();
+        let buf = wtr.into_inner().unwrap();
+        assert_eq!(String::from_utf8(buf).unwrap(), "\"# comment\",another\n");
+    }
 }


Test patch: