mod harness;
use harness::{MAGIC, OVERHEAD, SharedKey, TestRepo};
struct Shape {
path: &'static str,
written: &'static [u8],
text: bool,
}
fn shapes() -> Vec<Shape> {
vec![
Shape {
path: "secrets/lf.env",
written: b"first\nsecond\nthird\n",
text: true,
},
Shape {
path: "secrets/crlf.env",
written: b"first\r\nsecond\r\nthird\r\n",
text: true,
},
Shape {
path: "secrets/mixed.env",
written: b"first\r\nsecond\nthird\r\n",
text: true,
},
Shape {
path: "secrets/keystore.env",
written: b"\x00\x01binary\r\ncontent\r\n\x1f\x00",
text: false,
},
]
}
fn normalise(content: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(content.len());
let mut index = 0;
while index < content.len() {
if content[index] == b'\r' && content.get(index + 1) == Some(&b'\n') {
out.push(b'\n');
index += 2;
} else {
out.push(content[index]);
index += 1;
}
}
out
}
fn to_crlf(content: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(content.len());
for (index, &byte) in content.iter().enumerate() {
if byte == b'\n' && (index == 0 || content[index - 1] != b'\r') {
out.push(b'\r');
}
out.push(byte);
}
out
}
fn expected_worktree(shape: &Shape, autocrlf: &str, eol: &str) -> Vec<u8> {
if !shape.text {
return shape.written.to_vec();
}
let stored = normalise(shape.written);
let wants_crlf = match autocrlf {
"true" => true,
"input" => false,
"false" => match eol {
"crlf" => true,
"lf" => false,
"native" => cfg!(windows),
other => panic!("core.eol={other} is not in the table"),
},
other => panic!("core.autocrlf={other} is not in the table"),
};
if wants_crlf { to_crlf(&stored) } else { stored }
}
#[test]
fn the_line_ending_matrix_stores_one_blob_and_checks_out_what_each_machine_asks_for() {
let key = SharedKey::minted();
let shapes = shapes();
let mut baseline: Vec<Option<Vec<u8>>> = vec![None; shapes.len()];
for autocrlf in ["false", "true", "input"] {
for eol in ["lf", "crlf", "native"] {
let label = format!("core.autocrlf={autocrlf} core.eol={eol}");
let repo = TestRepo::init();
repo.set_eol_config(autocrlf, eol);
repo.init_xcrypt_with(&key);
repo.write_xcrypt_config("secrets/\n");
repo.xcrypt_ok(["sync"]);
for shape in &shapes {
repo.write_file(shape.path, shape.written);
}
repo.commit_all("secrets in every shape of line ending");
repo.assert_status_clean();
for (index, shape) in shapes.iter().enumerate() {
let blob = repo.blob_bytes(shape.path);
let stored = normalise(shape.written);
let plaintext: &[u8] = if shape.text { &stored } else { shape.written };
assert!(
blob.starts_with(MAGIC),
"{label}: {} was not encrypted",
shape.path
);
assert_eq!(
blob.len(),
OVERHEAD + plaintext.len(),
"{label}: {} was normalised against the machine's \
configuration instead of the frozen rule",
shape.path
);
assert_eq!(
repo.blob_records_normalisation(shape.path),
shape.text,
"{label}: {} recorded the wrong text/binary verdict in its \
header, so smudge will convert the wrong file",
shape.path
);
match &baseline[index] {
None => baseline[index] = Some(blob),
Some(first) => assert_eq!(
&blob, first,
"{label}: {} stored different bytes than the first \
configuration did — determinism across machines is gone",
shape.path
),
}
}
for shape in &shapes {
repo.recheckout(shape.path);
let expected = expected_worktree(shape, autocrlf, eol);
assert_eq!(
repo.worktree_bytes(shape.path),
expected,
"{label}: {} came back with line endings this configuration \
did not ask for",
shape.path
);
}
repo.assert_status_clean();
repo.git_ok(["add", "-A"]);
repo.assert_status_clean();
}
}
}
#[test]
fn content_whose_original_cannot_come_back_is_named_and_nothing_else_is() {
let repo = TestRepo::init();
repo.set_eol_config("true", "native");
repo.init_xcrypt();
repo.write_xcrypt_config("secrets/\nforced/ text\n");
repo.xcrypt_ok(["sync"]);
let warning = "do not survive a round trip";
for (path, content) in [
("secrets/lf.env", &b"first\nsecond\n"[..]),
("secrets/crlf.env", b"first\r\nsecond\r\n"),
("secrets/none.env", b"no line ending at all"),
("secrets/empty.env", b""),
("secrets/binary.env", b"\x00\x01mixed\r\nendings\n\x00"),
("forced/lf.env", b"first\nsecond\n"),
("forced/crlf.env", b"first\r\nsecond\r\n"),
] {
repo.write_file(path, content);
let quiet = repo.git(["add", path]);
let said = String::from_utf8_lossy(&quiet.stderr).into_owned();
assert!(
quiet.status.success(),
"{path}: a healthy file was refused:\n{said}"
);
assert!(
!said.contains(warning),
"{path}: healthy content was called lossy, which is how a warning \
stops being read:\n{said}"
);
}
repo.write_file("secrets/mixed.env", b"first\r\nsecond\nthird\r\n");
let mixed = repo.git(["add", "secrets/mixed.env"]);
let said = String::from_utf8_lossy(&mixed.stderr).into_owned();
assert!(
mixed.status.success(),
"the warning refused a `git add`, which it must never do — with \
`required = true` that stops every git operation in the repository \
over content that is converted, not lost:\n{said}"
);
assert!(
said.contains(warning) && said.contains("secrets/mixed.env"),
"mixed line endings went by unnamed under the default mode:\n{said}"
);
assert!(
said.contains("binary"),
"the message must name the remedy that is inside this tool:\n{said}"
);
repo.write_file("forced/collapse.env", b"first\r\r\nsecond\n");
let collapsing = repo.git(["add", "forced/collapse.env"]);
let said = String::from_utf8_lossy(&collapsing.stderr).into_owned();
assert!(
collapsing.status.success(),
"refused rather than warned:\n{said}"
);
assert!(
said.contains(warning) && said.contains("forced/collapse.env"),
"a `CR` before a `CRLF` under an explicit `text` went by unnamed — this \
is the shape `core.safecrlf` does not catch either:\n{said}"
);
repo.commit_all("every shape");
for (path, written) in [
("secrets/mixed.env", &b"first\r\nsecond\nthird\r\n"[..]),
("forced/collapse.env", b"first\r\r\nsecond\n"),
] {
repo.recheckout(path);
assert_ne!(
repo.worktree_bytes(path),
written,
"{path} came back unchanged, so the warning is now false"
);
}
for (path, written) in [
("secrets/crlf.env", &b"first\r\nsecond\r\n"[..]),
("secrets/binary.env", b"\x00\x01mixed\r\nendings\n\x00"),
] {
repo.recheckout(path);
assert_eq!(
repo.worktree_bytes(path),
written,
"{path} was silently changed and nothing warned about it"
);
}
}
#[test]
fn a_declared_path_is_converted_no_further_than_an_undeclared_one_when_nothing_asks() {
let home = tempfile::TempDir::new().expect("could not create a home directory");
let repo = TestRepo::init().with_home(home.path());
repo.set_config("core.autocrlf", "false");
repo.git(["config", "--unset", "core.eol"]);
let configured = repo.git(["config", "--get", "core.eol"]);
assert!(
!configured.status.success(),
"this machine sets core.eol to {:?}, so the unset row cannot be tested here",
String::from_utf8_lossy(&configured.stdout).trim()
);
repo.init_xcrypt();
repo.write_xcrypt_config("secrets/\n");
repo.xcrypt_ok(["sync"]);
let lf = &b"first\nsecond\n"[..];
let crlf = &b"first\r\nsecond\r\n"[..];
for (declared, plain, content) in [
("secrets/lf.env", "plain/lf.env", lf),
("secrets/crlf.env", "plain/crlf.env", crlf),
] {
repo.write_file(declared, content);
repo.write_file(plain, content);
}
repo.commit_all("the same bytes, declared and not");
repo.recheckout("secrets/lf.env");
repo.recheckout("plain/lf.env");
assert_eq!(
repo.worktree_bytes("secrets/lf.env"),
repo.worktree_bytes("plain/lf.env"),
"declaring a path secret converted it where git converts nothing"
);
assert_eq!(
repo.worktree_bytes("secrets/lf.env"),
lf,
"nothing asked for a conversion, so the stored bytes must come back"
);
repo.recheckout("secrets/crlf.env");
repo.recheckout("plain/crlf.env");
assert_eq!(
repo.worktree_bytes("plain/crlf.env"),
crlf,
"git converts nothing here, so the undeclared twin keeps its CRLF"
);
assert_eq!(
repo.worktree_bytes("secrets/crlf.env"),
lf,
"a declared path is normalised on the way in, and that is not recoverable"
);
repo.assert_status_clean();
repo.git_ok(["add", "-A"]);
repo.assert_status_clean();
}
#[test]
fn an_override_given_on_the_command_line_reaches_the_filter_too() {
let repo = TestRepo::init();
repo.set_config("core.autocrlf", "false");
repo.init_xcrypt();
repo.write_xcrypt_config("secrets/\n");
repo.xcrypt_ok(["sync"]);
let lf = &b"first\nsecond\n"[..];
repo.write_file("secrets/a.env", lf);
repo.write_file("plain/b.txt", lf);
repo.commit_all("the same bytes, declared and not");
for path in ["secrets/a.env", "plain/b.txt"] {
std::fs::remove_file(repo.path().join(path)).expect("could not remove the file");
}
repo.git_ok([
"-c",
"core.autocrlf=true",
"checkout",
"--",
"secrets",
"plain",
]);
assert_eq!(
repo.worktree_bytes("secrets/a.env"),
repo.worktree_bytes("plain/b.txt"),
"`git -c core.autocrlf=true` reached git and not the filter, so one \
command gave two answers"
);
assert_eq!(
repo.worktree_bytes("secrets/a.env"),
b"first\r\nsecond\r\n",
"the override asked for CRLF and git obeyed it, so we must too"
);
repo.recheckout("secrets/a.env");
assert_eq!(repo.worktree_bytes("secrets/a.env"), lf);
let status = repo.git_ok(["status", "--porcelain"]);
let status = String::from_utf8_lossy(&status.stdout);
assert!(
status.contains("plain/b.txt"),
"the undeclared file came back unconverted, so the override never \
reached git and this scenario proved nothing; status was:\n{status}"
);
assert!(
!status.contains("secrets/a.env"),
"the declared path did not settle after the override went away; \
status was:\n{status}"
);
}
#[test]
fn an_eol_the_content_will_never_accept_is_named_and_the_working_one_is_not() {
let repo = TestRepo::init();
repo.init_xcrypt();
repo.write_xcrypt_config("blobs/ eol=crlf\ntext/ eol=crlf\n");
repo.xcrypt_ok(["sync"]);
repo.write_file("blobs/bin.dat", b"A=1\nB=2\n\x00\x01\x02\n");
repo.write_file("text/t.env", b"A=1\nB=2\n");
let output = repo.git_ok(["add", "-A"]);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("blobs/bin.dat") && stderr.contains("`eol=crlf` does not reach"),
"the declaration promised CRLF for a file stored verbatim and nothing \
said so; stderr was:\n{stderr}"
);
assert!(
!stderr.contains("text/t.env"),
"the file `eol=crlf` genuinely applies to must not be warned about, or \
the warning teaches the reader to skip it; stderr was:\n{stderr}"
);
repo.commit_all("two files, one declaration");
repo.recheckout("blobs/bin.dat");
assert_eq!(
repo.worktree_bytes("blobs/bin.dat"),
b"A=1\nB=2\n\x00\x01\x02\n",
"the warning says it is stored verbatim, so it had better be"
);
repo.recheckout("text/t.env");
assert_eq!(
repo.worktree_bytes("text/t.env"),
b"A=1\r\nB=2\r\n",
"and `eol=crlf` really does apply where the content allows it"
);
}