History

Merge branch 'worktree-encoding'

Sebastian Thiel committed 3 years ago

Commit 5af2cf32 parents 3fd5e16 d1fed3e11 files · +406 −72Browse files

Unified · Split

11 files
  1. Cargo.lock +1 −0
  2. gix-filter/Cargo.toml +1 −0
  3. gix-filter/src/ident.rs +43 −25
  4. gix-filter/src/lib.rs +4 −2
  5. gix-filter/src/worktree/encode_to_git.rs +90 −0
  6. gix-filter/src/worktree/encode_to_worktree.rs +69 −0
  7. gix-filter/src/worktree/encoding.rs +31 −0
  8. gix-filter/src/worktree/mod.rs +16 −0
  9. gix-filter/tests/filter.rs +1 −0
  10. gix-filter/tests/ident/mod.rs +49 −45
  11. gix-filter/tests/worktree/mod.rs +101 −0
Cargo.lock+1−0
@@ -1650,6 +1650,7 @@ name = "gix-filter" · 1649 unmodified lines
16501650 version = "0.0.0"
16511651 dependencies = [
16521652 "bstr",
1653+ "encoding_rs",
16531654 "gix-hash 0.11.3",
16541655 "gix-object 0.31.0",
16551656 "gix-trace 0.1.1",
gix-filter/Cargo.toml+1−0
@@ -16,5 +16,6 @@ gix-hash = { version = "^0.11.3", path = "../gix-hash" } · 15 unmodified lines
1616 gix-trace = { version = "^0.1.1", path = "../gix-trace" }
1717 gix-object = { version = "^0.31.0", path = "../gix-object" }
1818
19+encoding_rs = "0.8.32"
1920 bstr = { version = "1.5.0", default-features = false, features = ["std"] }
2021 thiserror = "1.0.38"
gix-filter/src/ident.rs+43−25
@@ -1,9 +1,10 @@
1−use bstr::{BStr, ByteSlice, ByteVec};
2−use std::borrow::Cow;
1+use crate::clear_and_set_capacity;
2+use bstr::{ByteSlice, ByteVec};
33 use std::ops::Range;
44
5−/// Undo identifiers like `$Id:<hexsha>$` to `$Id$`. Newlines between dollars are ignored.
6−pub fn undo(mut input: Cow<'_, BStr>) -> Cow<'_, BStr> {
5+/// Undo identifiers like `$Id:<hexsha>$` to `$Id$` in `src` and write to `buf`. Newlines between dollars are ignored.
6+/// Return `true` if `buf` was written or `false` if `src` was left unaltered (as there was nothing to do).
7+pub fn undo(src: &[u8], buf: &mut Vec<u8>) -> bool {
78 fn find_range(input: &[u8]) -> Option<Range<usize>> {
89 let mut ofs = 0;
910 loop {
@@ -21,37 +22,54 @@ pub fn undo(mut input: Cow<'_, BStr>) -> Cow<'_, BStr> { · 11 unmodified lines
2122 }
2223
2324 let mut ofs = 0;
24− while let Some(range) = find_range(&input[ofs..]) {
25− input
26− .to_mut()
27− .replace_range((range.start + ofs)..(range.end + ofs), b"$Id$");
28− ofs += range.start + 4;
25+ let mut initialized = false;
26+ while let Some(range) = find_range(&src[ofs..]) {
27+ if !initialized {
28+ clear_and_set_capacity(buf, src.len());
29+ initialized = true;
30+ }
31+ buf.push_str(&src[ofs..][..range.start]);
32+ buf.push_str(b"$Id$");
33+ ofs += range.end;
34+ }
35+ if initialized {
36+ buf.push_str(&src[ofs..]);
2937 }
30− input
38+ initialized
3139 }
3240
33−/// Substitute all occurrences of `$Id$` with `$Id: <hexsha-of-input>$` if present and return the changed buffer, with `object_hash`
34−/// being used accordingly.
41+/// Substitute all occurrences of `$Id$` with `$Id: <hexsha-of-input>$` if present in `src` and write all changes to `buf`,
42+/// with `object_hash` being used accordingly. Return `true` if `buf` was written to or `false` if no change was made
43+/// (as there was nothing to do).
3544 ///
3645 /// ### Deviation
3746 ///
3847 /// `Git` also tries to cleanup 'stray' substituted `$Id: <hex>$`, but we don't do that, sticking exactly to what ought to be done.
3948 /// The respective code is up to 16 years old and one might assume that `git` by now handles checking and checkout filters correctly.
40−pub fn apply(mut input: Cow<'_, BStr>, object_hash: gix_hash::Kind) -> Cow<'_, BStr> {
41− let mut buf: [u8; b": $".len() + gix_hash::Kind::longest().len_in_hex()] = std::array::from_fn(|_| 0);
49+pub fn apply(src: &[u8], object_hash: gix_hash::Kind, buf: &mut Vec<u8>) -> bool {
50+ const HASH_LEN: usize = ": ".len() + gix_hash::Kind::longest().len_in_hex();
4251 let mut id = None;
4352 let mut ofs = 0;
44− while let Some(pos) = input[ofs..].find(b"$Id$") {
45− let id = id.get_or_insert_with(|| gix_object::compute_hash(object_hash, gix_object::Kind::Blob, &input));
53+ while let Some(pos) = src[ofs..].find(b"$Id$") {
54+ let id = match id {
55+ None => {
56+ let new_id = gix_object::compute_hash(object_hash, gix_object::Kind::Blob, src);
57+ id = new_id.into();
58+ clear_and_set_capacity(buf, src.len() + HASH_LEN); // pre-allocate for one ID
59+ new_id
60+ }
61+ Some(id) => id.to_owned(),
62+ };
4663
47− buf[..2].copy_from_slice(b": ");
48− let _ = id.hex_to_buf(&mut buf[2..][..object_hash.len_in_hex()]);
49− let replaced_id = &mut buf[..2 + object_hash.len_in_hex() + 1];
50− *replaced_id.last_mut().expect("present") = b'$';
51− input
52− .to_mut()
53− .replace_range((ofs + pos + 3)..(ofs + pos + 4), &*replaced_id);
54− ofs += pos + 3 + replaced_id.len();
64+ buf.push_str(&src[ofs..][..pos + 3]);
65+ buf.push_str(b": ");
66+ id.write_hex_to(&mut *buf).expect("writes to memory always work");
67+ buf.push(b'$');
68+
69+ ofs += pos + 4;
70+ }
71+ if id.is_some() {
72+ buf.push_str(&src[ofs..]);
5573 }
56− input
74+ id.is_some()
5775 }
gix-filter/src/lib.rs+4−2
@@ -8,8 +8,7 @@ · 7 unmodified lines
88 //!
99 //! This crate implements the building blocks in terms of applying and undoing filters, along with logic to decide whether
1010 //! or not to apply such a filter.
11−#![deny(rust_2018_idioms, missing_docs)]
12−#![forbid(unsafe_code)]
11+#![deny(rust_2018_idioms, missing_docs, unsafe_code)]
1312
1413 ///
1514 pub mod ident;
@@ -17,6 +16,9 @@ pub mod ident; · 1 unmodified line
1716 /// utilities related to handling line endings in buffers
1817 pub mod eol;
1918
19+/// Utilities for handling worktree encodings.
20+pub mod worktree;
21+
2022 fn clear_and_set_capacity(buf: &mut Vec<u8>, cap: usize) {
2123 buf.clear();
2224 if buf.capacity() < cap {
gix-filter/src/worktree/encode_to_git.rs+90−0
@@ -0,0 +1,90 @@
1+/// Whether or not to perform round-trip checks.
2+#[derive(Debug, Copy, Clone)]
3+pub enum RoundTrip {
4+ /// Assure that we can losslessly convert the UTF-8 result back to the original encoding.
5+ Validate,
6+ /// Do not check if the encoding is round-trippable.
7+ Ignore,
8+}
9+
10+/// The error returned by [`encode_to_git()][super::encode_to_git()].
11+#[derive(Debug, thiserror::Error)]
12+#[allow(missing_docs)]
13+pub enum Error {
14+ #[error("Cannot convert input of {input_len} bytes to UTF-8 without overflowing")]
15+ Overflow { input_len: usize },
16+ #[error("The input was malformed and could not be decoded as '{encoding}'")]
17+ Malformed { encoding: &'static str },
18+ #[error("Encoding from '{src_encoding}' to '{dest_encoding}' and back is not the same")]
19+ RoundTrip {
20+ src_encoding: &'static str,
21+ dest_encoding: &'static str,
22+ },
23+}
24+
25+pub(crate) mod function {
26+ use super::{Error, RoundTrip};
27+ use crate::clear_and_set_capacity;
28+ use encoding_rs::DecoderResult;
29+
30+ /// Decode `src` according to `src_encoding` to `UTF-8` for storage in git and place it in `buf`.
31+ /// Note that the encoding is always applied, there is no conditional even if `src_encoding` already is `UTF-8`.
32+ pub fn encode_to_git(
33+ src: &[u8],
34+ src_encoding: &'static encoding_rs::Encoding,
35+ buf: &mut Vec<u8>,
36+ round_trip: RoundTrip,
37+ ) -> Result<(), Error> {
38+ let mut decoder = src_encoding.new_decoder_with_bom_removal();
39+ let buf_len = decoder
40+ .max_utf8_buffer_length_without_replacement(src.len())
41+ .ok_or(Error::Overflow { input_len: src.len() })?;
42+ clear_and_set_capacity(buf, buf_len);
43+ // SAFETY: `clear_and_set_capacity` assure that we have the given `buf_len` allocated, so setting its length is only making available
44+ // what is allocated. Later we will truncate to the amount of actually written bytes.
45+ #[allow(unsafe_code)]
46+ unsafe {
47+ buf.set_len(buf_len);
48+ }
49+ let (res, read, written) = decoder.decode_to_utf8_without_replacement(src, buf, true);
50+ match res {
51+ DecoderResult::InputEmpty => {
52+ assert!(
53+ buf_len >= written,
54+ "encoding_rs estimates the maximum amount of bytes written correctly"
55+ );
56+ assert_eq!(read, src.len(), "input buffer should be fully consumed");
57+ // SAFETY: we trust that `encoding_rs` reports this number correctly, and truncate everything else.
58+ #[allow(unsafe_code)]
59+ unsafe {
60+ buf.set_len(written);
61+ }
62+ }
63+ DecoderResult::OutputFull => {
64+ unreachable!("we assure that the output buffer is big enough as per the encoder's estimate")
65+ }
66+ DecoderResult::Malformed(_, _) => {
67+ return Err(Error::Malformed {
68+ encoding: src_encoding.name(),
69+ })
70+ }
71+ }
72+
73+ match round_trip {
74+ RoundTrip::Validate => {
75+ // SAFETY: we trust `encoding_rs` to output valid UTF-8 only if we ask it to.
76+ #[allow(unsafe_code)]
77+ let str = unsafe { std::str::from_utf8_unchecked(buf) };
78+ let (should_equal_src, _actual_encoding, _had_errors) = src_encoding.encode(str);
79+ if should_equal_src != src {
80+ return Err(Error::RoundTrip {
81+ src_encoding: src_encoding.name(),
82+ dest_encoding: "UTF-8",
83+ });
84+ }
85+ }
86+ RoundTrip::Ignore => {}
87+ }
88+ Ok(())
89+ }
90+}
gix-filter/src/worktree/encode_to_worktree.rs+69−0
@@ -0,0 +1,69 @@
1+/// The error returned by [`encode_to_worktree()][super::encode_to_worktree()].
2+#[derive(Debug, thiserror::Error)]
3+#[allow(missing_docs)]
4+pub enum Error {
5+ #[error("Cannot convert input of {input_len} UTF-8 bytes to target encoding without overflowing")]
6+ Overflow { input_len: usize },
7+ #[error("Input was not UTF-8 encoded")]
8+ InputAsUtf8(#[from] std::str::Utf8Error),
9+ #[error("The character '{character}' could not be mapped to the {worktree_encoding}")]
10+ Unmappable {
11+ character: char,
12+ worktree_encoding: &'static str,
13+ },
14+}
15+
16+pub(crate) mod function {
17+ use super::Error;
18+ use crate::clear_and_set_capacity;
19+ use encoding_rs::EncoderResult;
20+
21+ /// Encode `src_utf8`, which is assumed to be UTF-8 encoded, according to `worktree_encoding` for placement in the working directory,
22+ /// and write it to `buf`, possibly resizing it.
23+ /// Note that the encoding is always applied, there is no conditional even if `worktree_encoding` and the `src` encoding are the same.
24+ pub fn encode_to_worktree(
25+ src_utf8: &[u8],
26+ worktree_encoding: &'static encoding_rs::Encoding,
27+ buf: &mut Vec<u8>,
28+ ) -> Result<(), Error> {
29+ let mut encoder = worktree_encoding.new_encoder();
30+ let buf_len = encoder
31+ .max_buffer_length_from_utf8_if_no_unmappables(src_utf8.len())
32+ .ok_or(Error::Overflow {
33+ input_len: src_utf8.len(),
34+ })?;
35+ clear_and_set_capacity(buf, buf_len);
36+ // SAFETY: `clear_and_set_capacity` assure that we have the given `buf_len` allocated, so setting its length is only making available
37+ // what is allocated. Later we will truncate to the amount of actually written bytes.
38+ #[allow(unsafe_code)]
39+ unsafe {
40+ buf.set_len(buf_len);
41+ }
42+ let src = std::str::from_utf8(src_utf8)?;
43+ let (res, read, written) = encoder.encode_from_utf8_without_replacement(src, buf, true);
44+ match res {
45+ EncoderResult::InputEmpty => {
46+ assert!(
47+ buf_len >= written,
48+ "encoding_rs estimates the maximum amount of bytes written correctly"
49+ );
50+ assert_eq!(read, src_utf8.len(), "input buffer should be fully consumed");
51+ // SAFETY: we trust that `encoding_rs` reports this number correctly, and truncate everything else.
52+ #[allow(unsafe_code)]
53+ unsafe {
54+ buf.set_len(written);
55+ }
56+ }
57+ EncoderResult::OutputFull => {
58+ unreachable!("we assure that the output buffer is big enough as per the encoder's estimate")
59+ }
60+ EncoderResult::Unmappable(c) => {
61+ return Err(Error::Unmappable {
62+ worktree_encoding: worktree_encoding.name(),
63+ character: c,
64+ })
65+ }
66+ }
67+ Ok(())
68+ }
69+}
gix-filter/src/worktree/encoding.rs+31−0
@@ -0,0 +1,31 @@
1+use bstr::BStr;
2+use encoding_rs::Encoding;
3+
4+///
5+pub mod for_label {
6+ use bstr::BString;
7+
8+ /// The error returned by [for_label()][super::for_label()].
9+ #[derive(Debug, thiserror::Error)]
10+ #[allow(missing_docs)]
11+ pub enum Error {
12+ #[error("An encoding named '{name}' is not known")]
13+ Unknown { name: BString },
14+ }
15+}
16+
17+/// Try to produce a new `Encoding` for `label` or report an error if it is not known.
18+///
19+/// ### Deviation
20+///
21+/// * There is no special handling of UTF-16LE/BE with checks if data contains a BOM or not, like `git` as we don't expect to have
22+/// data available here.
23+/// * Special `-BOM` suffixed versions of `UTF-16` encodings are not supported.
24+pub fn for_label<'a>(label: impl Into<&'a BStr>) -> Result<&'static Encoding, for_label::Error> {
25+ let mut label = label.into();
26+ if label == "latin-1" {
27+ label = "ISO-8859-1".into();
28+ }
29+ let enc = Encoding::for_label(label.as_ref()).ok_or_else(|| for_label::Error::Unknown { name: label.into() })?;
30+ Ok(enc)
31+}
gix-filter/src/worktree/mod.rs+16−0
@@ -0,0 +1,16 @@
1+//! Worktree encodings are powered by the `encoding_rs` crate, which has a narrower focus than the `iconv` library. Thus this implementation
2+//! is inherently more limited but will handle the common cases.
3+//!
4+//! Note that for encoding to legacy formats, [additional normalization steps](https://docs.rs/encoding_rs/0.8.32/encoding_rs/#preparing-text-for-the-encoders)
5+//! can be taken, which we do not yet take unless there is specific examples or problems to solve.
6+
7+///
8+pub mod encoding;
9+
10+///
11+pub mod encode_to_git;
12+pub use encode_to_git::function::encode_to_git;
13+
14+///
15+pub mod encode_to_worktree;
16+pub use encode_to_worktree::function::encode_to_worktree;
gix-filter/tests/filter.rs+1−0
@@ -1,4 +1,5 @@
11 mod eol;
22 mod ident;
3+mod worktree;
34
45 pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
gix-filter/tests/ident/mod.rs+49−45
@@ -1,107 +1,111 @@
1−use bstr::BStr;
2−use std::borrow::Cow;
3−
4−fn cowstr(input: &str) -> Cow<'_, BStr> {
5− Cow::Borrowed(input.into())
6−}
71 mod undo {
8− use crate::ident::cowstr;
9− use std::borrow::Cow;
2+ use bstr::{ByteSlice, B};
103
114 #[test]
125 fn no_id_changes_nothing() {
13− let cow = gix_filter::ident::undo(cowstr("hello"));
14− assert!(matches!(cow, Cow::Borrowed(_)), "the buffer is not touched");
15− assert_eq!(cow.as_ref(), "hello");
6+ let mut buf = Vec::new();
7+ let changed = gix_filter::ident::undo(B("hello"), &mut buf);
8+ assert!(!changed, "the buffer is not touched");
9+ assert_eq!(buf.len(), 0);
1610 }
1711
1812 #[test]
1913 fn empty() {
20− assert!(matches!(gix_filter::ident::undo(cowstr("")), Cow::Borrowed(_)));
14+ let mut buf = Vec::new();
15+ assert!(
16+ !gix_filter::ident::undo(B(""), &mut buf),
17+ "nothing to be done in empty buffer"
18+ );
2119 }
2220
2321 #[test]
2422 fn nothing_if_newline_between_dollars() {
25− assert!(matches!(gix_filter::ident::undo(cowstr(" $Id: \n$")), Cow::Borrowed(_)));
23+ let mut buf = Vec::new();
24+ assert!(!gix_filter::ident::undo(B(" $Id: \n$"), &mut buf));
25+ assert_eq!(buf.len(), 0);
2626 }
2727
2828 #[test]
2929 fn nothing_if_it_is_not_id() {
30+ let mut buf = Vec::new();
3031 assert!(
31− matches!(gix_filter::ident::undo(cowstr(" $id: something$")), Cow::Borrowed(_)),
32+ !gix_filter::ident::undo(B(" $id: something$"), &mut buf),
3233 "it's matching case-sensitively"
3334 );
35+ assert_eq!(buf.len(), 0);
3436 }
3537
3638 #[test]
3739 fn anything_between_dollar_id_dollar() {
38− assert_eq!(
39− gix_filter::ident::undo(cowstr(" $Id: something$\nhello")).as_ref(),
40− " $Id$\nhello"
41− );
40+ let mut buf = Vec::new();
41+ assert!(gix_filter::ident::undo(B(" $Id: something$\nhello"), &mut buf));
42+ assert_eq!(buf.as_bstr(), " $Id$\nhello");
4243 }
4344
4445 #[test]
4546 fn multiple() {
46− assert_eq!(
47− gix_filter::ident::undo(cowstr(
48− "$Id: a\n$ $Id: something$\nhello$Id: hex$\nlast $Id:other$\n$Id: \n$"
49− ))
50− .as_ref(),
51− "$Id: a\n$ $Id$\nhello$Id$\nlast $Id$\n$Id: \n$",
52− );
53− assert_eq!(
54− gix_filter::ident::undo(cowstr("$Id: a\n$$Id:$$Id: hex$\n$Id:other$$Id: $end")).as_ref(),
55− "$Id: a\n$$Id$$Id$\n$Id$$Id$end",
56− );
47+ let mut buf = Vec::new();
48+ assert!(gix_filter::ident::undo(
49+ B("$Id: a\n$ $Id: something$\nhello$Id: hex$\nlast $Id:other$\n$Id: \n$"),
50+ &mut buf
51+ ));
52+ assert_eq!(buf.as_bstr(), "$Id: a\n$ $Id$\nhello$Id$\nlast $Id$\n$Id: \n$");
53+
54+ assert!(gix_filter::ident::undo(
55+ B("$Id: a\n$$Id:$$Id: hex$\n$Id:other$$Id: $end"),
56+ &mut buf
57+ ));
58+ assert_eq!(buf.as_bstr(), "$Id: a\n$$Id$$Id$\n$Id$$Id$end");
5759 }
5860 }
5961
6062 mod apply {
61− use crate::ident::cowstr;
63+ use bstr::{ByteSlice, B};
6264 use gix_filter::ident;
63− use std::borrow::Cow;
6465
6566 #[test]
6667 fn no_change() {
68+ let mut buf = Vec::new();
6769 for input_no_match in [
6870 "",
6971 "nothing",
7072 "$ID$ case sensitive matching",
7173 "$Id: expanded is ignored$",
7274 ] {
73− let res = ident::apply(cowstr(input_no_match), gix_hash::Kind::Sha1);
74− assert!(
75− matches!(res, Cow::Borrowed(_)),
76− "no substitution happens, so no mutable version of the Cow is created"
77− );
78− assert_eq!(res.as_ref(), input_no_match, "there definitely is no change");
75+ let changed = ident::apply(input_no_match.as_bytes(), gix_hash::Kind::Sha1, &mut buf);
76+ assert!(!changed, "no substitution happens, nothing to do");
77+ assert_eq!(buf.len(), 0);
7978 }
8079 }
8180
8281 #[test]
8382 fn simple() {
84− assert_eq!(
85− ident::apply(cowstr("$Id$"), gix_hash::Kind::Sha1).as_ref(),
86− "$Id: b3f5ebfb5843bc43ceecff6d4f26bb37c615beb1$"
83+ let mut buf = Vec::new();
84+ assert!(
85+ ident::apply(B("$Id$"), gix_hash::Kind::Sha1, &mut buf),
86+ "a change happens"
8787 );
88+ assert_eq!(buf.as_bstr(), "$Id: b3f5ebfb5843bc43ceecff6d4f26bb37c615beb1$");
8889
90+ assert!(ident::apply(B("$Id$ $Id$ foo"), gix_hash::Kind::Sha1, &mut buf));
8991 assert_eq!(
90− ident::apply(cowstr("$Id$ $Id$"), gix_hash::Kind::Sha1).as_ref(),
91− "$Id: f6f3176060328ef7030a8b8eeda57fbf0587b2f9$ $Id: f6f3176060328ef7030a8b8eeda57fbf0587b2f9$"
92+ buf.as_bstr(),
93+ "$Id: e230cff7a9624f59eaa28bfb97602c3a03651a49$ $Id: e230cff7a9624f59eaa28bfb97602c3a03651a49$ foo"
9294 );
9395 }
9496
9597 #[test]
9698 fn round_trips() {
99+ let mut buf = Vec::new();
97100 for input in [
98101 "hi\n$Id$\nho\n\t$Id$$Id$$Id$",
99102 "$Id$",
100103 "$Id$ and one more $Id$ and done",
101104 ] {
102− let res = ident::apply(cowstr(input), gix_hash::Kind::Sha1);
103− assert_ne!(res.as_ref(), input, "the input was rewritten");
104− assert_eq!(ident::undo(res).as_ref(), input, "the filter can be undone perfectly");
105+ let changed = ident::apply(B(input), gix_hash::Kind::Sha1, &mut buf);
106+ assert!(changed, "the input was rewritten");
107+ assert!(ident::undo(&buf.clone(), &mut buf), "undo does something as well");
108+ assert_eq!(buf.as_bstr(), input, "the filter can be undone perfectly");
105109 }
106110 }
107111 }
gix-filter/tests/worktree/mod.rs+101−0
@@ -0,0 +1,101 @@
1+mod encoding {
2+ mod for_label {
3+ use gix_filter::worktree;
4+
5+ #[test]
6+ fn unknown() {
7+ assert_eq!(
8+ worktree::encoding::for_label("FOO").unwrap_err().to_string(),
9+ "An encoding named 'FOO' is not known"
10+ );
11+ }
12+
13+ #[test]
14+ fn utf32_is_not_supported() {
15+ for enc in ["UTF-32BE", "UTF-32LE", "UTF-32", "UTF-32LE-BOM", "UTF-32BE-BOM"] {
16+ assert!(
17+ matches!(
18+ worktree::encoding::for_label(enc).unwrap_err(),
19+ worktree::encoding::for_label::Error::Unknown { .. }
20+ ),
21+ "it's not needed for the web and this crate is meant for use in firefox"
22+ );
23+ }
24+ }
25+
26+ #[test]
27+ fn various_spellings_of_utf_8_are_supported() {
28+ for enc in ["UTF8", "UTF-8", "utf-8", "utf8"] {
29+ let enc = worktree::encoding::for_label(enc).unwrap();
30+ assert_eq!(enc.name(), "UTF-8");
31+ }
32+ }
33+
34+ #[test]
35+ fn various_utf_16_without_bom_suffix_are_supported() {
36+ for label in ["UTF-16BE", "UTF-16LE"] {
37+ let enc = worktree::encoding::for_label(label).unwrap();
38+ assert_eq!(enc.name(), label);
39+ }
40+ }
41+
42+ #[test]
43+ fn various_utf_16_with_bom_suffix_are_unsupported() {
44+ for label in ["UTF-16BE-BOM", "UTF-16LE-BOM"] {
45+ assert!(
46+ matches!(
47+ worktree::encoding::for_label(label).unwrap_err(),
48+ worktree::encoding::for_label::Error::Unknown { .. }
49+ ),
50+ "git supports these and has special handling, but we have not for now. Git has no tests for that either."
51+ );
52+ }
53+ }
54+
55+ #[test]
56+ fn latin_1_is_supported_with_fallback() {
57+ let enc = worktree::encoding::for_label("latin-1").unwrap();
58+ assert_eq!(
59+ enc.name(),
60+ "windows-1252",
61+ "the encoding crate has its own fallback for ISO-8859-1 which we try to use"
62+ );
63+ }
64+ }
65+}
66+
67+mod encode_to_git {
68+ use bstr::ByteSlice;
69+ use gix_filter::worktree;
70+ use gix_filter::worktree::encode_to_git::RoundTrip;
71+
72+ #[test]
73+ fn simple() -> crate::Result {
74+ let input = &b"hello"[..];
75+ for round_trip in [RoundTrip::Ignore, RoundTrip::Validate] {
76+ let mut buf = Vec::new();
77+ worktree::encode_to_git(input, encoding_rs::UTF_8, &mut buf, round_trip)?;
78+ assert_eq!(buf.as_bstr(), input)
79+ }
80+ Ok(())
81+ }
82+}
83+
84+mod encode_to_worktree {
85+ use bstr::ByteSlice;
86+ use gix_filter::worktree;
87+ use gix_filter::worktree::encode_to_git::RoundTrip;
88+
89+ #[test]
90+ fn shift_jis() -> crate::Result {
91+ let input = "ハローワールド";
92+ let mut buf = Vec::new();
93+ worktree::encode_to_worktree(input.as_bytes(), encoding_rs::SHIFT_JIS, &mut buf)?;
94+
95+ let mut re_encoded = Vec::new();
96+ worktree::encode_to_git(&buf, encoding_rs::SHIFT_JIS, &mut re_encoded, RoundTrip::Validate)?;
97+
98+ assert_eq!(re_encoded.as_bstr(), input, "this should be round-trippable too");
99+ Ok(())
100+ }
101+}