Unified · Split
| @@ -1650,6 +1650,7 @@ name = "gix-filter" · 1649 unmodified lines | ||
| 1650 | 1650 | version = "0.0.0" |
| 1651 | 1651 | dependencies = [ |
| 1652 | 1652 | "bstr", |
| 1653 | + "encoding_rs", | |
| 1653 | 1654 | "gix-hash 0.11.3", |
| 1654 | 1655 | "gix-object 0.31.0", |
| 1655 | 1656 | "gix-trace 0.1.1", |
| @@ -16,5 +16,6 @@ gix-hash = { version = "^0.11.3", path = "../gix-hash" } · 15 unmodified lines | ||
| 16 | 16 | gix-trace = { version = "^0.1.1", path = "../gix-trace" } |
| 17 | 17 | gix-object = { version = "^0.31.0", path = "../gix-object" } |
| 18 | 18 | |
| 19 | +encoding_rs = "0.8.32" | |
| 19 | 20 | bstr = { version = "1.5.0", default-features = false, features = ["std"] } |
| 20 | 21 | thiserror = "1.0.38" |
| @@ -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}; | |
| 3 | 3 | use std::ops::Range; |
| 4 | 4 | |
| 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 { | |
| 7 | 8 | fn find_range(input: &[u8]) -> Option<Range<usize>> { |
| 8 | 9 | let mut ofs = 0; |
| 9 | 10 | loop { |
| @@ -21,37 +22,54 @@ pub fn undo(mut input: Cow<'_, BStr>) -> Cow<'_, BStr> { · 11 unmodified lines | ||
| 21 | 22 | } |
| 22 | 23 | |
| 23 | 24 | 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..]); | |
| 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` | |
| 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). | |
| 35 | 44 | /// |
| 36 | 45 | /// ### Deviation |
| 37 | 46 | /// |
| 38 | 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 | 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> { | |
| 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(); | |
| 42 | 51 | let mut id = None; |
| 43 | 52 | 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 | + }; | |
| 46 | 63 | |
| 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..]); | |
| 55 | 73 | } |
| 56 | − input | |
| 74 | + id.is_some() | |
| 57 | 75 | } |
| @@ -8,8 +8,7 @@ · 7 unmodified lines | ||
| 8 | 8 | //! |
| 9 | 9 | //! This crate implements the building blocks in terms of applying and undoing filters, along with logic to decide whether |
| 10 | 10 | //! 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)] | |
| 13 | 12 | |
| 14 | 13 | /// |
| 15 | 14 | pub mod ident; |
| @@ -17,6 +16,9 @@ pub mod ident; · 1 unmodified line | ||
| 17 | 16 | /// utilities related to handling line endings in buffers |
| 18 | 17 | pub mod eol; |
| 19 | 18 | |
| 19 | +/// Utilities for handling worktree encodings. | |
| 20 | +pub mod worktree; | |
| 21 | + | |
| 20 | 22 | fn clear_and_set_capacity(buf: &mut Vec<u8>, cap: usize) { |
| 21 | 23 | buf.clear(); |
| 22 | 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 | 1 | mod eol; |
| 2 | 2 | mod ident; |
| 3 | +mod worktree; | |
| 3 | 4 | |
| 4 | 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 | 1 | mod undo { |
| 8 | − use crate::ident::cowstr; | |
| 9 | − use std::borrow::Cow; | |
| 2 | + use bstr::{ByteSlice, B}; | |
| 10 | 3 | |
| 11 | 4 | #[test] |
| 12 | 5 | 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); | |
| 16 | 10 | } |
| 17 | 11 | |
| 18 | 12 | #[test] |
| 19 | 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 | 21 | #[test] |
| 24 | 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 | 28 | #[test] |
| 29 | 29 | fn nothing_if_it_is_not_id() { |
| 30 | + let mut buf = Vec::new(); | |
| 30 | 31 | assert!( |
| 31 | − matches!(gix_filter::ident::undo(cowstr(" $id: something$")), Cow::Borrowed(_)), | |
| 32 | + !gix_filter::ident::undo(B(" $id: something$"), &mut buf), | |
| 32 | 33 | "it's matching case-sensitively" |
| 33 | 34 | ); |
| 35 | + assert_eq!(buf.len(), 0); | |
| 34 | 36 | } |
| 35 | 37 | |
| 36 | 38 | #[test] |
| 37 | 39 | 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"); | |
| 42 | 43 | } |
| 43 | 44 | |
| 44 | 45 | #[test] |
| 45 | 46 | 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"); | |
| 57 | 59 | } |
| 58 | 60 | } |
| 59 | 61 | |
| 60 | 62 | mod apply { |
| 61 | − use crate::ident::cowstr; | |
| 63 | + use bstr::{ByteSlice, B}; | |
| 62 | 64 | use gix_filter::ident; |
| 63 | − use std::borrow::Cow; | |
| 64 | 65 | |
| 65 | 66 | #[test] |
| 66 | 67 | fn no_change() { |
| 68 | + let mut buf = Vec::new(); | |
| 67 | 69 | for input_no_match in [ |
| 68 | 70 | "", |
| 69 | 71 | "nothing", |
| 70 | 72 | "$ID$ case sensitive matching", |
| 71 | 73 | "$Id: expanded is ignored$", |
| 72 | 74 | ] { |
| 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); | |
| 79 | 78 | } |
| 80 | 79 | } |
| 81 | 80 | |
| 82 | 81 | #[test] |
| 83 | 82 | 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" | |
| 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 | 91 | 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" | |
| 92 | 94 | ); |
| 93 | 95 | } |
| 94 | 96 | |
| 95 | 97 | #[test] |
| 96 | 98 | fn round_trips() { |
| 99 | + let mut buf = Vec::new(); | |
| 97 | 100 | for input in [ |
| 98 | 101 | "hi\n$Id$\nho\n\t$Id$$Id$$Id$", |
| 99 | 102 | "$Id$", |
| 100 | 103 | "$Id$ and one more $Id$ and done", |
| 101 | 104 | ] { |
| 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"); | |
| 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 | +} | |