// Copyright 2025 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. use std::io::Cursor; use bstr::ByteSlice as _;use tokio::io::AsyncRead;use tokio::io::AsyncReadExt as _; use crate::config::ConfigGetError;use crate::settings::UserSettings; fn is_binary(bytes: &[u8]) -> bool { // TODO(06393993): align the algorithm with git so that the git config autocrlf // users won't see different decisions on whether a file is binary and needs to // perform EOL conversion. let mut bytes = bytes.iter().peekable(); while let Some(byte) = bytes.next() { match *byte { b'\0' => return true, b'\r' => { if bytes.peek() != Some(&&b'\n') { return true; } } _ => {} } } false} #[derive(Clone)]pub(crate) struct TargetEolStrategy { eol_conversion_mode: EolConversionMode,} impl TargetEolStrategy { pub(crate) fn new(eol_conversion_mode: EolConversionMode) -> Self { Self { eol_conversion_mode, } } /// The limit to probe for whether the file is binary is 8KB. /// All files strictly smaller than the limit are always /// evaluated correctly and in full. /// Files larger than the limit - or with ambiguous content at the limit - /// are potentially misclassified. const PROBE_LIMIT: u64 = 8 << 10; /// Peek into the first [`TargetEolStrategy::PROBE_LIMIT`] bytes of content /// to determine if it is binary data. /// /// Peeked data is stored in `peek`. async fn probe_for_binary( mut contents: impl AsyncRead + Unpin, peek: &mut Vec<u8>, ) -> Result<bool, std::io::Error> { (&mut contents) .take(Self::PROBE_LIMIT) .read_to_end(peek) .await?; // The probe limit may have sliced a CRLF sequence, which would cause // misclassification as binary. let slice_to_check = if peek.get(Self::PROBE_LIMIT as usize - 1) == Some(&b'\r') { &peek[0..Self::PROBE_LIMIT as usize - 1] } else { peek }; Ok(is_binary(slice_to_check)) } pub(crate) async fn convert_eol_for_snapshot<'a>( &self, mut contents: impl AsyncRead + Send + Unpin + 'a, ) -> Result<Box<dyn AsyncRead + Send + Unpin + 'a>, std::io::Error> { match self.eol_conversion_mode { EolConversionMode::None => Ok(Box::new(contents)), EolConversionMode::Input | EolConversionMode::InputOutput => { let mut peek = vec![]; let target_eol = if Self::probe_for_binary(&mut contents, &mut peek).await? { TargetEol::PassThrough } else { TargetEol::Lf }; let peek = Cursor::new(peek); let contents = peek.chain(contents); convert_eol(contents, target_eol).await } } } pub(crate) async fn convert_eol_for_update<'a>( &self, mut contents: impl AsyncRead + Send + Unpin + 'a, ) -> Result<Box<dyn AsyncRead + Send + Unpin + 'a>, std::io::Error> { match self.eol_conversion_mode { EolConversionMode::None | EolConversionMode::Input => Ok(Box::new(contents)), EolConversionMode::InputOutput => { let mut peek = vec![]; let target_eol = if Self::probe_for_binary(&mut contents, &mut peek).await? { TargetEol::PassThrough } else { TargetEol::Crlf }; let peek = Cursor::new(peek); let contents = peek.chain(contents); convert_eol(contents, target_eol).await } } }} /// Configuring auto-converting CRLF line endings into LF when you add a file to/// the backend, and vice versa when it checks out code onto your filesystem.#[derive(Debug, PartialEq, Eq, Copy, Clone, serde::Deserialize)]#[serde(rename_all(deserialize = "kebab-case"))]pub enum EolConversionMode { /// Do not perform EOL conversion. None, /// Only perform the CRLF to LF EOL conversion when writing to the backend /// store from the file system. Input, /// Perform CRLF to LF EOL conversion when writing to the backend store from /// the file system and LF to CRLF EOL conversion when writing to the file /// system from the backend store. InputOutput,} impl EolConversionMode { /// Try to create the [`EolConversionMode`] based on the /// `working-copy.eol-conversion` setting in the [`UserSettings`]. pub fn try_from_settings(user_settings: &UserSettings) -> Result<Self, ConfigGetError> { user_settings.get("working-copy.eol-conversion") }} #[derive(Clone, Copy, Debug, PartialEq, Eq)]enum TargetEol { Lf, Crlf, PassThrough,} async fn convert_eol<'a>( mut input: impl AsyncRead + Send + Unpin + 'a, target_eol: TargetEol,) -> Result<Box<dyn AsyncRead + Send + Unpin + 'a>, std::io::Error> { let eol = match target_eol { TargetEol::PassThrough => { return Ok(Box::new(input)); } TargetEol::Lf => b"\n".as_slice(), TargetEol::Crlf => b"\r\n".as_slice(), }; let mut contents = vec![]; input.read_to_end(&mut contents).await?; let lines = contents.lines_with_terminator(); let mut res = Vec::<u8>::with_capacity(contents.len()); fn trim_last_eol(input: &[u8]) -> Option<&[u8]> { input .strip_suffix(b"\r\n") .or_else(|| input.strip_suffix(b"\n")) } for line in lines { if let Some(line) = trim_last_eol(line) { res.extend_from_slice(line); // If the line ends with an EOL, we should append the target EOL. res.extend_from_slice(eol); } else { // If the line doesn't end with an EOL, we don't append the EOL. This can happen // on the last line. res.extend_from_slice(line); } } Ok(Box::new(Cursor::new(res)))} #[cfg(test)]mod tests { use std::error::Error; use std::pin::Pin; use std::task::Poll; use test_case::test_case; use super::*; #[tokio::main(flavor = "current_thread")] #[test_case(b"a\n", TargetEol::PassThrough, b"a\n"; "LF text with no EOL conversion")] #[test_case(b"a\r\n", TargetEol::PassThrough, b"a\r\n"; "CRLF text with no EOL conversion")] #[test_case(b"a", TargetEol::PassThrough, b"a"; "no EOL text with no EOL conversion")] #[test_case(b"a\n", TargetEol::Crlf, b"a\r\n"; "LF text with CRLF EOL conversion")] #[test_case(b"a\r\n", TargetEol::Crlf, b"a\r\n"; "CRLF text with CRLF EOL conversion")] #[test_case(b"a", TargetEol::Crlf, b"a"; "no EOL text with CRLF conversion")] #[test_case(b"", TargetEol::Crlf, b""; "empty text with CRLF EOL conversion")] #[test_case(b"a\nb", TargetEol::Crlf, b"a\r\nb"; "text ends without EOL with CRLF EOL conversion")] #[test_case(b"a\n", TargetEol::Lf, b"a\n"; "LF text with LF EOL conversion")] #[test_case(b"a\r\n", TargetEol::Lf, b"a\n"; "CRLF text with LF EOL conversion")] #[test_case(b"a", TargetEol::Lf, b"a"; "no EOL text with LF conversion")] #[test_case(b"", TargetEol::Lf, b""; "empty text with LF EOL conversion")] #[test_case(b"a\r\nb", TargetEol::Lf, b"a\nb"; "text ends without EOL with LF EOL conversion")] async fn test_eol_conversion(input: &[u8], target_eol: TargetEol, expected_output: &[u8]) { let mut input = input; let mut output = vec![]; convert_eol(&mut input, target_eol) .await .expect("Failed to call convert_eol") .read_to_end(&mut output) .await .expect("Failed to read from the result"); assert_eq!(output, expected_output); } struct ErrorReader(Option<std::io::Error>); impl ErrorReader { fn new(error: std::io::Error) -> Self { Self(Some(error)) } } impl AsyncRead for ErrorReader { fn poll_read( mut self: Pin<&mut Self>, _cx: &mut std::task::Context<'_>, _buf: &mut tokio::io::ReadBuf<'_>, ) -> Poll<std::io::Result<()>> { if let Some(e) = self.0.take() { return Poll::Ready(Err(e)); } Poll::Ready(Ok(())) } } #[tokio::main(flavor = "current_thread")] #[test_case(TargetEol::PassThrough; "no EOL conversion")] #[test_case(TargetEol::Lf; "LF EOL conversion")] #[test_case(TargetEol::Crlf; "CRLF EOL conversion")] async fn test_eol_convert_eol_read_error(target_eol: TargetEol) { let message = "test error"; let error_reader = ErrorReader::new(std::io::Error::other(message)); let mut output = vec![]; // TODO: use TryFutureExt::and_then and async closure after we upgrade to 1.85.0 // or later. let err = match convert_eol(error_reader, target_eol).await { Ok(mut reader) => reader.read_to_end(&mut output).await, Err(e) => Err(e), } .expect_err("should fail"); let has_expected_error_message = (0..) .scan(Some(&err as &(dyn Error + 'static)), |err, _| { let current_err = err.take()?; *err = current_err.source(); Some(current_err) }) .any(|e| e.to_string() == message); assert!( has_expected_error_message, "should have expected error message: {message}" ); } fn test_probe_limit_input_crlf() -> [u8; TargetEolStrategy::PROBE_LIMIT as usize + 1] { let mut arr = [b'a'; TargetEolStrategy::PROBE_LIMIT as usize + 1]; let crlf = b"\r\n"; arr[100..102].copy_from_slice(crlf); arr[500..502].copy_from_slice(crlf); arr[1000..1002].copy_from_slice(crlf); arr[4090..4092].copy_from_slice(crlf); arr[TargetEolStrategy::PROBE_LIMIT as usize - 1 ..TargetEolStrategy::PROBE_LIMIT as usize + 1] .copy_from_slice(crlf); arr } fn test_probe_limit_input_lf() -> Vec<u8> { test_probe_limit_input_crlf().replace(b"\r\n", b"\n") } #[tokio::main(flavor = "current_thread")] #[test_case(TargetEolStrategy { eol_conversion_mode: EolConversionMode::None, }, b"\r\n", b"\r\n"; "none settings")] #[test_case(TargetEolStrategy { eol_conversion_mode: EolConversionMode::Input, }, b"\r\n", b"\n"; "input settings text input")] #[test_case(TargetEolStrategy { eol_conversion_mode: EolConversionMode::InputOutput, }, b"\r\n", b"\n"; "input output settings text input")] #[test_case(TargetEolStrategy { eol_conversion_mode: EolConversionMode::Input, }, b"\0\r\n", b"\0\r\n"; "input settings binary input")] #[test_case(TargetEolStrategy { eol_conversion_mode: EolConversionMode::InputOutput, }, b"\0\r\n", b"\0\r\n"; "input output settings binary input with NUL")] #[test_case(TargetEolStrategy { eol_conversion_mode: EolConversionMode::InputOutput, }, b"\r\r\n", b"\r\r\n"; "input output settings binary input with lone CR")] #[test_case(TargetEolStrategy { eol_conversion_mode: EolConversionMode::Input, }, &[0; 20 << 10], &[0; 20 << 10]; "input settings long binary input")] #[test_case(TargetEolStrategy { eol_conversion_mode: EolConversionMode::Input, }, &test_probe_limit_input_crlf(), &test_probe_limit_input_lf(); "input settings with CRLF on probe boundary")] async fn test_eol_strategy_convert_eol_for_snapshot( strategy: TargetEolStrategy, contents: &[u8], expected_output: &[u8], ) { let mut actual_output = vec![]; strategy .convert_eol_for_snapshot(contents) .await .unwrap() .read_to_end(&mut actual_output) .await .unwrap(); assert_eq!(actual_output, expected_output); } #[tokio::main(flavor = "current_thread")] #[test_case(TargetEolStrategy { eol_conversion_mode: EolConversionMode::None, }, b"\n", b"\n"; "none settings")] #[test_case(TargetEolStrategy { eol_conversion_mode: EolConversionMode::Input, }, b"\n", b"\n"; "input settings")] #[test_case(TargetEolStrategy { eol_conversion_mode: EolConversionMode::InputOutput, }, b"\n", b"\r\n"; "input output settings text input")] #[test_case(TargetEolStrategy { eol_conversion_mode: EolConversionMode::InputOutput, }, b"\0\n", b"\0\n"; "input output settings binary input")] #[test_case(TargetEolStrategy { eol_conversion_mode: EolConversionMode::Input, }, &[0; 20 << 10], &[0; 20 << 10]; "input output settings long binary input")] async fn test_eol_strategy_convert_eol_for_update( strategy: TargetEolStrategy, contents: &[u8], expected_output: &[u8], ) { let mut actual_output = vec![]; strategy .convert_eol_for_update(contents) .await .unwrap() .read_to_end(&mut actual_output) .await .unwrap(); assert_eq!(actual_output, expected_output); }}
// Copyright 2025 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::io::Cursor;
use bstr::ByteSlice as _;
use tokio::io::AsyncRead;
use tokio::io::AsyncReadExt as _;
use crate::config::ConfigGetError;
use crate::settings::UserSettings;
fn is_binary(bytes: &[u8]) -> bool {
// TODO(06393993): align the algorithm with git so that the git config autocrlf
// users won't see different decisions on whether a file is binary and needs to
// perform EOL conversion.
let mut bytes = bytes.iter().peekable();
while let Some(byte) = bytes.next() {
match *byte {
b'\0' => return true,
b'\r' => {
if bytes.peek() != Some(&&b'\n') {
return true;
}
}
_ => {}
}
}
false
}
#[derive(Clone)]
pub(crate) struct TargetEolStrategy {
eol_conversion_mode: EolConversionMode,
}
impl TargetEolStrategy {
pub(crate) fn new(eol_conversion_mode: EolConversionMode) -> Self {
Self {
eol_conversion_mode,
}
}
/// The limit to probe for whether the file is binary is 8KB.
/// All files strictly smaller than the limit are always
/// evaluated correctly and in full.
/// Files larger than the limit - or with ambiguous content at the limit -
/// are potentially misclassified.
const PROBE_LIMIT: u64 = 8 << 10;
/// Peek into the first [`TargetEolStrategy::PROBE_LIMIT`] bytes of content
/// to determine if it is binary data.
///
/// Peeked data is stored in `peek`.
async fn probe_for_binary(
mut contents: impl AsyncRead + Unpin,
peek: &mut Vec<u8>,
) -> Result<bool, std::io::Error> {
(&mut contents)
.take(Self::PROBE_LIMIT)
.read_to_end(peek)
.await?;
// The probe limit may have sliced a CRLF sequence, which would cause
// misclassification as binary.
let slice_to_check = if peek.get(Self::PROBE_LIMIT as usize - 1) == Some(&b'\r') {
&peek[0..Self::PROBE_LIMIT as usize - 1]
} else {
peek
};
Ok(is_binary(slice_to_check))
}
pub(crate) async fn convert_eol_for_snapshot<'a>(
&self,
mut contents: impl AsyncRead + Send + Unpin + 'a,
) -> Result<Box<dyn AsyncRead + Send + Unpin + 'a>, std::io::Error> {
match self.eol_conversion_mode {
EolConversionMode::None => Ok(Box::new(contents)),
EolConversionMode::Input | EolConversionMode::InputOutput => {
let mut peek = vec![];
let target_eol = if Self::probe_for_binary(&mut contents, &mut peek).await? {
TargetEol::PassThrough
} else {
TargetEol::Lf
};
let peek = Cursor::new(peek);
let contents = peek.chain(contents);
convert_eol(contents, target_eol).await
}
}
}
pub(crate) async fn convert_eol_for_update<'a>(
&self,
mut contents: impl AsyncRead + Send + Unpin + 'a,
) -> Result<Box<dyn AsyncRead + Send + Unpin + 'a>, std::io::Error> {
match self.eol_conversion_mode {
EolConversionMode::None | EolConversionMode::Input => Ok(Box::new(contents)),
EolConversionMode::InputOutput => {
let mut peek = vec![];
let target_eol = if Self::probe_for_binary(&mut contents, &mut peek).await? {
TargetEol::PassThrough
} else {
TargetEol::Crlf
};
let peek = Cursor::new(peek);
let contents = peek.chain(contents);
convert_eol(contents, target_eol).await
}
}
}
}
/// Configuring auto-converting CRLF line endings into LF when you add a file to
/// the backend, and vice versa when it checks out code onto your filesystem.
#[derive(Debug, PartialEq, Eq, Copy, Clone, serde::Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
pub enum EolConversionMode {
/// Do not perform EOL conversion.
None,
/// Only perform the CRLF to LF EOL conversion when writing to the backend
/// store from the file system.
Input,
/// Perform CRLF to LF EOL conversion when writing to the backend store from
/// the file system and LF to CRLF EOL conversion when writing to the file
/// system from the backend store.
InputOutput,
}
impl EolConversionMode {
/// Try to create the [`EolConversionMode`] based on the
/// `working-copy.eol-conversion` setting in the [`UserSettings`].
pub fn try_from_settings(user_settings: &UserSettings) -> Result<Self, ConfigGetError> {
user_settings.get("working-copy.eol-conversion")
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TargetEol {
Lf,
Crlf,
PassThrough,
}
async fn convert_eol<'a>(
mut input: impl AsyncRead + Send + Unpin + 'a,
target_eol: TargetEol,
) -> Result<Box<dyn AsyncRead + Send + Unpin + 'a>, std::io::Error> {
let eol = match target_eol {
TargetEol::PassThrough => {
return Ok(Box::new(input));
}
TargetEol::Lf => b"\n".as_slice(),
TargetEol::Crlf => b"\r\n".as_slice(),
};
let mut contents = vec![];
input.read_to_end(&mut contents).await?;
let lines = contents.lines_with_terminator();
let mut res = Vec::<u8>::with_capacity(contents.len());
fn trim_last_eol(input: &[u8]) -> Option<&[u8]> {
input
.strip_suffix(b"\r\n")
.or_else(|| input.strip_suffix(b"\n"))
}
for line in lines {
if let Some(line) = trim_last_eol(line) {
res.extend_from_slice(line);
// If the line ends with an EOL, we should append the target EOL.
res.extend_from_slice(eol);
} else {
// If the line doesn't end with an EOL, we don't append the EOL. This can happen
// on the last line.
res.extend_from_slice(line);
}
}
Ok(Box::new(Cursor::new(res)))
}
#[cfg(test)]
mod tests {
use std::error::Error;
use std::pin::Pin;
use std::task::Poll;
use test_case::test_case;
use super::*;
#[tokio::main(flavor = "current_thread")]
#[test_case(b"a\n", TargetEol::PassThrough, b"a\n"; "LF text with no EOL conversion")]
#[test_case(b"a\r\n", TargetEol::PassThrough, b"a\r\n"; "CRLF text with no EOL conversion")]
#[test_case(b"a", TargetEol::PassThrough, b"a"; "no EOL text with no EOL conversion")]
#[test_case(b"a\n", TargetEol::Crlf, b"a\r\n"; "LF text with CRLF EOL conversion")]
#[test_case(b"a\r\n", TargetEol::Crlf, b"a\r\n"; "CRLF text with CRLF EOL conversion")]
#[test_case(b"a", TargetEol::Crlf, b"a"; "no EOL text with CRLF conversion")]
#[test_case(b"", TargetEol::Crlf, b""; "empty text with CRLF EOL conversion")]
#[test_case(b"a\nb", TargetEol::Crlf, b"a\r\nb"; "text ends without EOL with CRLF EOL conversion")]
#[test_case(b"a\n", TargetEol::Lf, b"a\n"; "LF text with LF EOL conversion")]
#[test_case(b"a\r\n", TargetEol::Lf, b"a\n"; "CRLF text with LF EOL conversion")]
#[test_case(b"a", TargetEol::Lf, b"a"; "no EOL text with LF conversion")]
#[test_case(b"", TargetEol::Lf, b""; "empty text with LF EOL conversion")]
#[test_case(b"a\r\nb", TargetEol::Lf, b"a\nb"; "text ends without EOL with LF EOL conversion")]
async fn test_eol_conversion(input: &[u8], target_eol: TargetEol, expected_output: &[u8]) {
let mut input = input;
let mut output = vec![];
convert_eol(&mut input, target_eol)
.await
.expect("Failed to call convert_eol")
.read_to_end(&mut output)
.await
.expect("Failed to read from the result");
assert_eq!(output, expected_output);
}
struct ErrorReader(Option<std::io::Error>);
impl ErrorReader {
fn new(error: std::io::Error) -> Self {
Self(Some(error))
}
}
impl AsyncRead for ErrorReader {
fn poll_read(
mut self: Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
_buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
if let Some(e) = self.0.take() {
return Poll::Ready(Err(e));
}
Poll::Ready(Ok(()))
}
}
#[tokio::main(flavor = "current_thread")]
#[test_case(TargetEol::PassThrough; "no EOL conversion")]
#[test_case(TargetEol::Lf; "LF EOL conversion")]
#[test_case(TargetEol::Crlf; "CRLF EOL conversion")]
async fn test_eol_convert_eol_read_error(target_eol: TargetEol) {
let message = "test error";
let error_reader = ErrorReader::new(std::io::Error::other(message));
let mut output = vec![];
// TODO: use TryFutureExt::and_then and async closure after we upgrade to 1.85.0
// or later.
let err = match convert_eol(error_reader, target_eol).await {
Ok(mut reader) => reader.read_to_end(&mut output).await,
Err(e) => Err(e),
}
.expect_err("should fail");
let has_expected_error_message = (0..)
.scan(Some(&err as &(dyn Error + 'static)), |err, _| {
let current_err = err.take()?;
*err = current_err.source();
Some(current_err)
})
.any(|e| e.to_string() == message);
assert!(
has_expected_error_message,
"should have expected error message: {message}"
);
}
fn test_probe_limit_input_crlf() -> [u8; TargetEolStrategy::PROBE_LIMIT as usize + 1] {
let mut arr = [b'a'; TargetEolStrategy::PROBE_LIMIT as usize + 1];
let crlf = b"\r\n";
arr[100..102].copy_from_slice(crlf);
arr[500..502].copy_from_slice(crlf);
arr[1000..1002].copy_from_slice(crlf);
arr[4090..4092].copy_from_slice(crlf);
arr[TargetEolStrategy::PROBE_LIMIT as usize - 1
..TargetEolStrategy::PROBE_LIMIT as usize + 1]
.copy_from_slice(crlf);
arr
}
fn test_probe_limit_input_lf() -> Vec<u8> {
test_probe_limit_input_crlf().replace(b"\r\n", b"\n")
}
#[tokio::main(flavor = "current_thread")]
#[test_case(TargetEolStrategy {
eol_conversion_mode: EolConversionMode::None,
}, b"\r\n", b"\r\n"; "none settings")]
#[test_case(TargetEolStrategy {
eol_conversion_mode: EolConversionMode::Input,
}, b"\r\n", b"\n"; "input settings text input")]
#[test_case(TargetEolStrategy {
eol_conversion_mode: EolConversionMode::InputOutput,
}, b"\r\n", b"\n"; "input output settings text input")]
#[test_case(TargetEolStrategy {
eol_conversion_mode: EolConversionMode::Input,
}, b"\0\r\n", b"\0\r\n"; "input settings binary input")]
#[test_case(TargetEolStrategy {
eol_conversion_mode: EolConversionMode::InputOutput,
}, b"\0\r\n", b"\0\r\n"; "input output settings binary input with NUL")]
#[test_case(TargetEolStrategy {
eol_conversion_mode: EolConversionMode::InputOutput,
}, b"\r\r\n", b"\r\r\n"; "input output settings binary input with lone CR")]
#[test_case(TargetEolStrategy {
eol_conversion_mode: EolConversionMode::Input,
}, &[0; 20 << 10], &[0; 20 << 10]; "input settings long binary input")]
#[test_case(TargetEolStrategy {
eol_conversion_mode: EolConversionMode::Input,
}, &test_probe_limit_input_crlf(), &test_probe_limit_input_lf(); "input settings with CRLF on probe boundary")]
async fn test_eol_strategy_convert_eol_for_snapshot(
strategy: TargetEolStrategy,
contents: &[u8],
expected_output: &[u8],
) {
let mut actual_output = vec![];
strategy
.convert_eol_for_snapshot(contents)
.await
.unwrap()
.read_to_end(&mut actual_output)
.await
.unwrap();
assert_eq!(actual_output, expected_output);
}
#[tokio::main(flavor = "current_thread")]
#[test_case(TargetEolStrategy {
eol_conversion_mode: EolConversionMode::None,
}, b"\n", b"\n"; "none settings")]
#[test_case(TargetEolStrategy {
eol_conversion_mode: EolConversionMode::Input,
}, b"\n", b"\n"; "input settings")]
#[test_case(TargetEolStrategy {
eol_conversion_mode: EolConversionMode::InputOutput,
}, b"\n", b"\r\n"; "input output settings text input")]
#[test_case(TargetEolStrategy {
eol_conversion_mode: EolConversionMode::InputOutput,
}, b"\0\n", b"\0\n"; "input output settings binary input")]
#[test_case(TargetEolStrategy {
eol_conversion_mode: EolConversionMode::Input,
}, &[0; 20 << 10], &[0; 20 << 10]; "input output settings long binary input")]
async fn test_eol_strategy_convert_eol_for_update(
strategy: TargetEolStrategy,
contents: &[u8],
expected_output: &[u8],
) {
let mut actual_output = vec![];
strategy
.convert_eol_for_update(contents)
.await
.unwrap()
.read_to_end(&mut actual_output)
.await
.unwrap();
assert_eq!(actual_output, expected_output);
}
}