1use super::*;
6use crate::attributes::*;
7use crate::ignore::*;
8use crate::index::*;
9use crate::index_io::*;
10use crate::types_admin::*;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub(crate) enum EolConversion {
35 None,
38 Lf,
41 Crlf,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub(crate) enum TextDecision {
50 Binary,
52 Text,
54 Auto,
57 Unspecified,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub(crate) struct ContentFilterPlan {
64 pub(crate) text: TextDecision,
65 pub(crate) eol: EolConversion,
67 pub(crate) ident: bool,
69 pub(crate) driver: Option<FilterDriver>,
71 pub(crate) encoding: WtEncoding,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub(crate) struct FilterDriver {
78 name: Vec<u8>,
79 process: Option<String>,
80 clean: Option<String>,
81 smudge: Option<String>,
82 required: bool,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
90pub(crate) enum WtEncoding {
91 None,
93 Invalid,
95 Named(Vec<u8>),
97}
98
99impl WtEncoding {
100 fn from_attr(state: Option<&AttributeState>) -> WtEncoding {
101 match state {
102 None | Some(AttributeState::Unset) => WtEncoding::None,
104 Some(AttributeState::Set) => WtEncoding::Invalid,
106 Some(AttributeState::Value(value)) => {
107 if value.is_empty() || encoding_name_is_utf8(value) {
110 WtEncoding::None
111 } else {
112 WtEncoding::Named(value.clone())
113 }
114 }
115 }
116 }
117}
118
119pub(crate) fn encoding_name_is_utf8(name: &[u8]) -> bool {
123 utf_suffix(name).is_some_and(|suffix| suffix == "8")
124}
125
126pub(crate) fn utf_suffix(name: &[u8]) -> Option<String> {
130 let upper: String = std::str::from_utf8(name).ok()?.to_ascii_uppercase();
131 let rest = upper.strip_prefix("UTF")?;
132 Some(rest.strip_prefix('-').unwrap_or(rest).to_string())
133}
134
135#[derive(Clone, Copy)]
136pub(crate) enum BomProblem {
137 Prohibited,
138 Required,
139}
140
141pub(crate) fn utf_bom_problem(suffix: &str, data: &[u8]) -> Option<BomProblem> {
145 let has16 = data.starts_with(&[0xFF, 0xFE]) || data.starts_with(&[0xFE, 0xFF]);
146 let has32 = data.starts_with(&[0xFF, 0xFE, 0, 0]) || data.starts_with(&[0, 0, 0xFE, 0xFF]);
147 match suffix {
148 "16LE" | "16BE" => has16.then_some(BomProblem::Prohibited),
149 "32LE" | "32BE" => has32.then_some(BomProblem::Prohibited),
150 "16" => (!has16).then_some(BomProblem::Required),
151 "32" => (!has32).then_some(BomProblem::Required),
152 _ => None,
153 }
154}
155
156#[cfg(target_os = "macos")]
160pub(crate) const ICONV_UTF_DEFAULT_LE: bool = false;
161#[cfg(not(target_os = "macos"))]
162pub(crate) const ICONV_UTF_DEFAULT_LE: bool = cfg!(target_endian = "little");
163
164pub(crate) fn decode_to_utf8(suffix: &str, data: &[u8]) -> Option<Vec<u8>> {
167 match suffix {
168 "16LE" => decode_utf16(data, true),
169 "16BE" => decode_utf16(data, false),
170 "16" | "16LE-BOM" | "16BE-BOM" => {
171 let (le, body) = strip_utf16_bom(data);
172 decode_utf16(body, le)
173 }
174 "32LE" => decode_utf32(data, true),
175 "32BE" => decode_utf32(data, false),
176 "32" | "32LE-BOM" | "32BE-BOM" => {
177 let (le, body) = strip_utf32_bom(data);
178 decode_utf32(body, le)
179 }
180 _ => None,
181 }
182}
183
184pub(crate) fn encode_from_utf8(suffix: &str, utf8: &[u8]) -> Option<Vec<u8>> {
187 match suffix {
188 "16LE" => encode_utf16(utf8, true, false),
189 "16BE" => encode_utf16(utf8, false, false),
190 "16LE-BOM" => encode_utf16(utf8, true, true),
191 "16BE-BOM" => encode_utf16(utf8, false, true),
192 "16" => encode_utf16(utf8, ICONV_UTF_DEFAULT_LE, true),
193 "32LE" => encode_utf32(utf8, true, false),
194 "32BE" => encode_utf32(utf8, false, false),
195 "32LE-BOM" => encode_utf32(utf8, true, true),
196 "32BE-BOM" => encode_utf32(utf8, false, true),
197 "32" => encode_utf32(utf8, ICONV_UTF_DEFAULT_LE, true),
198 _ => None,
199 }
200}
201
202pub(crate) fn decode_named_encoding_to_utf8(name: &[u8], data: &[u8]) -> Option<Vec<u8>> {
203 if let Some(suffix) = utf_suffix(name) {
204 return decode_to_utf8(&suffix, data);
205 }
206 let encoding = encoding_rs::Encoding::for_label(name)?;
207 let (decoded, _, had_errors) = encoding.decode(data);
208 (!had_errors).then(|| decoded.into_owned().into_bytes())
209}
210
211pub(crate) fn encode_utf8_to_named_encoding(name: &[u8], utf8: &[u8]) -> Option<Vec<u8>> {
212 if let Some(suffix) = utf_suffix(name) {
213 return encode_from_utf8(&suffix, utf8);
214 }
215 let text = std::str::from_utf8(utf8).ok()?;
216 let encoding = encoding_rs::Encoding::for_label(name)?;
217 let (encoded, _, had_errors) = encoding.encode(text);
218 (!had_errors).then(|| encoded.into_owned())
219}
220
221pub(crate) fn should_check_roundtrip_encoding(config: &GitConfig, name: &[u8]) -> bool {
222 match config.get("core", None, "checkRoundtripEncoding") {
223 Some(value) => value
224 .as_bytes()
225 .split(|byte| *byte == b',' || byte.is_ascii_whitespace())
226 .filter(|part| !part.is_empty())
227 .any(|part| same_encoding_name(part, name)),
228 None => same_encoding_name(b"SHIFT-JIS", name),
229 }
230}
231
232pub(crate) fn same_encoding_name(left: &[u8], right: &[u8]) -> bool {
233 match (utf_suffix(left), utf_suffix(right)) {
234 (Some(left), Some(right)) => left == right,
235 _ => left.eq_ignore_ascii_case(right),
236 }
237}
238
239pub(crate) fn trace_roundtrip_encoding_check(name: &[u8]) {
240 if std::env::var_os("GIT_TRACE_WORKING_TREE_ENCODING").is_none()
241 && std::env::var_os("GIT_TRACE").is_none()
242 {
243 return;
244 }
245 eprintln!(
246 "Checking roundtrip encoding for {}",
247 String::from_utf8_lossy(name)
248 );
249}
250
251pub(crate) fn strip_utf16_bom(data: &[u8]) -> (bool, &[u8]) {
252 if data.starts_with(&[0xFF, 0xFE]) {
253 (true, &data[2..])
254 } else if data.starts_with(&[0xFE, 0xFF]) {
255 (false, &data[2..])
256 } else {
257 (ICONV_UTF_DEFAULT_LE, data)
258 }
259}
260
261pub(crate) fn strip_utf32_bom(data: &[u8]) -> (bool, &[u8]) {
262 if data.starts_with(&[0xFF, 0xFE, 0, 0]) {
263 (true, &data[4..])
264 } else if data.starts_with(&[0, 0, 0xFE, 0xFF]) {
265 (false, &data[4..])
266 } else {
267 (ICONV_UTF_DEFAULT_LE, data)
268 }
269}
270
271pub(crate) fn decode_utf16(data: &[u8], le: bool) -> Option<Vec<u8>> {
272 if !data.len().is_multiple_of(2) {
273 return None;
274 }
275 let units = data.chunks_exact(2).map(|chunk| {
276 let pair = [chunk[0], chunk[1]];
277 if le {
278 u16::from_le_bytes(pair)
279 } else {
280 u16::from_be_bytes(pair)
281 }
282 });
283 let mut out = String::new();
284 for unit in char::decode_utf16(units) {
285 out.push(unit.ok()?);
286 }
287 Some(out.into_bytes())
288}
289
290pub(crate) fn decode_utf32(data: &[u8], le: bool) -> Option<Vec<u8>> {
291 if !data.len().is_multiple_of(4) {
292 return None;
293 }
294 let mut out = String::new();
295 for chunk in data.chunks_exact(4) {
296 let quad = [chunk[0], chunk[1], chunk[2], chunk[3]];
297 let cp = if le {
298 u32::from_le_bytes(quad)
299 } else {
300 u32::from_be_bytes(quad)
301 };
302 out.push(char::from_u32(cp)?);
303 }
304 Some(out.into_bytes())
305}
306
307pub(crate) fn encode_utf16(utf8: &[u8], le: bool, bom: bool) -> Option<Vec<u8>> {
308 let text = std::str::from_utf8(utf8).ok()?;
309 let mut out = Vec::with_capacity(utf8.len() * 2 + 2);
310 if bom {
311 out.extend_from_slice(if le { &[0xFF, 0xFE] } else { &[0xFE, 0xFF] });
312 }
313 for unit in text.encode_utf16() {
314 out.extend_from_slice(&if le {
315 unit.to_le_bytes()
316 } else {
317 unit.to_be_bytes()
318 });
319 }
320 Some(out)
321}
322
323pub(crate) fn encode_utf32(utf8: &[u8], le: bool, bom: bool) -> Option<Vec<u8>> {
324 let text = std::str::from_utf8(utf8).ok()?;
325 let mut out = Vec::with_capacity(utf8.len() * 4 + 4);
326 if bom {
327 out.extend_from_slice(if le {
328 &[0xFF, 0xFE, 0, 0]
329 } else {
330 &[0, 0, 0xFE, 0xFF]
331 });
332 }
333 for ch in text.chars() {
334 let cp = ch as u32;
335 out.extend_from_slice(&if le {
336 cp.to_le_bytes()
337 } else {
338 cp.to_be_bytes()
339 });
340 }
341 Some(out)
342}
343
344pub(crate) fn check_wt_encoding_valid(encoding: &WtEncoding) -> Result<()> {
348 if matches!(encoding, WtEncoding::Invalid) {
349 eprintln!("fatal: true/false are no valid working-tree-encodings");
350 return Err(GitError::Exit(128));
351 }
352 Ok(())
353}
354
355pub(crate) fn clean_encoding_needs_stat_match_validation(
356 config: &GitConfig,
357 attributes: &[AttributeCheck],
358) -> bool {
359 let plan = ContentFilterPlan::resolve(config, attributes);
360 matches!(plan.encoding, WtEncoding::Invalid | WtEncoding::Named(_))
361}
362
363pub(crate) fn validate_clean_encoding_for_stat_match(
364 config: &GitConfig,
365 attributes: &[AttributeCheck],
366 path: &[u8],
367 content: &[u8],
368) -> Result<()> {
369 let plan = ContentFilterPlan::resolve(config, attributes);
370 check_wt_encoding_valid(&plan.encoding)?;
371 let _ = encode_to_git(config, &plan.encoding, path, Cow::Borrowed(content), false)?;
372 Ok(())
373}
374
375pub(crate) fn encode_to_git<'a>(
379 config: &GitConfig,
380 encoding: &WtEncoding,
381 path: &[u8],
382 data: Cow<'a, [u8]>,
383 write_object: bool,
384) -> Result<Cow<'a, [u8]>> {
385 let name = match encoding {
386 WtEncoding::None => return Ok(data),
387 WtEncoding::Invalid => return check_wt_encoding_valid(encoding).map(|()| data),
388 WtEncoding::Named(name) => name,
389 };
390 if data.is_empty() {
391 return Ok(data);
392 }
393 let display = String::from_utf8_lossy(path);
394 let enc = String::from_utf8_lossy(name);
395 if let Some(suffix) = utf_suffix(name)
396 && let Some(problem) = utf_bom_problem(&suffix, &data)
397 {
398 let number = &suffix[..2.min(suffix.len())];
399 match problem {
400 BomProblem::Prohibited => {
401 eprintln!(
402 "hint: The file '{display}' contains a byte order mark (BOM). \
403Please use UTF-{number} as working-tree-encoding."
404 );
405 report_encode_failure(
406 write_object,
407 &format!("BOM is prohibited in '{display}' if encoded as {enc}"),
408 )?;
409 return Ok(data);
410 }
411 BomProblem::Required => {
412 eprintln!(
413 "hint: The file '{display}' is missing a byte order mark (BOM). \
414Please use UTF-{number}BE or UTF-{number}LE (depending on the byte order) as \
415working-tree-encoding."
416 );
417 report_encode_failure(
418 write_object,
419 &format!("BOM is required in '{display}' if encoded as {enc}"),
420 )?;
421 return Ok(data);
422 }
423 }
424 }
425 match decode_named_encoding_to_utf8(name, &data) {
426 Some(utf8) => {
427 if should_check_roundtrip_encoding(config, name) {
428 trace_roundtrip_encoding_check(name);
429 if encode_utf8_to_named_encoding(name, &utf8).as_deref() != Some(data.as_ref()) {
430 report_encode_failure(
431 write_object,
432 &format!("encoding round trip failed for '{display}' from {enc} to UTF-8"),
433 )?;
434 return Ok(data);
435 }
436 }
437 Ok(Cow::Owned(utf8))
438 }
439 None => {
440 report_encode_failure(
441 write_object,
442 &format!("failed to encode '{display}' from {enc} to UTF-8"),
443 )?;
444 Ok(data)
445 }
446 }
447}
448
449pub(crate) fn encode_to_worktree<'a>(
453 encoding: &WtEncoding,
454 path: &[u8],
455 data: Cow<'a, [u8]>,
456) -> Result<Cow<'a, [u8]>> {
457 let name = match encoding {
458 WtEncoding::None => return Ok(data),
459 WtEncoding::Invalid => return check_wt_encoding_valid(encoding).map(|()| data),
460 WtEncoding::Named(name) => name,
461 };
462 if data.is_empty() {
463 return Ok(data);
464 }
465 match encode_utf8_to_named_encoding(name, &data) {
466 Some(encoded) => Ok(Cow::Owned(encoded)),
467 None => {
468 let display = String::from_utf8_lossy(path);
469 let enc = String::from_utf8_lossy(name);
470 eprintln!("error: failed to encode '{display}' from UTF-8 to {enc}");
471 Ok(data)
472 }
473 }
474}
475
476pub(crate) fn report_encode_failure(write_object: bool, message: &str) -> Result<()> {
479 if write_object {
480 eprintln!("fatal: {message}");
481 Err(GitError::Exit(128))
482 } else {
483 eprintln!("error: {message}");
484 Ok(())
485 }
486}
487
488pub(crate) fn decode_crlf_family_attribute(
496 state: Option<&AttributeState>,
497) -> (TextDecision, EolConversion) {
498 match state {
499 Some(AttributeState::Set) => (TextDecision::Text, EolConversion::None),
500 Some(AttributeState::Unset) => (TextDecision::Binary, EolConversion::None),
501 Some(AttributeState::Value(value)) if value == b"auto" => {
502 (TextDecision::Auto, EolConversion::None)
503 }
504 Some(AttributeState::Value(value)) if value == b"input" => {
507 (TextDecision::Text, EolConversion::Lf)
508 }
509 _ => (TextDecision::Unspecified, EolConversion::None),
511 }
512}
513
514impl ContentFilterPlan {
515 fn resolve(config: &GitConfig, checks: &[AttributeCheck]) -> Self {
517 let text_attr = checks.iter().find(|check| check.attribute == b"text");
518 let crlf_attr = checks.iter().find(|check| check.attribute == b"crlf");
519 let ident_attr = checks.iter().find(|check| check.attribute == b"ident");
520 let eol_attr = checks.iter().find(|check| check.attribute == b"eol");
521 let filter_attr = checks.iter().find(|check| check.attribute == b"filter");
522 let encoding_attr = checks
523 .iter()
524 .find(|check| check.attribute == b"working-tree-encoding");
525 let encoding = WtEncoding::from_attr(encoding_attr.and_then(|check| check.state.as_ref()));
526
527 let eol_value = eol_attr.and_then(|check| match &check.state {
529 Some(AttributeState::Value(value)) => Some(value.clone()),
530 _ => None,
531 });
532
533 let mut forced_eol = EolConversion::None;
536 let mut text = match text_attr.map(|check| &check.state) {
537 Some(Some(AttributeState::Set)) => TextDecision::Text,
538 Some(Some(AttributeState::Unset)) => TextDecision::Binary,
539 Some(Some(AttributeState::Value(value))) if value == b"auto" => TextDecision::Auto,
540 Some(Some(AttributeState::Value(value))) if value == b"input" => {
541 forced_eol = EolConversion::Lf;
542 TextDecision::Text
543 }
544 Some(Some(AttributeState::Value(_))) => TextDecision::Text,
546 _ => {
548 let (decision, eol) =
549 decode_crlf_family_attribute(crlf_attr.and_then(|check| check.state.as_ref()));
550 forced_eol = eol;
551 decision
552 }
553 };
554
555 let eol = match (&text, eol_value.as_deref()) {
560 (TextDecision::Binary, _) => EolConversion::None,
561 (_, Some(b"crlf")) => {
562 if text == TextDecision::Unspecified {
563 text = TextDecision::Text;
564 }
565 EolConversion::Crlf
566 }
567 (_, Some(b"lf")) => {
568 if text == TextDecision::Unspecified {
569 text = TextDecision::Text;
570 }
571 EolConversion::Lf
572 }
573 _ if forced_eol == EolConversion::Lf => EolConversion::Lf,
577 _ => eol_from_config(config),
579 };
580
581 let eol = match (&text, eol) {
585 (TextDecision::Text | TextDecision::Auto, EolConversion::None) => EolConversion::Lf,
586 (_, eol) => eol,
587 };
588
589 let text = match (text, eol_attr.is_some()) {
592 (TextDecision::Unspecified, _) => {
593 if autocrlf_enabled(config) {
596 TextDecision::Auto
597 } else {
598 TextDecision::Unspecified
599 }
600 }
601 (text, _) => text,
602 };
603
604 let driver = resolve_filter_driver(config, filter_attr);
605 let ident = matches!(
606 ident_attr.and_then(|check| check.state.as_ref()),
607 Some(AttributeState::Set)
608 );
609
610 ContentFilterPlan {
611 text,
612 eol,
613 ident,
614 driver,
615 encoding,
616 }
617 }
618
619 fn convert_eol(&self, content: &[u8]) -> bool {
621 match self.text {
622 TextDecision::Binary | TextDecision::Unspecified => false,
623 TextDecision::Text => self.eol != EolConversion::None,
624 TextDecision::Auto => self.eol != EolConversion::None && !looks_binary(content),
626 }
627 }
628
629 pub(crate) fn will_convert_lf_to_crlf(&self, content: &[u8]) -> bool {
637 self.will_convert_lf_to_crlf_stats(&gather_convert_stats(content))
638 }
639
640 fn will_convert_lf_to_crlf_stats(&self, stats: &ConvertStats) -> bool {
645 if self.eol != EolConversion::Crlf {
647 return false;
648 }
649 if stats.lonelf == 0 {
651 return false;
652 }
653 if self.text == TextDecision::Auto {
654 if stats.lonecr > 0 || stats.crlf > 0 {
656 return false;
657 }
658 if convert_is_binary(stats) {
659 return false;
660 }
661 }
662 true
663 }
664
665 fn safecrlf_applies(&self) -> bool {
669 matches!(self.text, TextDecision::Text | TextDecision::Auto)
670 }
671
672 fn check_safe_crlf_stats(
684 &self,
685 old_stats: &ConvertStats,
686 index_has_crlf: bool,
687 flags: ConvFlags,
688 path: &[u8],
689 ) -> Result<()> {
690 if flags == ConvFlags::Off || !self.safecrlf_applies() {
691 return Ok(());
692 }
693
694 let mut convert_crlf_into_lf = old_stats.crlf > 0;
699 if self.text == TextDecision::Auto {
700 if convert_is_binary(old_stats) {
701 return Ok(());
703 }
704 if index_has_crlf {
705 convert_crlf_into_lf = false;
706 }
707 }
708
709 let mut new_stats = old_stats.clone();
711 if convert_crlf_into_lf {
713 new_stats.lonelf += new_stats.crlf;
714 new_stats.crlf = 0;
715 }
716 if self.will_convert_lf_to_crlf_stats(&new_stats) {
718 new_stats.crlf += new_stats.lonelf;
719 new_stats.lonelf = 0;
720 }
721 check_safe_crlf(old_stats, &new_stats, flags, path)
722 }
723}
724
725pub(crate) fn eol_from_config(config: &GitConfig) -> EolConversion {
727 if let Some(value) = config.get("core", None, "autocrlf") {
728 match value.to_ascii_lowercase().as_str() {
729 "input" => return EolConversion::Lf,
730 "true" | "yes" | "on" | "1" => return EolConversion::Crlf,
731 _ => {}
732 }
733 }
734 if config.get_bool("core", None, "autocrlf") == Some(true) {
735 return EolConversion::Crlf;
736 }
737 match config
738 .get("core", None, "eol")
739 .map(|v| v.to_ascii_lowercase())
740 {
741 Some(ref v) if v == "crlf" => EolConversion::Crlf,
742 Some(ref v) if v == "lf" => EolConversion::Lf,
743 _ => EolConversion::None,
744 }
745}
746
747pub(crate) fn autocrlf_enabled(config: &GitConfig) -> bool {
750 if let Some(value) = config.get("core", None, "autocrlf")
751 && value.eq_ignore_ascii_case("input")
752 {
753 return true;
754 }
755 config.get_bool("core", None, "autocrlf") == Some(true)
756}
757
758pub(crate) fn resolve_filter_driver(
760 config: &GitConfig,
761 filter_attr: Option<&AttributeCheck>,
762) -> Option<FilterDriver> {
763 let name = match filter_attr.map(|check| &check.state) {
764 Some(Some(AttributeState::Value(value))) => value.clone(),
765 _ => return None,
767 };
768 let subsection = String::from_utf8_lossy(&name).into_owned();
769 let process = filter_config_value(config, &subsection, "process").filter(|cmd| !cmd.is_empty());
770 let clean = filter_config_value(config, &subsection, "clean").filter(|cmd| !cmd.is_empty());
771 let smudge = filter_config_value(config, &subsection, "smudge").filter(|cmd| !cmd.is_empty());
772 let required = filter_config_bool(config, &subsection, "required").unwrap_or(false);
773 if process.is_none() && clean.is_none() && smudge.is_none() && !required {
775 return None;
776 }
777 Some(FilterDriver {
778 name,
779 process,
780 clean,
781 smudge,
782 required,
783 })
784}
785
786pub(crate) fn filter_config_value(
787 config: &GitConfig,
788 subsection: &str,
789 key: &str,
790) -> Option<String> {
791 config
792 .get("filter", Some(subsection), key)
793 .map(str::to_owned)
794 .or_else(|| global_filter_config_value(subsection, key))
795}
796
797pub(crate) fn filter_config_bool(config: &GitConfig, subsection: &str, key: &str) -> Option<bool> {
798 config
799 .get_bool("filter", Some(subsection), key)
800 .or_else(|| {
801 global_filter_config_value(subsection, key)
802 .as_deref()
803 .and_then(sley_config::parse_config_bool)
804 })
805}
806
807pub(crate) fn global_filter_config_value(subsection: &str, key: &str) -> Option<String> {
808 for (path, _) in sley_config::default_config_layer_paths().into_iter().rev() {
809 let Ok(config) = GitConfig::read(path) else {
810 continue;
811 };
812 if let Some(value) = config.get("filter", Some(subsection), key) {
813 return Some(value.to_owned());
814 }
815 }
816 None
817}
818
819pub(crate) fn looks_binary(content: &[u8]) -> bool {
822 const FIRST_FEW_BYTES: usize = 8000;
823 let window = &content[..content.len().min(FIRST_FEW_BYTES)];
824 window.contains(&0)
825}
826
827pub(crate) fn convert_crlf_to_lf_cow(content: Cow<'_, [u8]>) -> Cow<'_, [u8]> {
831 if !content.windows(2).any(|window| window == b"\r\n") {
832 return content;
833 }
834 let mut out = Vec::with_capacity(content.len());
835 let mut index = 0;
836 while index < content.len() {
837 let byte = content[index];
838 if byte == b'\r' && content.get(index + 1) == Some(&b'\n') {
839 index += 1;
841 continue;
842 }
843 out.push(byte);
844 index += 1;
845 }
846 Cow::Owned(out)
847}
848
849pub(crate) fn convert_lf_to_crlf(content: &[u8]) -> Vec<u8> {
852 let mut out = Vec::with_capacity(content.len() + content.len() / 16);
853 let mut prev = 0u8;
854 for &byte in content {
855 if byte == b'\n' && prev != b'\r' {
856 out.push(b'\r');
857 }
858 out.push(byte);
859 prev = byte;
860 }
861 out
862}
863
864pub(crate) fn ident_to_git_cow(content: Cow<'_, [u8]>) -> Cow<'_, [u8]> {
866 let input = content.as_ref();
867 if !has_git_ident(input) {
868 return content;
869 }
870 let mut out = Vec::with_capacity(input.len());
871 let mut pos = 0;
872 while let Some(relative) = input[pos..].iter().position(|byte| *byte == b'$') {
873 let dollar = pos + relative;
874 out.extend_from_slice(&input[pos..=dollar]);
875 pos = dollar + 1;
876 if input.len().saturating_sub(pos) > 3 && input[pos..].starts_with(b"Id:") {
877 let search = &input[pos + 3..];
878 let Some(end_relative) = search.iter().position(|byte| *byte == b'$') else {
879 break;
880 };
881 let end = pos + 3 + end_relative;
882 if input[pos + 3..end].contains(&b'\n') {
883 continue;
884 }
885 out.extend_from_slice(b"Id$");
886 pos = end + 1;
887 }
888 }
889 out.extend_from_slice(&input[pos..]);
890 Cow::Owned(out)
891}
892
893pub(crate) fn ident_to_worktree_cow(
896 format: ObjectFormat,
897 content: Cow<'_, [u8]>,
898) -> Result<Cow<'_, [u8]>> {
899 let input = content.as_ref();
900 if !has_git_ident(input) {
901 return Ok(content);
902 }
903 let oid = EncodedObject::new(ObjectType::Blob, input.to_vec()).object_id(format)?;
904 let replacement = format!("Id: {} $", oid.to_hex());
905 let mut out = Vec::with_capacity(input.len() + replacement.len());
906 let mut pos = 0;
907 while let Some(relative) = input[pos..].iter().position(|byte| *byte == b'$') {
908 let dollar = pos + relative;
909 out.extend_from_slice(&input[pos..=dollar]);
910 pos = dollar + 1;
911 if input.len().saturating_sub(pos) < 3 || !input[pos..].starts_with(b"Id") {
912 continue;
913 }
914 match input.get(pos + 2) {
915 Some(b'$') => {
916 pos += 3;
917 }
918 Some(b':') => {
919 let search = &input[pos + 3..];
920 let Some(end_relative) = search.iter().position(|byte| *byte == b'$') else {
921 break;
922 };
923 let end = pos + 3 + end_relative;
924 if input[pos + 3..end].contains(&b'\n') || is_foreign_ident(&input[pos + 3..end]) {
925 continue;
926 }
927 pos = end + 1;
928 }
929 _ => continue,
930 }
931 out.extend_from_slice(replacement.as_bytes());
932 }
933 out.extend_from_slice(&input[pos..]);
934 Ok(Cow::Owned(out))
935}
936
937pub(crate) fn has_git_ident(content: &[u8]) -> bool {
938 let mut pos = 0;
939 while let Some(relative) = content[pos..].iter().position(|byte| *byte == b'$') {
940 let start = pos + relative + 1;
941 if content.len().saturating_sub(start) < 3 {
942 break;
943 }
944 if !content[start..].starts_with(b"Id") {
945 pos = start;
946 continue;
947 }
948 match content.get(start + 2) {
949 Some(b'$') => return true,
950 Some(b':') => {
951 let search = &content[start + 3..];
952 let Some(end_relative) = search.iter().position(|byte| *byte == b'$') else {
953 break;
954 };
955 let end = start + 3 + end_relative;
956 if !content[start + 3..end].contains(&b'\n') {
957 return true;
958 }
959 pos = end + 1;
960 }
961 _ => pos = start,
962 }
963 }
964 false
965}
966
967pub(crate) fn is_foreign_ident(expansion: &[u8]) -> bool {
968 if expansion.len() <= 1 {
969 return false;
970 }
971 expansion[1..expansion.len().saturating_sub(1)].contains(&b' ')
972}
973
974pub(crate) fn run_filter_command(command: &str, path: &[u8], content: &[u8]) -> Result<Vec<u8>> {
979 let display_path = String::from_utf8_lossy(path);
982 let expanded = command.replace("%f", &shell_quote(&display_path));
983 let (shell, flag) = if cfg!(windows) {
986 ("cmd", "/C")
987 } else {
988 ("/bin/sh", "-c")
989 };
990 let mut child = Command::new(shell)
991 .arg(flag)
992 .arg(&expanded)
993 .stdin(Stdio::piped())
994 .stdout(Stdio::piped())
995 .stderr(Stdio::piped())
996 .spawn()
997 .map_err(|err| GitError::Command(format!("failed to spawn filter `{command}`: {err}")))?;
998 let mut stdin = child
1001 .stdin
1002 .take()
1003 .ok_or_else(|| GitError::Command(format!("filter `{command}` stdin unavailable")))?;
1004 let payload = content.to_vec();
1005 let writer = std::thread::spawn(move || {
1006 let _ = stdin.write_all(&payload);
1007 });
1009 let output = child
1010 .wait_with_output()
1011 .map_err(|err| GitError::Command(format!("filter `{command}` failed: {err}")))?;
1012 let _ = writer.join();
1015 if !output.status.success() {
1016 let stderr = String::from_utf8_lossy(&output.stderr);
1017 return Err(GitError::Command(format!(
1018 "filter `{command}` exited with {}: {}",
1019 output.status,
1020 stderr.trim()
1021 )));
1022 }
1023 Ok(output.stdout)
1024}
1025
1026pub(crate) const PROCESS_CAP_CLEAN: u8 = 1;
1027pub(crate) const PROCESS_CAP_SMUDGE: u8 = 1 << 1;
1028pub(crate) const PROCESS_CAP_DELAY: u8 = 1 << 2;
1029pub(crate) const PKT_DATA_MAX: usize = 65_516;
1030
1031pub(crate) static PROCESS_FILTERS: OnceLock<Mutex<HashMap<String, ProcessFilter>>> =
1032 OnceLock::new();
1033pub(crate) static PROCESS_FILTER_ABORTED: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
1034pub type ProcessFilterMetadata = Vec<(String, String)>;
1037pub(crate) static PROCESS_FILTER_METADATA: OnceLock<Mutex<Option<ProcessFilterMetadata>>> =
1038 OnceLock::new();
1039pub(crate) static PROCESS_FILTER_CWD: OnceLock<Mutex<Option<PathBuf>>> = OnceLock::new();
1040
1041pub struct ProcessFilterMetadataGuard {
1043 previous: Option<ProcessFilterMetadata>,
1044}
1045
1046impl Drop for ProcessFilterMetadataGuard {
1047 fn drop(&mut self) {
1048 if let Ok(mut guard) = PROCESS_FILTER_METADATA
1049 .get_or_init(|| Mutex::new(None))
1050 .lock()
1051 {
1052 *guard = self.previous.take();
1053 }
1054 }
1055}
1056
1057pub fn set_process_filter_metadata(
1058 metadata: Option<ProcessFilterMetadata>,
1059) -> ProcessFilterMetadataGuard {
1060 let mutex = PROCESS_FILTER_METADATA.get_or_init(|| Mutex::new(None));
1061 let previous = mutex
1062 .lock()
1063 .map(|mut guard| std::mem::replace(&mut *guard, metadata))
1064 .unwrap_or(None);
1065 ProcessFilterMetadataGuard { previous }
1066}
1067
1068pub struct ProcessFilterCwdGuard {
1070 previous: Option<PathBuf>,
1071}
1072
1073impl Drop for ProcessFilterCwdGuard {
1074 fn drop(&mut self) {
1075 if let Ok(mut guard) = PROCESS_FILTER_CWD.get_or_init(|| Mutex::new(None)).lock() {
1076 *guard = self.previous.take();
1077 }
1078 }
1079}
1080
1081pub fn set_process_filter_cwd(cwd: Option<PathBuf>) -> ProcessFilterCwdGuard {
1082 let mutex = PROCESS_FILTER_CWD.get_or_init(|| Mutex::new(None));
1083 let previous = mutex
1084 .lock()
1085 .map(|mut guard| std::mem::replace(&mut *guard, cwd))
1086 .unwrap_or(None);
1087 ProcessFilterCwdGuard { previous }
1088}
1089
1090pub(crate) fn current_process_filter_metadata() -> Option<ProcessFilterMetadata> {
1091 PROCESS_FILTER_METADATA
1092 .get_or_init(|| Mutex::new(None))
1093 .lock()
1094 .ok()
1095 .and_then(|guard| guard.clone())
1096}
1097
1098pub(crate) fn current_process_filter_cwd() -> Option<PathBuf> {
1099 PROCESS_FILTER_CWD
1100 .get_or_init(|| Mutex::new(None))
1101 .lock()
1102 .ok()
1103 .and_then(|guard| guard.clone())
1104}
1105
1106pub(crate) struct ProcessFilter {
1107 child: Child,
1108 stdin: Option<ChildStdin>,
1109 stdout: ChildStdout,
1110 capabilities: u8,
1111 closed: bool,
1112}
1113
1114pub(crate) enum ProcessFilterOutcome {
1115 Filtered(Vec<u8>),
1116 Unsupported,
1117 Status(String),
1118}
1119
1120pub(crate) enum DriverFilterResult<'a> {
1121 Content(Cow<'a, [u8]>),
1122 Delayed { process: String },
1123}
1124
1125pub(crate) enum SmudgeFilterResult<'a> {
1126 Content(Cow<'a, [u8]>),
1127 Delayed { process: String },
1128}
1129
1130pub(crate) struct ProcessFilterFailure {
1131 pub(crate) message: String,
1132 pub(crate) protocol: bool,
1133}
1134
1135impl ProcessFilterFailure {
1136 fn protocol(message: impl Into<String>) -> Self {
1137 Self {
1138 message: message.into(),
1139 protocol: true,
1140 }
1141 }
1142}
1143
1144pub(crate) fn run_process_filter(
1145 command: &str,
1146 direction: &str,
1147 path: &[u8],
1148 content: &[u8],
1149 blob: Option<ObjectId>,
1150 can_delay: bool,
1151) -> std::result::Result<ProcessFilterOutcome, ProcessFilterFailure> {
1152 if PROCESS_FILTER_ABORTED
1153 .get_or_init(|| Mutex::new(HashSet::new()))
1154 .lock()
1155 .map(|aborted| aborted.contains(command))
1156 .unwrap_or(false)
1157 {
1158 return Ok(ProcessFilterOutcome::Status("abort".to_string()));
1159 }
1160 let filters = PROCESS_FILTERS.get_or_init(|| Mutex::new(HashMap::new()));
1161 let mut filters = filters
1162 .lock()
1163 .map_err(|_| ProcessFilterFailure::protocol("process filter cache poisoned"))?;
1164 if !filters.contains_key(command) {
1165 let filter = ProcessFilter::start(command)?;
1166 filters.insert(command.to_string(), filter);
1167 }
1168 let result = filters
1169 .get_mut(command)
1170 .expect("process filter was inserted")
1171 .apply(direction, path, content, blob, can_delay);
1172 if matches!(result, Ok(ProcessFilterOutcome::Status(ref status)) if status == "abort") {
1173 if let Some(mut filter) = filters.remove(command) {
1174 filter.finish_gracefully();
1175 }
1176 if let Ok(mut aborted) = PROCESS_FILTER_ABORTED
1177 .get_or_init(|| Mutex::new(HashSet::new()))
1178 .lock()
1179 {
1180 aborted.insert(command.to_string());
1181 }
1182 }
1183 if result.as_ref().is_err_and(|err| err.protocol) {
1184 filters.remove(command);
1185 }
1186 result
1187}
1188
1189pub(crate) fn list_available_process_filter_blobs(
1190 command: &str,
1191) -> std::result::Result<Vec<Vec<u8>>, ProcessFilterFailure> {
1192 let filters = PROCESS_FILTERS.get_or_init(|| Mutex::new(HashMap::new()));
1193 let mut filters = filters
1194 .lock()
1195 .map_err(|_| ProcessFilterFailure::protocol("process filter cache poisoned"))?;
1196 let Some(filter) = filters.get_mut(command) else {
1197 return Err(ProcessFilterFailure::protocol(format!(
1198 "external filter '{command}' is not available anymore although not all paths have been filtered"
1199 )));
1200 };
1201 let result = filter.list_available_blobs();
1202 if result.as_ref().is_err_and(|err| err.protocol) {
1203 filters.remove(command);
1204 }
1205 result
1206}
1207
1208impl ProcessFilter {
1209 fn start(command: &str) -> std::result::Result<Self, ProcessFilterFailure> {
1210 let (shell, flag) = if cfg!(windows) {
1211 ("cmd", "/C")
1212 } else {
1213 ("/bin/sh", "-c")
1214 };
1215 let mut process = Command::new(shell);
1216 process
1217 .arg(flag)
1218 .arg(command)
1219 .stdin(Stdio::piped())
1220 .stdout(Stdio::piped())
1221 .stderr(Stdio::inherit());
1222 if let Some(cwd) = current_process_filter_cwd() {
1223 process.current_dir(cwd);
1224 }
1225 let mut child = process.spawn().map_err(|err| {
1226 ProcessFilterFailure::protocol(format!(
1227 "cannot fork to run subprocess '{command}': {err}"
1228 ))
1229 })?;
1230 let mut stdin = child
1231 .stdin
1232 .take()
1233 .ok_or_else(|| ProcessFilterFailure::protocol("process filter stdin unavailable"))?;
1234 let mut stdout = child
1235 .stdout
1236 .take()
1237 .ok_or_else(|| ProcessFilterFailure::protocol("process filter stdout unavailable"))?;
1238
1239 write_pkt_text(&mut stdin, "git-filter-client\n")?;
1240 write_pkt_text(&mut stdin, "version=2\n")?;
1241 write_flush(&mut stdin)?;
1242
1243 let line = read_pkt_text(&mut stdout)?.ok_or_else(|| {
1244 ProcessFilterFailure::protocol(
1245 "Unexpected line '<flush packet>', expected git-filter-server",
1246 )
1247 })?;
1248 if line != "git-filter-server" {
1249 return Err(ProcessFilterFailure::protocol(format!(
1250 "Unexpected line '{line}', expected git-filter-server"
1251 )));
1252 }
1253 let line = read_pkt_text(&mut stdout)?.ok_or_else(|| {
1254 ProcessFilterFailure::protocol("Unexpected line '<flush packet>', expected version")
1255 })?;
1256 if line != "version=2" {
1257 return Err(ProcessFilterFailure::protocol(format!(
1258 "Unexpected line '{line}', expected version"
1259 )));
1260 }
1261 if let Some(line) = read_pkt_text(&mut stdout)? {
1262 return Err(ProcessFilterFailure::protocol(format!(
1263 "Unexpected line '{line}', expected flush"
1264 )));
1265 }
1266
1267 write_pkt_text(&mut stdin, "capability=clean\n")?;
1268 write_pkt_text(&mut stdin, "capability=smudge\n")?;
1269 write_pkt_text(&mut stdin, "capability=delay\n")?;
1270 write_flush(&mut stdin)?;
1271
1272 let mut capabilities = 0;
1273 while let Some(line) = read_pkt_text(&mut stdout)? {
1274 match line.as_str() {
1275 "capability=clean" => capabilities |= PROCESS_CAP_CLEAN,
1276 "capability=smudge" => capabilities |= PROCESS_CAP_SMUDGE,
1277 "capability=delay" => capabilities |= PROCESS_CAP_DELAY,
1278 _ => {}
1279 }
1280 }
1281
1282 Ok(Self {
1283 child,
1284 stdin: Some(stdin),
1285 stdout,
1286 capabilities,
1287 closed: false,
1288 })
1289 }
1290
1291 fn apply(
1292 &mut self,
1293 direction: &str,
1294 path: &[u8],
1295 content: &[u8],
1296 blob: Option<ObjectId>,
1297 can_delay: bool,
1298 ) -> std::result::Result<ProcessFilterOutcome, ProcessFilterFailure> {
1299 let wanted = match direction {
1300 "clean" => PROCESS_CAP_CLEAN,
1301 "smudge" => PROCESS_CAP_SMUDGE,
1302 _ => 0,
1303 };
1304 if self.capabilities & wanted == 0 {
1305 return Ok(ProcessFilterOutcome::Unsupported);
1306 }
1307
1308 let can_delay =
1309 can_delay && direction == "smudge" && self.capabilities & PROCESS_CAP_DELAY != 0;
1310
1311 {
1312 let stdin = self
1313 .stdin
1314 .as_mut()
1315 .ok_or_else(|| ProcessFilterFailure::protocol("process filter stdin closed"))?;
1316 write_pkt_text(stdin, &format!("command={direction}\n"))?;
1317 write_pkt_text(
1318 stdin,
1319 &format!("pathname={}\n", String::from_utf8_lossy(path)),
1320 )?;
1321 if direction == "smudge"
1322 && let Some(blob) = blob
1323 {
1324 if let Some(metadata) = current_process_filter_metadata() {
1325 for (key, value) in metadata {
1326 write_pkt_text(stdin, &format!("{key}={value}\n"))?;
1327 }
1328 }
1329 write_pkt_text(stdin, &format!("blob={}\n", blob.to_hex()))?;
1330 }
1331 if can_delay {
1332 write_pkt_text(stdin, "can-delay=1\n")?;
1333 }
1334 write_flush(stdin)?;
1335 write_pkt_content(stdin, content)?;
1336 write_flush(stdin)?;
1337 }
1338
1339 let mut status = read_process_status(&mut self.stdout)?.unwrap_or_default();
1340 match status.as_str() {
1341 "success" => {}
1342 "error" | "abort" | "delayed" => return Ok(ProcessFilterOutcome::Status(status)),
1343 other => {
1344 return Err(ProcessFilterFailure::protocol(format!(
1345 "external filter returned unsupported status '{other}'"
1346 )));
1347 }
1348 }
1349
1350 let output = read_pkt_content(&mut self.stdout)?;
1351 if let Some(next) = read_process_status(&mut self.stdout)? {
1352 status = next;
1353 }
1354 match status.as_str() {
1355 "" | "success" => Ok(ProcessFilterOutcome::Filtered(output)),
1356 "error" | "abort" | "delayed" => Ok(ProcessFilterOutcome::Status(status)),
1357 other => Err(ProcessFilterFailure::protocol(format!(
1358 "external filter returned unsupported status '{other}'"
1359 ))),
1360 }
1361 }
1362
1363 fn list_available_blobs(&mut self) -> std::result::Result<Vec<Vec<u8>>, ProcessFilterFailure> {
1364 if self.capabilities & PROCESS_CAP_DELAY == 0 {
1365 return Ok(Vec::new());
1366 }
1367 {
1368 let stdin = self
1369 .stdin
1370 .as_mut()
1371 .ok_or_else(|| ProcessFilterFailure::protocol("process filter stdin closed"))?;
1372 write_pkt_text(stdin, "command=list_available_blobs\n")?;
1373 write_flush(stdin)?;
1374 }
1375
1376 let mut paths = Vec::new();
1377 while let Some(line) = read_pkt_text(&mut self.stdout)? {
1378 if let Some(path) = line.strip_prefix("pathname=") {
1379 paths.push(path.as_bytes().to_vec());
1380 }
1381 }
1382 let status = read_process_status(&mut self.stdout)?.unwrap_or_default();
1383 match status.as_str() {
1384 "" | "success" => Ok(paths),
1385 other => Err(ProcessFilterFailure::protocol(format!(
1386 "external filter returned unsupported status '{other}'"
1387 ))),
1388 }
1389 }
1390
1391 fn finish_gracefully(&mut self) {
1392 self.stdin.take();
1393 for _ in 0..10 {
1394 match self.child.try_wait() {
1395 Ok(Some(_)) => {
1396 self.closed = true;
1397 return;
1398 }
1399 Ok(None) => std::thread::sleep(std::time::Duration::from_millis(10)),
1400 Err(_) => {
1401 self.closed = true;
1402 return;
1403 }
1404 }
1405 }
1406 let _ = self.child.kill();
1407 let _ = self.child.wait();
1408 self.closed = true;
1409 }
1410}
1411
1412impl Drop for ProcessFilter {
1413 fn drop(&mut self) {
1414 if self.closed {
1415 return;
1416 }
1417 if let Some(stdin) = self.stdin.as_mut() {
1418 let _ = stdin.flush();
1419 }
1420 let _ = self.child.kill();
1421 let _ = self.child.wait();
1422 }
1423}
1424
1425pub(crate) fn write_pkt_text(
1426 writer: &mut ChildStdin,
1427 text: &str,
1428) -> std::result::Result<(), ProcessFilterFailure> {
1429 write_pkt_data(writer, text.as_bytes())
1430}
1431
1432pub(crate) fn write_pkt_content(
1433 writer: &mut ChildStdin,
1434 content: &[u8],
1435) -> std::result::Result<(), ProcessFilterFailure> {
1436 for chunk in content.chunks(PKT_DATA_MAX) {
1437 write_pkt_data(writer, chunk)?;
1438 }
1439 Ok(())
1440}
1441
1442pub(crate) fn write_pkt_data(
1443 writer: &mut ChildStdin,
1444 data: &[u8],
1445) -> std::result::Result<(), ProcessFilterFailure> {
1446 let len = data.len() + 4;
1447 write!(writer, "{len:04x}")
1448 .and_then(|_| writer.write_all(data))
1449 .map_err(|err| {
1450 ProcessFilterFailure::protocol(format!("process filter write failed: {err}"))
1451 })
1452}
1453
1454pub(crate) fn write_flush(
1455 writer: &mut ChildStdin,
1456) -> std::result::Result<(), ProcessFilterFailure> {
1457 writer
1458 .write_all(b"0000")
1459 .and_then(|_| writer.flush())
1460 .map_err(|err| {
1461 ProcessFilterFailure::protocol(format!("process filter write failed: {err}"))
1462 })
1463}
1464
1465pub(crate) fn read_pkt_text(
1466 reader: &mut ChildStdout,
1467) -> std::result::Result<Option<String>, ProcessFilterFailure> {
1468 let Some(mut data) = read_pkt_data(reader)? else {
1469 return Ok(None);
1470 };
1471 if data.last() == Some(&b'\n') {
1472 data.pop();
1473 }
1474 Ok(Some(String::from_utf8_lossy(&data).into_owned()))
1475}
1476
1477pub(crate) fn read_pkt_content(
1478 reader: &mut ChildStdout,
1479) -> std::result::Result<Vec<u8>, ProcessFilterFailure> {
1480 let mut out = Vec::new();
1481 while let Some(data) = read_pkt_data(reader)? {
1482 out.extend_from_slice(&data);
1483 }
1484 Ok(out)
1485}
1486
1487pub(crate) fn read_pkt_data(
1488 reader: &mut ChildStdout,
1489) -> std::result::Result<Option<Vec<u8>>, ProcessFilterFailure> {
1490 let mut header = [0u8; 4];
1491 reader.read_exact(&mut header).map_err(|err| {
1492 ProcessFilterFailure::protocol(format!("process filter read failed: {err}"))
1493 })?;
1494 let header = std::str::from_utf8(&header)
1495 .map_err(|err| ProcessFilterFailure::protocol(format!("invalid pkt-line header: {err}")))?;
1496 let len = usize::from_str_radix(header, 16)
1497 .map_err(|err| ProcessFilterFailure::protocol(format!("invalid pkt-line length: {err}")))?;
1498 if len == 0 {
1499 return Ok(None);
1500 }
1501 if len < 4 {
1502 return Err(ProcessFilterFailure::protocol(format!(
1503 "invalid pkt-line length {len}"
1504 )));
1505 }
1506 let mut data = vec![0; len - 4];
1507 reader.read_exact(&mut data).map_err(|err| {
1508 ProcessFilterFailure::protocol(format!("process filter read failed: {err}"))
1509 })?;
1510 Ok(Some(data))
1511}
1512
1513pub(crate) fn read_process_status(
1514 reader: &mut ChildStdout,
1515) -> std::result::Result<Option<String>, ProcessFilterFailure> {
1516 let mut status = None;
1517 while let Some(line) = read_pkt_text(reader)? {
1518 if let Some(value) = line.strip_prefix("status=") {
1519 status = Some(value.to_string());
1520 }
1521 }
1522 Ok(status)
1523}
1524
1525pub(crate) fn shell_quote(value: &str) -> String {
1528 let mut out = String::with_capacity(value.len() + 2);
1529 out.push('\'');
1530 for ch in value.chars() {
1531 if ch == '\'' {
1532 out.push_str("'\\''");
1533 } else {
1534 out.push(ch);
1535 }
1536 }
1537 out.push('\'');
1538 out
1539}
1540
1541pub fn apply_clean_filter(
1555 worktree_root: impl AsRef<Path>,
1556 git_dir: impl AsRef<Path>,
1557 config: &GitConfig,
1558 path: &[u8],
1559 content: &[u8],
1560) -> Result<Vec<u8>> {
1561 let _ = git_dir.as_ref();
1565 let checks = filter_attribute_checks(worktree_root.as_ref(), path)?;
1566 apply_clean_filter_with_attributes(config, &checks, path, content)
1567}
1568
1569pub struct WorktreeAttributes {
1579 matcher: AttributeMatcher,
1580}
1581
1582impl WorktreeAttributes {
1583 pub fn from_worktree_root(worktree_root: impl AsRef<Path>) -> Result<Self> {
1586 Ok(Self {
1587 matcher: AttributeMatcher::from_worktree_root(worktree_root.as_ref())?,
1588 })
1589 }
1590
1591 pub fn apply_clean_filter(
1594 &self,
1595 config: &GitConfig,
1596 path: &[u8],
1597 content: &[u8],
1598 ) -> Result<Vec<u8>> {
1599 let checks = self
1600 .matcher
1601 .attributes_for_path(path, &filter_attribute_names(), false);
1602 apply_clean_filter_with_attributes(config, &checks, path, content)
1603 }
1604}
1605
1606pub struct TreeAttributes {
1623 matcher: AttributeMatcher,
1624}
1625
1626impl TreeAttributes {
1627 pub fn from_tree(
1638 attr_root: impl AsRef<Path>,
1639 git_dir: impl AsRef<Path>,
1640 db: &FileObjectDatabase,
1641 format: ObjectFormat,
1642 tree_oid: &ObjectId,
1643 ) -> Result<Self> {
1644 let attr_root = attr_root.as_ref();
1645 let git_dir = git_dir.as_ref();
1646 let mut matcher = AttributeMatcher::default();
1647 matcher.configure_case_sensitivity(git_dir);
1648 if !matcher.read_configured_attributes(attr_root, git_dir) {
1649 matcher.read_default_global_attributes();
1650 }
1651 collect_attribute_patterns_from_tree(db, format, tree_oid, Vec::new(), &mut matcher)?;
1652 read_attribute_patterns(
1653 git_dir.join("info").join("attributes"),
1654 &mut matcher,
1655 &[],
1656 b"info/attributes",
1657 false,
1658 );
1659 Ok(Self { matcher })
1660 }
1661
1662 pub fn apply_smudge_filter(
1668 &self,
1669 config: &GitConfig,
1670 path: &[u8],
1671 content: &[u8],
1672 ) -> Result<Vec<u8>> {
1673 let checks = self
1674 .matcher
1675 .attributes_for_path(path, &filter_attribute_names(), false);
1676 apply_smudge_filter_with_attributes(config, &checks, path, content)
1677 }
1678
1679 pub fn attributes_for_path(&self, path: &[u8], requested: &[Vec<u8>]) -> Vec<AttributeCheck> {
1680 self.matcher.attributes_for_path(path, requested, false)
1681 }
1682
1683 pub fn export_subst_for_path(&self, path: &[u8]) -> bool {
1687 self.attribute_is_set(path, b"export-subst")
1688 }
1689
1690 pub fn export_ignore_for_path(&self, path: &[u8]) -> bool {
1694 self.attribute_is_set(path, b"export-ignore")
1695 }
1696
1697 fn attribute_is_set(&self, path: &[u8], attribute: &[u8]) -> bool {
1698 let requested = [attribute.to_vec()];
1699 let checks = self.matcher.attributes_for_path(path, &requested, false);
1700 matches!(
1701 checks.first().and_then(|check| check.state.as_ref()),
1702 Some(AttributeState::Set)
1703 )
1704 }
1705
1706 pub fn diff_attribute_for_path(&self, path: &[u8]) -> Option<AttributeState> {
1711 let requested = [b"diff".to_vec()];
1712 let checks = self.matcher.attributes_for_path(path, &requested, false);
1713 checks.into_iter().next().and_then(|check| check.state)
1714 }
1715}
1716
1717pub fn apply_clean_filter_with_attributes(
1720 config: &GitConfig,
1721 attributes: &[AttributeCheck],
1722 path: &[u8],
1723 content: &[u8],
1724) -> Result<Vec<u8>> {
1725 Ok(apply_clean_filter_with_attributes_cow(config, attributes, path, content)?.into_owned())
1726}
1727
1728pub fn apply_clean_filter_with_attributes_cow<'a>(
1734 config: &GitConfig,
1735 attributes: &[AttributeCheck],
1736 path: &[u8],
1737 content: &'a [u8],
1738) -> Result<Cow<'a, [u8]>> {
1739 apply_clean_filter_with_attributes_cow_safecrlf(
1740 config,
1741 attributes,
1742 path,
1743 content,
1744 ConvFlags::Off,
1745 SafeCrlfIndexBlob::None,
1746 )
1747}
1748
1749pub enum SafeCrlfIndexBlob<'a> {
1753 None,
1756 Lookup {
1759 odb: &'a FileObjectDatabase,
1760 oid: ObjectId,
1761 },
1762}
1763
1764impl SafeCrlfIndexBlob<'_> {
1765 fn has_crlf(&self) -> bool {
1766 match self {
1767 SafeCrlfIndexBlob::None => false,
1768 SafeCrlfIndexBlob::Lookup { odb, oid } => has_crlf_in_index(odb, oid),
1769 }
1770 }
1771}
1772
1773pub fn apply_clean_filter_with_attributes_cow_safecrlf<'a>(
1782 config: &GitConfig,
1783 attributes: &[AttributeCheck],
1784 path: &[u8],
1785 content: &'a [u8],
1786 flags: ConvFlags,
1787 index_blob: SafeCrlfIndexBlob<'_>,
1788) -> Result<Cow<'a, [u8]>> {
1789 apply_clean_filter_cow_inner(config, attributes, path, content, flags, index_blob, false)
1792}
1793
1794pub(crate) fn apply_clean_filter_cow_inner<'a>(
1799 config: &GitConfig,
1800 attributes: &[AttributeCheck],
1801 path: &[u8],
1802 content: &'a [u8],
1803 flags: ConvFlags,
1804 index_blob: SafeCrlfIndexBlob<'_>,
1805 write_object: bool,
1806) -> Result<Cow<'a, [u8]>> {
1807 let plan = ContentFilterPlan::resolve(config, attributes);
1808 check_wt_encoding_valid(&plan.encoding)?;
1809 let mut data = Cow::Borrowed(content);
1810 if let Some(driver) = &plan.driver {
1811 data = run_driver(driver, driver.clean.as_deref(), "clean", None, path, data)?;
1812 }
1813 data = encode_to_git(config, &plan.encoding, path, data, write_object)?;
1817 if flags != ConvFlags::Off && !data.is_empty() && plan.safecrlf_applies() {
1822 let old_stats = gather_convert_stats(&data);
1823 plan.check_safe_crlf_stats(&old_stats, index_blob.has_crlf(), flags, path)?;
1824 }
1825 if plan.convert_eol(&data) {
1826 data = convert_crlf_to_lf_cow(data);
1827 }
1828 if plan.ident {
1829 data = ident_to_git_cow(data);
1830 }
1831 Ok(data)
1832}
1833
1834pub fn apply_smudge_filter(
1842 worktree_root: impl AsRef<Path>,
1843 git_dir: impl AsRef<Path>,
1844 format: ObjectFormat,
1845 config: &GitConfig,
1846 path: &[u8],
1847 content: &[u8],
1848) -> Result<Vec<u8>> {
1849 let checks =
1852 smudge_attribute_checks_from_index(worktree_root.as_ref(), git_dir.as_ref(), format, path)?;
1853 Ok(
1854 apply_smudge_filter_with_attributes_cow_format(config, &checks, path, content, format)?
1855 .into_owned(),
1856 )
1857}
1858
1859pub fn apply_smudge_filter_with_attributes(
1861 config: &GitConfig,
1862 attributes: &[AttributeCheck],
1863 path: &[u8],
1864 content: &[u8],
1865) -> Result<Vec<u8>> {
1866 Ok(apply_smudge_filter_with_attributes_cow(config, attributes, path, content)?.into_owned())
1867}
1868
1869pub fn apply_smudge_filter_with_attributes_cow<'a>(
1875 config: &GitConfig,
1876 attributes: &[AttributeCheck],
1877 path: &[u8],
1878 content: &'a [u8],
1879) -> Result<Cow<'a, [u8]>> {
1880 apply_smudge_filter_with_attributes_cow_format(
1881 config,
1882 attributes,
1883 path,
1884 content,
1885 ObjectFormat::Sha1,
1886 )
1887}
1888
1889pub(crate) fn apply_smudge_filter_with_attributes_cow_format<'a>(
1890 config: &GitConfig,
1891 attributes: &[AttributeCheck],
1892 path: &[u8],
1893 content: &'a [u8],
1894 format: ObjectFormat,
1895) -> Result<Cow<'a, [u8]>> {
1896 match apply_smudge_filter_with_attributes_maybe_delayed(
1897 config, attributes, path, content, format, false,
1898 )? {
1899 SmudgeFilterResult::Content(data) => Ok(data),
1900 SmudgeFilterResult::Delayed { .. } => unreachable!("delay was not enabled"),
1901 }
1902}
1903
1904pub(crate) fn apply_smudge_filter_with_attributes_maybe_delayed<'a>(
1905 config: &GitConfig,
1906 attributes: &[AttributeCheck],
1907 path: &[u8],
1908 content: &'a [u8],
1909 format: ObjectFormat,
1910 allow_delay: bool,
1911) -> Result<SmudgeFilterResult<'a>> {
1912 let plan = ContentFilterPlan::resolve(config, attributes);
1913 check_wt_encoding_valid(&plan.encoding)?;
1914 let process_blob = if plan
1915 .driver
1916 .as_ref()
1917 .and_then(|driver| driver.process.as_ref())
1918 .is_some()
1919 {
1920 Some(EncodedObject::new(ObjectType::Blob, content.to_vec()).object_id(format)?)
1921 } else {
1922 None
1923 };
1924 let mut data = Cow::Borrowed(content);
1925 if plan.ident {
1926 data = ident_to_worktree_cow(format, data)?;
1927 }
1928 if plan.eol == EolConversion::Crlf
1929 && plan.convert_eol(&data)
1930 && plan.will_convert_lf_to_crlf(&data)
1931 {
1932 data = Cow::Owned(convert_lf_to_crlf(&data));
1933 }
1934 data = encode_to_worktree(&plan.encoding, path, data)?;
1938 if let Some(driver) = &plan.driver {
1939 return match run_driver_maybe_delayed(
1940 driver,
1941 driver.smudge.as_deref(),
1942 "smudge",
1943 Some(format),
1944 process_blob,
1945 path,
1946 data,
1947 allow_delay,
1948 )? {
1949 DriverFilterResult::Content(data) => Ok(SmudgeFilterResult::Content(data)),
1950 DriverFilterResult::Delayed { process } => Ok(SmudgeFilterResult::Delayed { process }),
1951 };
1952 }
1953 Ok(SmudgeFilterResult::Content(data))
1954}
1955
1956pub(crate) fn run_driver<'a>(
1958 driver: &FilterDriver,
1959 command: Option<&str>,
1960 direction: &str,
1961 format: Option<ObjectFormat>,
1962 path: &[u8],
1963 content: Cow<'a, [u8]>,
1964) -> Result<Cow<'a, [u8]>> {
1965 match run_driver_maybe_delayed(
1966 driver, command, direction, format, None, path, content, false,
1967 )? {
1968 DriverFilterResult::Content(data) => Ok(data),
1969 DriverFilterResult::Delayed { .. } => unreachable!("delay was not enabled"),
1970 }
1971}
1972
1973pub(crate) fn run_driver_maybe_delayed<'a>(
1974 driver: &FilterDriver,
1975 command: Option<&str>,
1976 direction: &str,
1977 format: Option<ObjectFormat>,
1978 process_blob: Option<ObjectId>,
1979 path: &[u8],
1980 content: Cow<'a, [u8]>,
1981 allow_delay: bool,
1982) -> Result<DriverFilterResult<'a>> {
1983 if let Some(process) = &driver.process {
1984 let blob = if direction == "smudge" {
1985 match (process_blob, format) {
1986 (Some(blob), _) => Some(blob),
1987 (None, Some(format)) => {
1988 Some(EncodedObject::new(ObjectType::Blob, content.to_vec()).object_id(format)?)
1989 }
1990 (None, None) => None,
1991 }
1992 } else {
1993 None
1994 };
1995 match run_process_filter(process, direction, path, &content, blob, allow_delay) {
1996 Ok(ProcessFilterOutcome::Filtered(output)) => {
1997 return Ok(DriverFilterResult::Content(Cow::Owned(output)));
1998 }
1999 Ok(ProcessFilterOutcome::Unsupported) => {}
2000 Ok(ProcessFilterOutcome::Status(status)) => {
2001 if allow_delay && status == "delayed" {
2002 return Ok(DriverFilterResult::Delayed {
2003 process: process.clone(),
2004 });
2005 }
2006 if driver.required {
2007 return Err(GitError::Command(format!(
2008 "external filter '{}' returned status {status}",
2009 process
2010 )));
2011 }
2012 return Ok(DriverFilterResult::Content(content));
2013 }
2014 Err(err) => {
2015 if err.protocol {
2016 eprintln!("error: external filter '{}' failed", process);
2017 }
2018 if driver.required {
2019 return Err(GitError::Command(err.message));
2020 }
2021 return Ok(DriverFilterResult::Content(content));
2022 }
2023 }
2024 }
2025 let Some(command) = command else {
2026 if driver.required {
2029 let path = String::from_utf8_lossy(path);
2030 let name = String::from_utf8_lossy(&driver.name);
2031 if direction == "clean" {
2032 eprintln!("fatal: {path}: clean filter '{name}' failed");
2033 } else {
2034 eprintln!("fatal: {path}: smudge filter {name} failed");
2035 }
2036 return Err(GitError::Exit(128));
2037 }
2038 return Ok(DriverFilterResult::Content(content));
2039 };
2040 match run_filter_command(command, path, &content) {
2041 Ok(output) => Ok(DriverFilterResult::Content(Cow::Owned(output))),
2042 Err(err) => {
2043 if driver.required {
2044 Err(err)
2045 } else {
2046 Ok(DriverFilterResult::Content(content))
2049 }
2050 }
2051 }
2052}
2053
2054pub(crate) fn filter_attribute_checks(
2057 worktree_root: &Path,
2058 path: &[u8],
2059) -> Result<Vec<AttributeCheck>> {
2060 let requested = filter_attribute_names();
2061 let mut matcher = AttributeMatcher::default();
2062 let git_dir = worktree_root.join(".git");
2063 matcher.configure_case_sensitivity(&git_dir);
2064 if !matcher.read_configured_attributes(worktree_root, &git_dir) {
2065 matcher.read_default_global_attributes();
2066 }
2067 read_dir_attribute_patterns_for_base(worktree_root, &[], &mut matcher)?;
2068 let mut prefix = Vec::new();
2069 let mut parts = path.split(|byte| *byte == b'/').peekable();
2070 while let Some(part) = parts.next() {
2071 if parts.peek().is_none() {
2072 break;
2073 }
2074 if !prefix.is_empty() {
2075 prefix.push(b'/');
2076 }
2077 prefix.extend_from_slice(part);
2078 let dir = worktree_root.join(repo_path_to_os_path(&prefix)?);
2079 read_dir_attribute_patterns_for_base(&dir, &prefix, &mut matcher)?;
2080 }
2081 read_attribute_patterns(
2082 worktree_root.join(".git").join("info").join("attributes"),
2083 &mut matcher,
2084 &[],
2085 b".git/info/attributes",
2086 false,
2087 );
2088 Ok(matcher.attributes_for_path(path, &requested, false))
2089}
2090
2091pub(crate) fn smudge_attribute_checks_from_index(
2105 worktree_root: &Path,
2106 git_dir: &Path,
2107 format: ObjectFormat,
2108 path: &[u8],
2109) -> Result<Vec<AttributeCheck>> {
2110 let requested = filter_attribute_names();
2111 let mut matcher = AttributeMatcher::default();
2112 matcher.configure_case_sensitivity(git_dir);
2113 if !matcher.read_configured_attributes(worktree_root, git_dir) {
2114 matcher.read_default_global_attributes();
2115 }
2116
2117 let index_attributes = index_gitattributes_by_base(git_dir, format)?;
2120
2121 fold_checkout_attribute_frame(worktree_root, &[], &index_attributes, &mut matcher)?;
2124 let mut prefix = Vec::new();
2125 let mut parts = path.split(|byte| *byte == b'/').peekable();
2126 while let Some(part) = parts.next() {
2127 if parts.peek().is_none() {
2128 break;
2129 }
2130 if !prefix.is_empty() {
2131 prefix.push(b'/');
2132 }
2133 prefix.extend_from_slice(part);
2134 let dir = worktree_root.join(repo_path_to_os_path(&prefix)?);
2135 fold_checkout_attribute_frame(&dir, &prefix, &index_attributes, &mut matcher)?;
2136 }
2137
2138 read_attribute_patterns(
2139 worktree_root.join(".git").join("info").join("attributes"),
2140 &mut matcher,
2141 &[],
2142 b".git/info/attributes",
2143 false,
2144 );
2145 Ok(matcher.attributes_for_path(path, &requested, false))
2146}
2147
2148pub(crate) fn fold_checkout_attribute_frame(
2153 dir: &Path,
2154 base: &[u8],
2155 index_attributes: &BTreeMap<Vec<u8>, Vec<u8>>,
2156 matcher: &mut AttributeMatcher,
2157) -> Result<()> {
2158 let worktree_file = dir.join(".gitattributes");
2159 let source = attribute_source_for_base(base);
2160 if let Ok(contents) = fs::read(&worktree_file) {
2161 read_attribute_patterns_from_bytes(&contents, matcher, base, &source);
2164 } else if let Some(contents) = index_attributes.get(base) {
2165 read_attribute_patterns_from_bytes(contents, matcher, base, &source);
2166 }
2167 Ok(())
2168}
2169
2170pub(crate) fn index_gitattributes_by_base(
2173 git_dir: &Path,
2174 format: ObjectFormat,
2175) -> Result<BTreeMap<Vec<u8>, Vec<u8>>> {
2176 let mut map = BTreeMap::new();
2177 let index_path = repository_index_path(git_dir);
2178 if !index_path.exists() {
2179 return Ok(map);
2180 }
2181 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2182 let entries = Index::parse(&fs::read(index_path)?, format)?.entries;
2183 for entry in entries {
2184 let is_attributes_file =
2185 entry.path == b".gitattributes" || entry.path.as_bytes().ends_with(b"/.gitattributes");
2186 if index_entry_stage(&entry) != 0
2187 || tree_entry_object_type(entry.mode) != ObjectType::Blob
2188 || !is_attributes_file
2189 {
2190 continue;
2191 }
2192 let base = match entry.path.as_bytes().strip_suffix(b".gitattributes") {
2193 Some(b"") => Vec::new(),
2194 Some(parent) => parent.strip_suffix(b"/").unwrap_or(parent).to_vec(),
2195 None => continue,
2196 };
2197 let object = db
2198 .read_object(&entry.oid)
2199 .map_err(|err| expect_missing_object_kind(err, entry.oid, MissingObjectKind::Blob))?;
2200 if object.object_type == ObjectType::Blob {
2201 map.insert(base, object.body.clone());
2202 }
2203 }
2204 Ok(map)
2205}
2206
2207pub(crate) fn filter_attribute_names() -> Vec<Vec<u8>> {
2208 vec![
2211 b"text".to_vec(),
2212 b"crlf".to_vec(),
2213 b"ident".to_vec(),
2214 b"eol".to_vec(),
2215 b"filter".to_vec(),
2216 b"working-tree-encoding".to_vec(),
2217 ]
2218}
2219
2220#[derive(Clone)]
2235pub(crate) struct ConvertStats {
2236 nul: u32,
2237 lonecr: u32,
2238 lonelf: u32,
2239 crlf: u32,
2240 printable: u32,
2241 nonprintable: u32,
2242}
2243
2244pub(crate) fn gather_convert_stats(buf: &[u8]) -> ConvertStats {
2245 let mut stats = ConvertStats {
2246 nul: 0,
2247 lonecr: 0,
2248 lonelf: 0,
2249 crlf: 0,
2250 printable: 0,
2251 nonprintable: 0,
2252 };
2253 let mut i = 0;
2254 while i < buf.len() {
2255 let c = buf[i];
2256 if c == b'\r' {
2257 if buf.get(i + 1) == Some(&b'\n') {
2258 stats.crlf += 1;
2259 i += 1;
2260 } else {
2261 stats.lonecr += 1;
2262 }
2263 i += 1;
2264 continue;
2265 }
2266 if c == b'\n' {
2267 stats.lonelf += 1;
2268 i += 1;
2269 continue;
2270 }
2271 if c == 127 {
2272 stats.nonprintable += 1;
2274 } else if c < 32 {
2275 match c {
2276 0x08 | 0x09 | 0x1b | 0x0c => stats.printable += 1,
2278 0 => {
2279 stats.nul += 1;
2280 stats.nonprintable += 1;
2281 }
2282 _ => stats.nonprintable += 1,
2283 }
2284 } else {
2285 stats.printable += 1;
2286 }
2287 i += 1;
2288 }
2289 if buf.last() == Some(&0x1a) {
2291 stats.nonprintable = stats.nonprintable.saturating_sub(1);
2292 }
2293 stats
2294}
2295
2296pub(crate) fn has_crlf_in_index(odb: &FileObjectDatabase, oid: &ObjectId) -> bool {
2302 let Ok(object) = odb.read_object(oid) else {
2303 return false;
2304 };
2305 if object.object_type != ObjectType::Blob {
2306 return false;
2307 }
2308 let data = &object.body;
2309 if !data.contains(&b'\r') {
2311 return false;
2312 }
2313 let stats = gather_convert_stats(data);
2314 !convert_is_binary(&stats) && stats.crlf > 0
2315}
2316
2317pub(crate) fn convert_is_binary(stats: &ConvertStats) -> bool {
2320 if stats.lonecr > 0 {
2321 return true;
2322 }
2323 if stats.nul > 0 {
2324 return true;
2325 }
2326 (stats.printable >> 7) < stats.nonprintable
2327}
2328
2329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2334pub enum ConvFlags {
2335 Off,
2337 Warn,
2340 Die,
2342}
2343
2344impl ConvFlags {
2345 pub fn from_config(config: &GitConfig) -> Self {
2351 match config.get("core", None, "safecrlf") {
2352 Some(value) if value.eq_ignore_ascii_case("warn") => ConvFlags::Warn,
2353 Some(_) => {
2354 if config.get_bool("core", None, "safecrlf") == Some(true) {
2355 ConvFlags::Die
2356 } else {
2357 ConvFlags::Off
2358 }
2359 }
2360 None => ConvFlags::Warn,
2361 }
2362 }
2363}
2364
2365pub(crate) fn check_safe_crlf(
2375 old_stats: &ConvertStats,
2376 new_stats: &ConvertStats,
2377 flags: ConvFlags,
2378 path: &[u8],
2379) -> Result<()> {
2380 if flags == ConvFlags::Off {
2381 return Ok(());
2382 }
2383 let display = String::from_utf8_lossy(path);
2384 if old_stats.crlf > 0 && new_stats.crlf == 0 {
2385 match flags {
2387 ConvFlags::Die => {
2388 eprintln!("fatal: CRLF would be replaced by LF in {display}");
2389 return Err(GitError::Exit(128));
2390 }
2391 ConvFlags::Warn => {
2392 eprintln!(
2393 "warning: in the working copy of '{display}', CRLF will be replaced by LF the next time Git touches it"
2394 );
2395 }
2396 ConvFlags::Off => unreachable!("handled above"),
2397 }
2398 } else if old_stats.lonelf > 0 && new_stats.lonelf == 0 {
2399 match flags {
2401 ConvFlags::Die => {
2402 eprintln!("fatal: LF would be replaced by CRLF in {display}");
2403 return Err(GitError::Exit(128));
2404 }
2405 ConvFlags::Warn => {
2406 eprintln!(
2407 "warning: in the working copy of '{display}', LF will be replaced by CRLF the next time Git touches it"
2408 );
2409 }
2410 ConvFlags::Off => unreachable!("handled above"),
2411 }
2412 }
2413 Ok(())
2414}
2415
2416pub(crate) fn convert_stats_ascii(content: &[u8]) -> &'static str {
2419 if content.is_empty() {
2420 return "none";
2421 }
2422 let stats = gather_convert_stats(content);
2423 if convert_is_binary(&stats) {
2424 return "-text";
2425 }
2426 match (stats.lonelf > 0, stats.crlf > 0) {
2427 (true, false) => "lf",
2428 (false, true) => "crlf",
2429 (true, true) => "mixed",
2430 (false, false) => "none",
2431 }
2432}
2433
2434pub(crate) fn convert_attr_ascii(checks: &[AttributeCheck]) -> &'static str {
2438 fn state_of<'a>(checks: &'a [AttributeCheck], name: &[u8]) -> Option<&'a AttributeState> {
2439 checks
2440 .iter()
2441 .find(|check| check.attribute == name)
2442 .and_then(|check| check.state.as_ref())
2443 }
2444
2445 #[derive(Clone, Copy, PartialEq)]
2449 enum Action {
2450 Undefined,
2451 Binary,
2452 Text,
2453 TextInput,
2454 TextCrlf,
2455 Auto,
2456 AutoCrlf,
2457 AutoInput,
2458 }
2459 fn check_crlf(state: Option<&AttributeState>) -> Action {
2460 match state {
2461 Some(AttributeState::Set) => Action::Text,
2462 Some(AttributeState::Unset) => Action::Binary,
2463 Some(AttributeState::Value(value)) if value == b"input" => Action::TextInput,
2464 Some(AttributeState::Value(value)) if value == b"auto" => Action::Auto,
2465 _ => Action::Undefined,
2467 }
2468 }
2469
2470 let mut action = check_crlf(state_of(checks, b"text"));
2473 if action == Action::Undefined {
2474 action = check_crlf(state_of(checks, b"crlf"));
2475 }
2476
2477 if action != Action::Binary {
2478 let eol = match state_of(checks, b"eol") {
2480 Some(AttributeState::Value(value)) if value == b"lf" => Some(false),
2481 Some(AttributeState::Value(value)) if value == b"crlf" => Some(true),
2482 _ => None,
2483 };
2484 action = match (action, eol) {
2485 (Action::Auto, Some(false)) => Action::AutoInput,
2486 (Action::Auto, Some(true)) => Action::AutoCrlf,
2487 (_, Some(false)) if action != Action::Auto => Action::TextInput,
2488 (_, Some(true)) if action != Action::Auto => Action::TextCrlf,
2489 _ => action,
2490 };
2491 }
2492
2493 match action {
2494 Action::Undefined => "",
2495 Action::Binary => "-text",
2496 Action::Text => "text",
2497 Action::TextInput => "text eol=lf",
2498 Action::TextCrlf => "text eol=crlf",
2499 Action::Auto => "text=auto",
2500 Action::AutoCrlf => "text=auto eol=crlf",
2501 Action::AutoInput => "text=auto eol=lf",
2502 }
2503}
2504
2505pub struct EolInfo {
2507 pub index: &'static str,
2509 pub worktree: &'static str,
2511 pub attr: &'static str,
2513}
2514
2515impl EolInfo {
2516 pub fn format_prefix(&self) -> String {
2518 format!(
2519 "i/{:<5} w/{:<5} attr/{:<17}\t",
2520 self.index, self.worktree, self.attr
2521 )
2522 }
2523}
2524
2525pub fn eol_info_for_path(
2533 worktree_root: impl AsRef<Path>,
2534 path: &[u8],
2535 index_content: Option<&[u8]>,
2536 attr_checks: &[AttributeCheck],
2537) -> EolInfo {
2538 let index = index_content.map(convert_stats_ascii).unwrap_or("");
2539
2540 let worktree_root = worktree_root.as_ref();
2541 let worktree = match repo_path_to_os_path(path) {
2542 Ok(rel) => {
2543 let absolute = worktree_root.join(rel);
2544 match fs::symlink_metadata(&absolute) {
2545 Ok(meta) if meta.file_type().is_file() => match fs::read(&absolute) {
2547 Ok(content) => convert_stats_ascii_owned(&content),
2548 Err(_) => "",
2549 },
2550 _ => "",
2551 }
2552 }
2553 Err(_) => "",
2554 };
2555
2556 let attr = convert_attr_ascii(attr_checks);
2557
2558 EolInfo {
2559 index,
2560 worktree,
2561 attr,
2562 }
2563}
2564
2565pub(crate) fn convert_stats_ascii_owned(content: &[u8]) -> &'static str {
2568 convert_stats_ascii(content)
2569}
2570
2571pub fn eol_attribute_checks(
2575 worktree_root: impl AsRef<Path>,
2576 path: &[u8],
2577) -> Result<Vec<AttributeCheck>> {
2578 filter_attribute_checks(worktree_root.as_ref(), path)
2579}