Unified · Split
| @@ -1650,6 +1650,7 @@ name = "gix-filter" · 1649 unmodified lines | |||
| 1650 | version = "0.0.0" | 1650 | version = "0.0.0" |
| 1651 | dependencies = [ | 1651 | dependencies = [ |
| 1652 | "bstr", | 1652 | "bstr", |
| 1653 | "encoding_rs", | ||
| 1653 | "gix-hash 0.11.3", | 1654 | "gix-hash 0.11.3", |
| 1654 | "gix-object 0.31.0", | 1655 | "gix-object 0.31.0", |
| 1655 | "gix-trace 0.1.1", | 1656 | "gix-trace 0.1.1", |
| @@ -16,5 +16,6 @@ gix-hash = { version = "^0.11.3", path = "../gix-hash" } · 15 unmodified lines | |||
| 16 | gix-trace = { version = "^0.1.1", path = "../gix-trace" } | 16 | gix-trace = { version = "^0.1.1", path = "../gix-trace" } |
| 17 | gix-object = { version = "^0.31.0", path = "../gix-object" } | 17 | gix-object = { version = "^0.31.0", path = "../gix-object" } |
| 18 | 18 | ||
| 19 | encoding_rs = "0.8.32" | ||
| 19 | bstr = { version = "1.5.0", default-features = false, features = ["std"] } | 20 | bstr = { version = "1.5.0", default-features = false, features = ["std"] } |
| 20 | thiserror = "1.0.38" | 21 | thiserror = "1.0.38" |
| @@ -1,9 +1,10 @@ | |||
| 1 | use bstr::{BStr, ByteSlice, ByteVec}; | 1 | use crate::clear_and_set_capacity; |
| 2 | use std::borrow::Cow; | 2 | use bstr::{ByteSlice, ByteVec}; |
| 3 | use std::ops::Range; | 3 | use std::ops::Range; |
| 4 | 4 | ||
| 5 | /// Undo identifiers like `$Id:<hexsha>$` to `$Id$`. Newlines between dollars are ignored. | 5 | /// Undo identifiers like `$Id:<hexsha>$` to `$Id$` in `src` and write to `buf`. Newlines between dollars are ignored. |
| 6 | pub fn undo(mut input: Cow<'_, BStr>) -> Cow<'_, BStr> { | 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 { | ||
| 7 | fn find_range(input: &[u8]) -> Option<Range<usize>> { | 8 | fn find_range(input: &[u8]) -> Option<Range<usize>> { |
| 8 | let mut ofs = 0; | 9 | let mut ofs = 0; |
| 9 | loop { | 10 | loop { |
| @@ -21,37 +22,54 @@ pub fn undo(mut input: Cow<'_, BStr>) -> Cow<'_, BStr> { · 11 unmodified lines | |||
| 21 | } | 22 | } |
| 22 | 23 | ||
| 23 | let mut ofs = 0; | 24 | let mut ofs = 0; |
| 24 | while let Some(range) = find_range(&input[ofs..]) { | 25 | let mut initialized = false; |
| 25 | input | 26 | while let Some(range) = find_range(&src[ofs..]) { |
| 26 | .to_mut() | 27 | if !initialized { |
| 27 | .replace_range((range.start + ofs)..(range.end + ofs), b"$Id$"); | 28 | clear_and_set_capacity(buf, src.len()); |
| 28 | ofs += range.start + 4; | 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..]); | ||
| 29 | } | 37 | } |
| 30 | input | 38 | initialized |
| 31 | } | 39 | } |
| 32 | 40 | ||
| 33 | /// Substitute all occurrences of `$Id$` with `$Id: <hexsha-of-input>$` if present and return the changed buffer, with `object_hash` | 41 | /// Substitute all occurrences of `$Id$` with `$Id: <hexsha-of-input>$` if present in `src` and write all changes to `buf`, |
| 34 | /// being used accordingly. | 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). | ||
| 35 | /// | 44 | /// |
| 36 | /// ### Deviation | 45 | /// ### Deviation |
| 37 | /// | 46 | /// |
| 38 | /// `Git` also tries to cleanup 'stray' substituted `$Id: <hex>$`, but we don't do that, sticking exactly to what ought to be done. | 47 | /// `Git` also tries to cleanup 'stray' substituted `$Id: <hex>$`, but we don't do that, sticking exactly to what ought to be done. |
| 39 | /// The respective code is up to 16 years old and one might assume that `git` by now handles checking and checkout filters correctly. | 48 | /// 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> { | 49 | pub fn apply(src: &[u8], object_hash: gix_hash::Kind, buf: &mut Vec<u8>) -> bool { |
| 41 | let mut buf: [u8; b": $".len() + gix_hash::Kind::longest().len_in_hex()] = std::array::from_fn(|_| 0); | 50 | const HASH_LEN: usize = ": ".len() + gix_hash::Kind::longest().len_in_hex(); |
| 42 | let mut id = None; | 51 | let mut id = None; |
| 43 | let mut ofs = 0; | 52 | let mut ofs = 0; |
| 44 | while let Some(pos) = input[ofs..].find(b"$Id$") { | 53 | while let Some(pos) = src[ofs..].find(b"$Id$") { |
| 45 | let id = id.get_or_insert_with(|| gix_object::compute_hash(object_hash, gix_object::Kind::Blob, &input)); | 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 | }; | ||
| 46 | 63 | ||
| 47 | buf[..2].copy_from_slice(b": "); | 64 | buf.push_str(&src[ofs..][..pos + 3]); |
| 48 | let _ = id.hex_to_buf(&mut buf[2..][..object_hash.len_in_hex()]); | 65 | buf.push_str(b": "); |
| 49 | let replaced_id = &mut buf[..2 + object_hash.len_in_hex() + 1]; | 66 | id.write_hex_to(&mut *buf).expect("writes to memory always work"); |
| 50 | *replaced_id.last_mut().expect("present") = b'$'; | 67 | buf.push(b'$'); |
| 51 | input | 68 | |
| 52 | .to_mut() | 69 | ofs += pos + 4; |
| 53 | .replace_range((ofs + pos + 3)..(ofs + pos + 4), &*replaced_id); | 70 | } |
| 54 | ofs += pos + 3 + replaced_id.len(); | 71 | if id.is_some() { |
| 72 | buf.push_str(&src[ofs..]); | ||
| 55 | } | 73 | } |
| 56 | input | 74 | id.is_some() |
| 57 | } | 75 | } |
| @@ -8,8 +8,7 @@ · 7 unmodified lines | |||
| 8 | //! | 8 | //! |
| 9 | //! This crate implements the building blocks in terms of applying and undoing filters, along with logic to decide whether | 9 | //! This crate implements the building blocks in terms of applying and undoing filters, along with logic to decide whether |
| 10 | //! or not to apply such a filter. | 10 | //! or not to apply such a filter. |
| 11 | #![deny(rust_2018_idioms, missing_docs)] | 11 | #![deny(rust_2018_idioms, missing_docs, unsafe_code)] |
| 12 | #![forbid(unsafe_code)] | ||
| 13 | 12 | ||
| 14 | /// | 13 | /// |
| 15 | pub mod ident; | 14 | pub mod ident; |
| @@ -17,6 +16,9 @@ pub mod ident; · 1 unmodified line | |||
| 17 | /// utilities related to handling line endings in buffers | 16 | /// utilities related to handling line endings in buffers |
| 18 | pub mod eol; | 17 | pub mod eol; |
| 19 | 18 | ||
| 19 | /// Utilities for handling worktree encodings. | ||
| 20 | pub mod worktree; | ||
| 21 | |||
| 20 | fn clear_and_set_capacity(buf: &mut Vec<u8>, cap: usize) { | 22 | fn clear_and_set_capacity(buf: &mut Vec<u8>, cap: usize) { |
| 21 | buf.clear(); | 23 | buf.clear(); |
| 22 | if buf.capacity() < cap { | 24 | if buf.capacity() < cap { |
| @@ -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 | } | ||
| @@ -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 | } | ||
| @@ -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 | } | ||
| @@ -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; | ||
| @@ -1,4 +1,5 @@ | |||
| 1 | mod eol; | 1 | mod eol; |
| 2 | mod ident; | 2 | mod ident; |
| 3 | mod worktree; | ||
| 3 | 4 | ||
| 4 | pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>; | 5 | pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>; |
| @@ -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 | } | ||
| 7 | mod undo { | 1 | mod undo { |
| 8 | use crate::ident::cowstr; | 2 | use bstr::{ByteSlice, B}; |
| 9 | use std::borrow::Cow; | ||
| 10 | 3 | ||
| 11 | #[test] | 4 | #[test] |
| 12 | fn no_id_changes_nothing() { | 5 | fn no_id_changes_nothing() { |
| 13 | let cow = gix_filter::ident::undo(cowstr("hello")); | 6 | let mut buf = Vec::new(); |
| 14 | assert!(matches!(cow, Cow::Borrowed(_)), "the buffer is not touched"); | 7 | let changed = gix_filter::ident::undo(B("hello"), &mut buf); |
| 15 | assert_eq!(cow.as_ref(), "hello"); | 8 | assert!(!changed, "the buffer is not touched"); |
| 9 | assert_eq!(buf.len(), 0); | ||
| 16 | } | 10 | } |
| 17 | 11 | ||
| 18 | #[test] | 12 | #[test] |
| 19 | fn empty() { | 13 | 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 | ); | ||
| 21 | } | 19 | } |
| 22 | 20 | ||
| 23 | #[test] | 21 | #[test] |
| 24 | fn nothing_if_newline_between_dollars() { | 22 | 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); | ||
| 26 | } | 26 | } |
| 27 | 27 | ||
| 28 | #[test] | 28 | #[test] |
| 29 | fn nothing_if_it_is_not_id() { | 29 | fn nothing_if_it_is_not_id() { |
| 30 | let mut buf = Vec::new(); | ||
| 30 | assert!( | 31 | assert!( |
| 31 | matches!(gix_filter::ident::undo(cowstr(" $id: something$")), Cow::Borrowed(_)), | 32 | !gix_filter::ident::undo(B(" $id: something$"), &mut buf), |
| 32 | "it's matching case-sensitively" | 33 | "it's matching case-sensitively" |
| 33 | ); | 34 | ); |
| 35 | assert_eq!(buf.len(), 0); | ||
| 34 | } | 36 | } |
| 35 | 37 | ||
| 36 | #[test] | 38 | #[test] |
| 37 | fn anything_between_dollar_id_dollar() { | 39 | fn anything_between_dollar_id_dollar() { |
| 38 | assert_eq!( | 40 | let mut buf = Vec::new(); |
| 39 | gix_filter::ident::undo(cowstr(" $Id: something$\nhello")).as_ref(), | 41 | assert!(gix_filter::ident::undo(B(" $Id: something$\nhello"), &mut buf)); |
| 40 | " $Id$\nhello" | 42 | assert_eq!(buf.as_bstr(), " $Id$\nhello"); |
| 41 | ); | ||
| 42 | } | 43 | } |
| 43 | 44 | ||
| 44 | #[test] | 45 | #[test] |
| 45 | fn multiple() { | 46 | fn multiple() { |
| 46 | assert_eq!( | 47 | let mut buf = Vec::new(); |
| 47 | gix_filter::ident::undo(cowstr( | 48 | assert!(gix_filter::ident::undo( |
| 48 | "$Id: a\n$ $Id: something$\nhello$Id: hex$\nlast $Id:other$\n$Id: \n$" | 49 | B("$Id: a\n$ $Id: something$\nhello$Id: hex$\nlast $Id:other$\n$Id: \n$"), |
| 49 | )) | 50 | &mut buf |
| 50 | .as_ref(), | 51 | )); |
| 51 | "$Id: a\n$ $Id$\nhello$Id$\nlast $Id$\n$Id: \n$", | 52 | assert_eq!(buf.as_bstr(), "$Id: a\n$ $Id$\nhello$Id$\nlast $Id$\n$Id: \n$"); |
| 52 | ); | 53 | |
| 53 | assert_eq!( | 54 | assert!(gix_filter::ident::undo( |
| 54 | gix_filter::ident::undo(cowstr("$Id: a\n$$Id:$$Id: hex$\n$Id:other$$Id: $end")).as_ref(), | 55 | B("$Id: a\n$$Id:$$Id: hex$\n$Id:other$$Id: $end"), |
| 55 | "$Id: a\n$$Id$$Id$\n$Id$$Id$end", | 56 | &mut buf |
| 56 | ); | 57 | )); |
| 58 | assert_eq!(buf.as_bstr(), "$Id: a\n$$Id$$Id$\n$Id$$Id$end"); | ||
| 57 | } | 59 | } |
| 58 | } | 60 | } |
| 59 | 61 | ||
| 60 | mod apply { | 62 | mod apply { |
| 61 | use crate::ident::cowstr; | 63 | use bstr::{ByteSlice, B}; |
| 62 | use gix_filter::ident; | 64 | use gix_filter::ident; |
| 63 | use std::borrow::Cow; | ||
| 64 | 65 | ||
| 65 | #[test] | 66 | #[test] |
| 66 | fn no_change() { | 67 | fn no_change() { |
| 68 | let mut buf = Vec::new(); | ||
| 67 | for input_no_match in [ | 69 | for input_no_match in [ |
| 68 | "", | 70 | "", |
| 69 | "nothing", | 71 | "nothing", |
| 70 | "$ID$ case sensitive matching", | 72 | "$ID$ case sensitive matching", |
| 71 | "$Id: expanded is ignored$", | 73 | "$Id: expanded is ignored$", |
| 72 | ] { | 74 | ] { |
| 73 | let res = ident::apply(cowstr(input_no_match), gix_hash::Kind::Sha1); | 75 | let changed = ident::apply(input_no_match.as_bytes(), gix_hash::Kind::Sha1, &mut buf); |
| 74 | assert!( | 76 | assert!(!changed, "no substitution happens, nothing to do"); |
| 75 | matches!(res, Cow::Borrowed(_)), | 77 | assert_eq!(buf.len(), 0); |
| 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"); | ||
| 79 | } | 78 | } |
| 80 | } | 79 | } |
| 81 | 80 | ||
| 82 | #[test] | 81 | #[test] |
| 83 | fn simple() { | 82 | fn simple() { |
| 84 | assert_eq!( | 83 | let mut buf = Vec::new(); |
| 85 | ident::apply(cowstr("$Id$"), gix_hash::Kind::Sha1).as_ref(), | 84 | assert!( |
| 86 | "$Id: b3f5ebfb5843bc43ceecff6d4f26bb37c615beb1$" | 85 | ident::apply(B("$Id$"), gix_hash::Kind::Sha1, &mut buf), |
| 86 | "a change happens" | ||
| 87 | ); | 87 | ); |
| 88 | assert_eq!(buf.as_bstr(), "$Id: b3f5ebfb5843bc43ceecff6d4f26bb37c615beb1$"); | ||
| 88 | 89 | ||
| 90 | assert!(ident::apply(B("$Id$ $Id$ foo"), gix_hash::Kind::Sha1, &mut buf)); | ||
| 89 | assert_eq!( | 91 | assert_eq!( |
| 90 | ident::apply(cowstr("$Id$ $Id$"), gix_hash::Kind::Sha1).as_ref(), | 92 | buf.as_bstr(), |
| 91 | "$Id: f6f3176060328ef7030a8b8eeda57fbf0587b2f9$ $Id: f6f3176060328ef7030a8b8eeda57fbf0587b2f9$" | 93 | "$Id: e230cff7a9624f59eaa28bfb97602c3a03651a49$ $Id: e230cff7a9624f59eaa28bfb97602c3a03651a49$ foo" |
| 92 | ); | 94 | ); |
| 93 | } | 95 | } |
| 94 | 96 | ||
| 95 | #[test] | 97 | #[test] |
| 96 | fn round_trips() { | 98 | fn round_trips() { |
| 99 | let mut buf = Vec::new(); | ||
| 97 | for input in [ | 100 | for input in [ |
| 98 | "hi\n$Id$\nho\n\t$Id$$Id$$Id$", | 101 | "hi\n$Id$\nho\n\t$Id$$Id$$Id$", |
| 99 | "$Id$", | 102 | "$Id$", |
| 100 | "$Id$ and one more $Id$ and done", | 103 | "$Id$ and one more $Id$ and done", |
| 101 | ] { | 104 | ] { |
| 102 | let res = ident::apply(cowstr(input), gix_hash::Kind::Sha1); | 105 | let changed = ident::apply(B(input), gix_hash::Kind::Sha1, &mut buf); |
| 103 | assert_ne!(res.as_ref(), input, "the input was rewritten"); | 106 | assert!(changed, "the input was rewritten"); |
| 104 | assert_eq!(ident::undo(res).as_ref(), input, "the filter can be undone perfectly"); | 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"); | ||
| 105 | } | 109 | } |
| 106 | } | 110 | } |
| 107 | } | 111 | } |
| @@ -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 | } | ||