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 Some(filter) = filters.get_mut(command) else {
1169 return Err(ProcessFilterFailure::protocol(
1170 "process filter missing after insert",
1171 ));
1172 };
1173 let result = filter.apply(direction, path, content, blob, can_delay);
1174 if matches!(result, Ok(ProcessFilterOutcome::Status(ref status)) if status == "abort") {
1175 if let Some(mut filter) = filters.remove(command) {
1176 filter.finish_gracefully();
1177 }
1178 if let Ok(mut aborted) = PROCESS_FILTER_ABORTED
1179 .get_or_init(|| Mutex::new(HashSet::new()))
1180 .lock()
1181 {
1182 aborted.insert(command.to_string());
1183 }
1184 }
1185 if result.as_ref().is_err_and(|err| err.protocol) {
1186 filters.remove(command);
1187 }
1188 result
1189}
1190
1191pub(crate) fn list_available_process_filter_blobs(
1192 command: &str,
1193) -> std::result::Result<Vec<Vec<u8>>, ProcessFilterFailure> {
1194 let filters = PROCESS_FILTERS.get_or_init(|| Mutex::new(HashMap::new()));
1195 let mut filters = filters
1196 .lock()
1197 .map_err(|_| ProcessFilterFailure::protocol("process filter cache poisoned"))?;
1198 let Some(filter) = filters.get_mut(command) else {
1199 return Err(ProcessFilterFailure::protocol(format!(
1200 "external filter '{command}' is not available anymore although not all paths have been filtered"
1201 )));
1202 };
1203 let result = filter.list_available_blobs();
1204 if result.as_ref().is_err_and(|err| err.protocol) {
1205 filters.remove(command);
1206 }
1207 result
1208}
1209
1210impl ProcessFilter {
1211 fn start(command: &str) -> std::result::Result<Self, ProcessFilterFailure> {
1212 let (shell, flag) = if cfg!(windows) {
1213 ("cmd", "/C")
1214 } else {
1215 ("/bin/sh", "-c")
1216 };
1217 let mut process = Command::new(shell);
1218 process
1219 .arg(flag)
1220 .arg(command)
1221 .stdin(Stdio::piped())
1222 .stdout(Stdio::piped())
1223 .stderr(Stdio::inherit());
1224 if let Some(cwd) = current_process_filter_cwd() {
1225 process.current_dir(cwd);
1226 }
1227 let mut child = process.spawn().map_err(|err| {
1228 ProcessFilterFailure::protocol(format!(
1229 "cannot fork to run subprocess '{command}': {err}"
1230 ))
1231 })?;
1232 let mut stdin = child
1233 .stdin
1234 .take()
1235 .ok_or_else(|| ProcessFilterFailure::protocol("process filter stdin unavailable"))?;
1236 let mut stdout = child
1237 .stdout
1238 .take()
1239 .ok_or_else(|| ProcessFilterFailure::protocol("process filter stdout unavailable"))?;
1240
1241 write_pkt_text(&mut stdin, "git-filter-client\n")?;
1242 write_pkt_text(&mut stdin, "version=2\n")?;
1243 write_flush(&mut stdin)?;
1244
1245 let line = read_pkt_text(&mut stdout)?.ok_or_else(|| {
1246 ProcessFilterFailure::protocol(
1247 "Unexpected line '<flush packet>', expected git-filter-server",
1248 )
1249 })?;
1250 if line != "git-filter-server" {
1251 return Err(ProcessFilterFailure::protocol(format!(
1252 "Unexpected line '{line}', expected git-filter-server"
1253 )));
1254 }
1255 let line = read_pkt_text(&mut stdout)?.ok_or_else(|| {
1256 ProcessFilterFailure::protocol("Unexpected line '<flush packet>', expected version")
1257 })?;
1258 if line != "version=2" {
1259 return Err(ProcessFilterFailure::protocol(format!(
1260 "Unexpected line '{line}', expected version"
1261 )));
1262 }
1263 if let Some(line) = read_pkt_text(&mut stdout)? {
1264 return Err(ProcessFilterFailure::protocol(format!(
1265 "Unexpected line '{line}', expected flush"
1266 )));
1267 }
1268
1269 write_pkt_text(&mut stdin, "capability=clean\n")?;
1270 write_pkt_text(&mut stdin, "capability=smudge\n")?;
1271 write_pkt_text(&mut stdin, "capability=delay\n")?;
1272 write_flush(&mut stdin)?;
1273
1274 let mut capabilities = 0;
1275 while let Some(line) = read_pkt_text(&mut stdout)? {
1276 match line.as_str() {
1277 "capability=clean" => capabilities |= PROCESS_CAP_CLEAN,
1278 "capability=smudge" => capabilities |= PROCESS_CAP_SMUDGE,
1279 "capability=delay" => capabilities |= PROCESS_CAP_DELAY,
1280 _ => {}
1281 }
1282 }
1283
1284 Ok(Self {
1285 child,
1286 stdin: Some(stdin),
1287 stdout,
1288 capabilities,
1289 closed: false,
1290 })
1291 }
1292
1293 fn apply(
1294 &mut self,
1295 direction: &str,
1296 path: &[u8],
1297 content: &[u8],
1298 blob: Option<ObjectId>,
1299 can_delay: bool,
1300 ) -> std::result::Result<ProcessFilterOutcome, ProcessFilterFailure> {
1301 let wanted = match direction {
1302 "clean" => PROCESS_CAP_CLEAN,
1303 "smudge" => PROCESS_CAP_SMUDGE,
1304 _ => 0,
1305 };
1306 if self.capabilities & wanted == 0 {
1307 return Ok(ProcessFilterOutcome::Unsupported);
1308 }
1309
1310 let can_delay =
1311 can_delay && direction == "smudge" && self.capabilities & PROCESS_CAP_DELAY != 0;
1312
1313 {
1314 let stdin = self
1315 .stdin
1316 .as_mut()
1317 .ok_or_else(|| ProcessFilterFailure::protocol("process filter stdin closed"))?;
1318 write_pkt_text(stdin, &format!("command={direction}\n"))?;
1319 write_pkt_text(
1320 stdin,
1321 &format!("pathname={}\n", String::from_utf8_lossy(path)),
1322 )?;
1323 if direction == "smudge"
1324 && let Some(blob) = blob
1325 {
1326 if let Some(metadata) = current_process_filter_metadata() {
1327 for (key, value) in metadata {
1328 write_pkt_text(stdin, &format!("{key}={value}\n"))?;
1329 }
1330 }
1331 write_pkt_text(stdin, &format!("blob={}\n", blob.to_hex()))?;
1332 }
1333 if can_delay {
1334 write_pkt_text(stdin, "can-delay=1\n")?;
1335 }
1336 write_flush(stdin)?;
1337 write_pkt_content(stdin, content)?;
1338 write_flush(stdin)?;
1339 }
1340
1341 let mut status = read_process_status(&mut self.stdout)?.unwrap_or_default();
1342 match status.as_str() {
1343 "success" => {}
1344 "error" | "abort" | "delayed" => return Ok(ProcessFilterOutcome::Status(status)),
1345 other => {
1346 return Err(ProcessFilterFailure::protocol(format!(
1347 "external filter returned unsupported status '{other}'"
1348 )));
1349 }
1350 }
1351
1352 let output = read_pkt_content(&mut self.stdout)?;
1353 if let Some(next) = read_process_status(&mut self.stdout)? {
1354 status = next;
1355 }
1356 match status.as_str() {
1357 "" | "success" => Ok(ProcessFilterOutcome::Filtered(output)),
1358 "error" | "abort" | "delayed" => Ok(ProcessFilterOutcome::Status(status)),
1359 other => Err(ProcessFilterFailure::protocol(format!(
1360 "external filter returned unsupported status '{other}'"
1361 ))),
1362 }
1363 }
1364
1365 fn list_available_blobs(&mut self) -> std::result::Result<Vec<Vec<u8>>, ProcessFilterFailure> {
1366 if self.capabilities & PROCESS_CAP_DELAY == 0 {
1367 return Ok(Vec::new());
1368 }
1369 {
1370 let stdin = self
1371 .stdin
1372 .as_mut()
1373 .ok_or_else(|| ProcessFilterFailure::protocol("process filter stdin closed"))?;
1374 write_pkt_text(stdin, "command=list_available_blobs\n")?;
1375 write_flush(stdin)?;
1376 }
1377
1378 let mut paths = Vec::new();
1379 while let Some(line) = read_pkt_text(&mut self.stdout)? {
1380 if let Some(path) = line.strip_prefix("pathname=") {
1381 paths.push(path.as_bytes().to_vec());
1382 }
1383 }
1384 let status = read_process_status(&mut self.stdout)?.unwrap_or_default();
1385 match status.as_str() {
1386 "" | "success" => Ok(paths),
1387 other => Err(ProcessFilterFailure::protocol(format!(
1388 "external filter returned unsupported status '{other}'"
1389 ))),
1390 }
1391 }
1392
1393 fn finish_gracefully(&mut self) {
1394 self.stdin.take();
1395 for _ in 0..10 {
1396 match self.child.try_wait() {
1397 Ok(Some(_)) => {
1398 self.closed = true;
1399 return;
1400 }
1401 Ok(None) => std::thread::sleep(std::time::Duration::from_millis(10)),
1402 Err(_) => {
1403 self.closed = true;
1404 return;
1405 }
1406 }
1407 }
1408 let _ = self.child.kill();
1409 let _ = self.child.wait();
1410 self.closed = true;
1411 }
1412}
1413
1414impl Drop for ProcessFilter {
1415 fn drop(&mut self) {
1416 if self.closed {
1417 return;
1418 }
1419 if let Some(stdin) = self.stdin.as_mut() {
1420 let _ = stdin.flush();
1421 }
1422 let _ = self.child.kill();
1423 let _ = self.child.wait();
1424 }
1425}
1426
1427pub(crate) fn write_pkt_text(
1428 writer: &mut ChildStdin,
1429 text: &str,
1430) -> std::result::Result<(), ProcessFilterFailure> {
1431 write_pkt_data(writer, text.as_bytes())
1432}
1433
1434pub(crate) fn write_pkt_content(
1435 writer: &mut ChildStdin,
1436 content: &[u8],
1437) -> std::result::Result<(), ProcessFilterFailure> {
1438 for chunk in content.chunks(PKT_DATA_MAX) {
1439 write_pkt_data(writer, chunk)?;
1440 }
1441 Ok(())
1442}
1443
1444pub(crate) fn write_pkt_data(
1445 writer: &mut ChildStdin,
1446 data: &[u8],
1447) -> std::result::Result<(), ProcessFilterFailure> {
1448 let len = data.len() + 4;
1449 write!(writer, "{len:04x}")
1450 .and_then(|_| writer.write_all(data))
1451 .map_err(|err| {
1452 ProcessFilterFailure::protocol(format!("process filter write failed: {err}"))
1453 })
1454}
1455
1456pub(crate) fn write_flush(
1457 writer: &mut ChildStdin,
1458) -> std::result::Result<(), ProcessFilterFailure> {
1459 writer
1460 .write_all(b"0000")
1461 .and_then(|_| writer.flush())
1462 .map_err(|err| {
1463 ProcessFilterFailure::protocol(format!("process filter write failed: {err}"))
1464 })
1465}
1466
1467pub(crate) fn read_pkt_text(
1468 reader: &mut ChildStdout,
1469) -> std::result::Result<Option<String>, ProcessFilterFailure> {
1470 let Some(mut data) = read_pkt_data(reader)? else {
1471 return Ok(None);
1472 };
1473 if data.last() == Some(&b'\n') {
1474 data.pop();
1475 }
1476 Ok(Some(String::from_utf8_lossy(&data).into_owned()))
1477}
1478
1479pub(crate) fn read_pkt_content(
1480 reader: &mut ChildStdout,
1481) -> std::result::Result<Vec<u8>, ProcessFilterFailure> {
1482 let mut out = Vec::new();
1483 while let Some(data) = read_pkt_data(reader)? {
1484 out.extend_from_slice(&data);
1485 }
1486 Ok(out)
1487}
1488
1489pub(crate) fn read_pkt_data(
1490 reader: &mut ChildStdout,
1491) -> std::result::Result<Option<Vec<u8>>, ProcessFilterFailure> {
1492 let mut header = [0u8; 4];
1493 reader.read_exact(&mut header).map_err(|err| {
1494 ProcessFilterFailure::protocol(format!("process filter read failed: {err}"))
1495 })?;
1496 let header = std::str::from_utf8(&header)
1497 .map_err(|err| ProcessFilterFailure::protocol(format!("invalid pkt-line header: {err}")))?;
1498 let len = usize::from_str_radix(header, 16)
1499 .map_err(|err| ProcessFilterFailure::protocol(format!("invalid pkt-line length: {err}")))?;
1500 if len == 0 {
1501 return Ok(None);
1502 }
1503 if len < 4 {
1504 return Err(ProcessFilterFailure::protocol(format!(
1505 "invalid pkt-line length {len}"
1506 )));
1507 }
1508 let mut data = vec![0; len - 4];
1509 reader.read_exact(&mut data).map_err(|err| {
1510 ProcessFilterFailure::protocol(format!("process filter read failed: {err}"))
1511 })?;
1512 Ok(Some(data))
1513}
1514
1515pub(crate) fn read_process_status(
1516 reader: &mut ChildStdout,
1517) -> std::result::Result<Option<String>, ProcessFilterFailure> {
1518 let mut status = None;
1519 while let Some(line) = read_pkt_text(reader)? {
1520 if let Some(value) = line.strip_prefix("status=") {
1521 status = Some(value.to_string());
1522 }
1523 }
1524 Ok(status)
1525}
1526
1527pub(crate) fn shell_quote(value: &str) -> String {
1530 let mut out = String::with_capacity(value.len() + 2);
1531 out.push('\'');
1532 for ch in value.chars() {
1533 if ch == '\'' {
1534 out.push_str("'\\''");
1535 } else {
1536 out.push(ch);
1537 }
1538 }
1539 out.push('\'');
1540 out
1541}
1542
1543pub fn apply_clean_filter(
1557 worktree_root: impl AsRef<Path>,
1558 git_dir: impl AsRef<Path>,
1559 config: &GitConfig,
1560 path: &[u8],
1561 content: &[u8],
1562) -> Result<Vec<u8>> {
1563 let _ = git_dir.as_ref();
1567 let checks = filter_attribute_checks(worktree_root.as_ref(), path)?;
1568 apply_clean_filter_with_attributes(config, &checks, path, content)
1569}
1570
1571pub struct WorktreeAttributes {
1581 matcher: AttributeMatcher,
1582}
1583
1584impl WorktreeAttributes {
1585 pub fn from_worktree_root(worktree_root: impl AsRef<Path>) -> Result<Self> {
1588 Ok(Self {
1589 matcher: AttributeMatcher::from_worktree_root(worktree_root.as_ref())?,
1590 })
1591 }
1592
1593 pub fn apply_clean_filter(
1596 &self,
1597 config: &GitConfig,
1598 path: &[u8],
1599 content: &[u8],
1600 ) -> Result<Vec<u8>> {
1601 let checks = self
1602 .matcher
1603 .attributes_for_path(path, &filter_attribute_names(), false);
1604 apply_clean_filter_with_attributes(config, &checks, path, content)
1605 }
1606}
1607
1608pub struct TreeAttributes {
1625 matcher: AttributeMatcher,
1626}
1627
1628impl TreeAttributes {
1629 pub fn from_tree(
1640 attr_root: impl AsRef<Path>,
1641 git_dir: impl AsRef<Path>,
1642 db: &FileObjectDatabase,
1643 format: ObjectFormat,
1644 tree_oid: &ObjectId,
1645 ) -> Result<Self> {
1646 let attr_root = attr_root.as_ref();
1647 let git_dir = git_dir.as_ref();
1648 let mut matcher = AttributeMatcher::default();
1649 matcher.configure_case_sensitivity(git_dir);
1650 if !matcher.read_configured_attributes(attr_root, git_dir) {
1651 matcher.read_default_global_attributes();
1652 }
1653 collect_attribute_patterns_from_tree(db, format, tree_oid, Vec::new(), &mut matcher)?;
1654 read_attribute_patterns(
1655 git_dir.join("info").join("attributes"),
1656 &mut matcher,
1657 &[],
1658 b"info/attributes",
1659 false,
1660 );
1661 Ok(Self { matcher })
1662 }
1663
1664 pub fn apply_smudge_filter(
1670 &self,
1671 config: &GitConfig,
1672 path: &[u8],
1673 content: &[u8],
1674 ) -> Result<Vec<u8>> {
1675 let checks = self
1676 .matcher
1677 .attributes_for_path(path, &filter_attribute_names(), false);
1678 apply_smudge_filter_with_attributes(config, &checks, path, content)
1679 }
1680
1681 pub fn attributes_for_path(&self, path: &[u8], requested: &[Vec<u8>]) -> Vec<AttributeCheck> {
1682 self.matcher.attributes_for_path(path, requested, false)
1683 }
1684
1685 pub fn export_subst_for_path(&self, path: &[u8]) -> bool {
1689 self.attribute_is_set(path, b"export-subst")
1690 }
1691
1692 pub fn export_ignore_for_path(&self, path: &[u8]) -> bool {
1696 self.attribute_is_set(path, b"export-ignore")
1697 }
1698
1699 fn attribute_is_set(&self, path: &[u8], attribute: &[u8]) -> bool {
1700 let requested = [attribute.to_vec()];
1701 let checks = self.matcher.attributes_for_path(path, &requested, false);
1702 matches!(
1703 checks.first().and_then(|check| check.state.as_ref()),
1704 Some(AttributeState::Set)
1705 )
1706 }
1707
1708 pub fn diff_attribute_for_path(&self, path: &[u8]) -> Option<AttributeState> {
1713 let requested = [b"diff".to_vec()];
1714 let checks = self.matcher.attributes_for_path(path, &requested, false);
1715 checks.into_iter().next().and_then(|check| check.state)
1716 }
1717}
1718
1719pub fn apply_clean_filter_with_attributes(
1722 config: &GitConfig,
1723 attributes: &[AttributeCheck],
1724 path: &[u8],
1725 content: &[u8],
1726) -> Result<Vec<u8>> {
1727 Ok(apply_clean_filter_with_attributes_cow(config, attributes, path, content)?.into_owned())
1728}
1729
1730pub fn apply_clean_filter_with_attributes_cow<'a>(
1736 config: &GitConfig,
1737 attributes: &[AttributeCheck],
1738 path: &[u8],
1739 content: &'a [u8],
1740) -> Result<Cow<'a, [u8]>> {
1741 apply_clean_filter_with_attributes_cow_safecrlf(
1742 config,
1743 attributes,
1744 path,
1745 content,
1746 ConvFlags::Off,
1747 SafeCrlfIndexBlob::None,
1748 )
1749}
1750
1751pub enum SafeCrlfIndexBlob<'a> {
1755 None,
1758 Lookup {
1761 odb: &'a FileObjectDatabase,
1762 oid: ObjectId,
1763 },
1764}
1765
1766impl SafeCrlfIndexBlob<'_> {
1767 fn has_crlf(&self) -> bool {
1768 match self {
1769 SafeCrlfIndexBlob::None => false,
1770 SafeCrlfIndexBlob::Lookup { odb, oid } => has_crlf_in_index(odb, oid),
1771 }
1772 }
1773}
1774
1775pub fn apply_clean_filter_with_attributes_cow_safecrlf<'a>(
1784 config: &GitConfig,
1785 attributes: &[AttributeCheck],
1786 path: &[u8],
1787 content: &'a [u8],
1788 flags: ConvFlags,
1789 index_blob: SafeCrlfIndexBlob<'_>,
1790) -> Result<Cow<'a, [u8]>> {
1791 apply_clean_filter_cow_inner(config, attributes, path, content, flags, index_blob, false)
1794}
1795
1796pub(crate) fn apply_clean_filter_cow_inner<'a>(
1801 config: &GitConfig,
1802 attributes: &[AttributeCheck],
1803 path: &[u8],
1804 content: &'a [u8],
1805 flags: ConvFlags,
1806 index_blob: SafeCrlfIndexBlob<'_>,
1807 write_object: bool,
1808) -> Result<Cow<'a, [u8]>> {
1809 let plan = ContentFilterPlan::resolve(config, attributes);
1810 check_wt_encoding_valid(&plan.encoding)?;
1811 let mut data = Cow::Borrowed(content);
1812 if let Some(driver) = &plan.driver {
1813 data = run_driver(driver, driver.clean.as_deref(), "clean", None, path, data)?;
1814 }
1815 data = encode_to_git(config, &plan.encoding, path, data, write_object)?;
1819 if flags != ConvFlags::Off && !data.is_empty() && plan.safecrlf_applies() {
1824 let old_stats = gather_convert_stats(&data);
1825 plan.check_safe_crlf_stats(&old_stats, index_blob.has_crlf(), flags, path)?;
1826 }
1827 if plan.convert_eol(&data) {
1828 data = convert_crlf_to_lf_cow(data);
1829 }
1830 if plan.ident {
1831 data = ident_to_git_cow(data);
1832 }
1833 Ok(data)
1834}
1835
1836pub fn apply_smudge_filter(
1844 worktree_root: impl AsRef<Path>,
1845 git_dir: impl AsRef<Path>,
1846 format: ObjectFormat,
1847 config: &GitConfig,
1848 path: &[u8],
1849 content: &[u8],
1850) -> Result<Vec<u8>> {
1851 let checks =
1854 smudge_attribute_checks_from_index(worktree_root.as_ref(), git_dir.as_ref(), format, path)?;
1855 Ok(
1856 apply_smudge_filter_with_attributes_cow_format(config, &checks, path, content, format)?
1857 .into_owned(),
1858 )
1859}
1860
1861pub fn apply_smudge_filter_with_attributes(
1863 config: &GitConfig,
1864 attributes: &[AttributeCheck],
1865 path: &[u8],
1866 content: &[u8],
1867) -> Result<Vec<u8>> {
1868 Ok(apply_smudge_filter_with_attributes_cow(config, attributes, path, content)?.into_owned())
1869}
1870
1871pub fn apply_smudge_filter_with_attributes_cow<'a>(
1877 config: &GitConfig,
1878 attributes: &[AttributeCheck],
1879 path: &[u8],
1880 content: &'a [u8],
1881) -> Result<Cow<'a, [u8]>> {
1882 apply_smudge_filter_with_attributes_cow_format(
1883 config,
1884 attributes,
1885 path,
1886 content,
1887 ObjectFormat::Sha1,
1888 )
1889}
1890
1891pub(crate) fn apply_smudge_filter_with_attributes_cow_format<'a>(
1892 config: &GitConfig,
1893 attributes: &[AttributeCheck],
1894 path: &[u8],
1895 content: &'a [u8],
1896 format: ObjectFormat,
1897) -> Result<Cow<'a, [u8]>> {
1898 match apply_smudge_filter_with_attributes_maybe_delayed(
1899 config, attributes, path, content, format, false,
1900 )? {
1901 SmudgeFilterResult::Content(data) => Ok(data),
1902 SmudgeFilterResult::Delayed { .. } => unreachable!("delay was not enabled"),
1903 }
1904}
1905
1906pub(crate) fn apply_smudge_filter_with_attributes_maybe_delayed<'a>(
1907 config: &GitConfig,
1908 attributes: &[AttributeCheck],
1909 path: &[u8],
1910 content: &'a [u8],
1911 format: ObjectFormat,
1912 allow_delay: bool,
1913) -> Result<SmudgeFilterResult<'a>> {
1914 let plan = ContentFilterPlan::resolve(config, attributes);
1915 check_wt_encoding_valid(&plan.encoding)?;
1916 let process_blob = if plan
1917 .driver
1918 .as_ref()
1919 .and_then(|driver| driver.process.as_ref())
1920 .is_some()
1921 {
1922 Some(EncodedObject::new(ObjectType::Blob, content.to_vec()).object_id(format)?)
1923 } else {
1924 None
1925 };
1926 let mut data = Cow::Borrowed(content);
1927 if plan.ident {
1928 data = ident_to_worktree_cow(format, data)?;
1929 }
1930 if plan.eol == EolConversion::Crlf
1931 && plan.convert_eol(&data)
1932 && plan.will_convert_lf_to_crlf(&data)
1933 {
1934 data = Cow::Owned(convert_lf_to_crlf(&data));
1935 }
1936 data = encode_to_worktree(&plan.encoding, path, data)?;
1940 if let Some(driver) = &plan.driver {
1941 return match run_driver_maybe_delayed(
1942 driver,
1943 driver.smudge.as_deref(),
1944 "smudge",
1945 Some(format),
1946 process_blob,
1947 path,
1948 data,
1949 allow_delay,
1950 )? {
1951 DriverFilterResult::Content(data) => Ok(SmudgeFilterResult::Content(data)),
1952 DriverFilterResult::Delayed { process } => Ok(SmudgeFilterResult::Delayed { process }),
1953 };
1954 }
1955 Ok(SmudgeFilterResult::Content(data))
1956}
1957
1958pub(crate) fn run_driver<'a>(
1960 driver: &FilterDriver,
1961 command: Option<&str>,
1962 direction: &str,
1963 format: Option<ObjectFormat>,
1964 path: &[u8],
1965 content: Cow<'a, [u8]>,
1966) -> Result<Cow<'a, [u8]>> {
1967 match run_driver_maybe_delayed(
1968 driver, command, direction, format, None, path, content, false,
1969 )? {
1970 DriverFilterResult::Content(data) => Ok(data),
1971 DriverFilterResult::Delayed { .. } => unreachable!("delay was not enabled"),
1972 }
1973}
1974
1975pub(crate) fn run_driver_maybe_delayed<'a>(
1976 driver: &FilterDriver,
1977 command: Option<&str>,
1978 direction: &str,
1979 format: Option<ObjectFormat>,
1980 process_blob: Option<ObjectId>,
1981 path: &[u8],
1982 content: Cow<'a, [u8]>,
1983 allow_delay: bool,
1984) -> Result<DriverFilterResult<'a>> {
1985 if let Some(process) = &driver.process {
1986 let blob = if direction == "smudge" {
1987 match (process_blob, format) {
1988 (Some(blob), _) => Some(blob),
1989 (None, Some(format)) => {
1990 Some(EncodedObject::new(ObjectType::Blob, content.to_vec()).object_id(format)?)
1991 }
1992 (None, None) => None,
1993 }
1994 } else {
1995 None
1996 };
1997 match run_process_filter(process, direction, path, &content, blob, allow_delay) {
1998 Ok(ProcessFilterOutcome::Filtered(output)) => {
1999 return Ok(DriverFilterResult::Content(Cow::Owned(output)));
2000 }
2001 Ok(ProcessFilterOutcome::Unsupported) => {}
2002 Ok(ProcessFilterOutcome::Status(status)) => {
2003 if allow_delay && status == "delayed" {
2004 return Ok(DriverFilterResult::Delayed {
2005 process: process.clone(),
2006 });
2007 }
2008 if driver.required {
2009 return Err(GitError::Command(format!(
2010 "external filter '{}' returned status {status}",
2011 process
2012 )));
2013 }
2014 return Ok(DriverFilterResult::Content(content));
2015 }
2016 Err(err) => {
2017 if err.protocol {
2018 eprintln!("error: external filter '{}' failed", process);
2019 }
2020 if driver.required {
2021 return Err(GitError::Command(err.message));
2022 }
2023 return Ok(DriverFilterResult::Content(content));
2024 }
2025 }
2026 }
2027 let Some(command) = command else {
2028 if driver.required {
2031 let path = String::from_utf8_lossy(path);
2032 let name = String::from_utf8_lossy(&driver.name);
2033 if direction == "clean" {
2034 eprintln!("fatal: {path}: clean filter '{name}' failed");
2035 } else {
2036 eprintln!("fatal: {path}: smudge filter {name} failed");
2037 }
2038 return Err(GitError::Exit(128));
2039 }
2040 return Ok(DriverFilterResult::Content(content));
2041 };
2042 match run_filter_command(command, path, &content) {
2043 Ok(output) => Ok(DriverFilterResult::Content(Cow::Owned(output))),
2044 Err(err) => {
2045 if driver.required {
2046 Err(err)
2047 } else {
2048 Ok(DriverFilterResult::Content(content))
2051 }
2052 }
2053 }
2054}
2055
2056pub(crate) fn filter_attribute_checks(
2059 worktree_root: &Path,
2060 path: &[u8],
2061) -> Result<Vec<AttributeCheck>> {
2062 let requested = filter_attribute_names();
2063 let mut matcher = AttributeMatcher::default();
2064 let git_dir = worktree_root.join(".git");
2065 matcher.configure_case_sensitivity(&git_dir);
2066 if !matcher.read_configured_attributes(worktree_root, &git_dir) {
2067 matcher.read_default_global_attributes();
2068 }
2069 read_dir_attribute_patterns_for_base(worktree_root, &[], &mut matcher)?;
2070 let mut prefix = Vec::new();
2071 let mut parts = path.split(|byte| *byte == b'/').peekable();
2072 while let Some(part) = parts.next() {
2073 if parts.peek().is_none() {
2074 break;
2075 }
2076 if !prefix.is_empty() {
2077 prefix.push(b'/');
2078 }
2079 prefix.extend_from_slice(part);
2080 let dir = worktree_root.join(repo_path_to_os_path(&prefix)?);
2081 read_dir_attribute_patterns_for_base(&dir, &prefix, &mut matcher)?;
2082 }
2083 read_attribute_patterns(
2084 worktree_root.join(".git").join("info").join("attributes"),
2085 &mut matcher,
2086 &[],
2087 b".git/info/attributes",
2088 false,
2089 );
2090 Ok(matcher.attributes_for_path(path, &requested, false))
2091}
2092
2093pub(crate) fn smudge_attribute_checks_from_index(
2107 worktree_root: &Path,
2108 git_dir: &Path,
2109 format: ObjectFormat,
2110 path: &[u8],
2111) -> Result<Vec<AttributeCheck>> {
2112 let requested = filter_attribute_names();
2113 let mut matcher = AttributeMatcher::default();
2114 matcher.configure_case_sensitivity(git_dir);
2115 if !matcher.read_configured_attributes(worktree_root, git_dir) {
2116 matcher.read_default_global_attributes();
2117 }
2118
2119 let index_attributes = index_gitattributes_by_base(git_dir, format)?;
2122
2123 fold_checkout_attribute_frame(worktree_root, &[], &index_attributes, &mut matcher)?;
2126 let mut prefix = Vec::new();
2127 let mut parts = path.split(|byte| *byte == b'/').peekable();
2128 while let Some(part) = parts.next() {
2129 if parts.peek().is_none() {
2130 break;
2131 }
2132 if !prefix.is_empty() {
2133 prefix.push(b'/');
2134 }
2135 prefix.extend_from_slice(part);
2136 let dir = worktree_root.join(repo_path_to_os_path(&prefix)?);
2137 fold_checkout_attribute_frame(&dir, &prefix, &index_attributes, &mut matcher)?;
2138 }
2139
2140 read_attribute_patterns(
2141 worktree_root.join(".git").join("info").join("attributes"),
2142 &mut matcher,
2143 &[],
2144 b".git/info/attributes",
2145 false,
2146 );
2147 Ok(matcher.attributes_for_path(path, &requested, false))
2148}
2149
2150pub(crate) fn fold_checkout_attribute_frame(
2155 dir: &Path,
2156 base: &[u8],
2157 index_attributes: &BTreeMap<Vec<u8>, Vec<u8>>,
2158 matcher: &mut AttributeMatcher,
2159) -> Result<()> {
2160 let worktree_file = dir.join(".gitattributes");
2161 let source = attribute_source_for_base(base);
2162 if let Ok(contents) = fs::read(&worktree_file) {
2163 read_attribute_patterns_from_bytes(&contents, matcher, base, &source);
2166 } else if let Some(contents) = index_attributes.get(base) {
2167 read_attribute_patterns_from_bytes(contents, matcher, base, &source);
2168 }
2169 Ok(())
2170}
2171
2172pub(crate) fn index_gitattributes_by_base(
2175 git_dir: &Path,
2176 format: ObjectFormat,
2177) -> Result<BTreeMap<Vec<u8>, Vec<u8>>> {
2178 let mut map = BTreeMap::new();
2179 let index_path = repository_index_path(git_dir);
2180 if !index_path.exists() {
2181 return Ok(map);
2182 }
2183 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2184 let entries = Index::parse(&fs::read(index_path)?, format)?.entries;
2185 for entry in entries {
2186 let is_attributes_file =
2187 entry.path == b".gitattributes" || entry.path.as_bytes().ends_with(b"/.gitattributes");
2188 if index_entry_stage(&entry) != 0
2189 || tree_entry_object_type(entry.mode) != ObjectType::Blob
2190 || !is_attributes_file
2191 {
2192 continue;
2193 }
2194 let base = match entry.path.as_bytes().strip_suffix(b".gitattributes") {
2195 Some(b"") => Vec::new(),
2196 Some(parent) => parent.strip_suffix(b"/").unwrap_or(parent).to_vec(),
2197 None => continue,
2198 };
2199 let object = db
2200 .read_object(&entry.oid)
2201 .map_err(|err| expect_missing_object_kind(err, entry.oid, MissingObjectKind::Blob))?;
2202 if object.object_type == ObjectType::Blob {
2203 map.insert(base, object.body.clone());
2204 }
2205 }
2206 Ok(map)
2207}
2208
2209pub(crate) fn filter_attribute_names() -> Vec<Vec<u8>> {
2210 vec![
2213 b"text".to_vec(),
2214 b"crlf".to_vec(),
2215 b"ident".to_vec(),
2216 b"eol".to_vec(),
2217 b"filter".to_vec(),
2218 b"working-tree-encoding".to_vec(),
2219 ]
2220}
2221
2222#[derive(Clone)]
2237pub(crate) struct ConvertStats {
2238 nul: u32,
2239 lonecr: u32,
2240 lonelf: u32,
2241 crlf: u32,
2242 printable: u32,
2243 nonprintable: u32,
2244}
2245
2246pub(crate) fn gather_convert_stats(buf: &[u8]) -> ConvertStats {
2247 let mut stats = ConvertStats {
2248 nul: 0,
2249 lonecr: 0,
2250 lonelf: 0,
2251 crlf: 0,
2252 printable: 0,
2253 nonprintable: 0,
2254 };
2255 let mut i = 0;
2256 while i < buf.len() {
2257 let c = buf[i];
2258 if c == b'\r' {
2259 if buf.get(i + 1) == Some(&b'\n') {
2260 stats.crlf += 1;
2261 i += 1;
2262 } else {
2263 stats.lonecr += 1;
2264 }
2265 i += 1;
2266 continue;
2267 }
2268 if c == b'\n' {
2269 stats.lonelf += 1;
2270 i += 1;
2271 continue;
2272 }
2273 if c == 127 {
2274 stats.nonprintable += 1;
2276 } else if c < 32 {
2277 match c {
2278 0x08 | 0x09 | 0x1b | 0x0c => stats.printable += 1,
2280 0 => {
2281 stats.nul += 1;
2282 stats.nonprintable += 1;
2283 }
2284 _ => stats.nonprintable += 1,
2285 }
2286 } else {
2287 stats.printable += 1;
2288 }
2289 i += 1;
2290 }
2291 if buf.last() == Some(&0x1a) {
2293 stats.nonprintable = stats.nonprintable.saturating_sub(1);
2294 }
2295 stats
2296}
2297
2298pub(crate) fn has_crlf_in_index(odb: &FileObjectDatabase, oid: &ObjectId) -> bool {
2304 let Ok(object) = odb.read_object(oid) else {
2305 return false;
2306 };
2307 if object.object_type != ObjectType::Blob {
2308 return false;
2309 }
2310 let data = &object.body;
2311 if !data.contains(&b'\r') {
2313 return false;
2314 }
2315 let stats = gather_convert_stats(data);
2316 !convert_is_binary(&stats) && stats.crlf > 0
2317}
2318
2319pub(crate) fn convert_is_binary(stats: &ConvertStats) -> bool {
2322 if stats.lonecr > 0 {
2323 return true;
2324 }
2325 if stats.nul > 0 {
2326 return true;
2327 }
2328 (stats.printable >> 7) < stats.nonprintable
2329}
2330
2331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2336pub enum ConvFlags {
2337 Off,
2339 Warn,
2342 Die,
2344}
2345
2346impl ConvFlags {
2347 pub fn from_config(config: &GitConfig) -> Self {
2353 match config.get("core", None, "safecrlf") {
2354 Some(value) if value.eq_ignore_ascii_case("warn") => ConvFlags::Warn,
2355 Some(_) => {
2356 if config.get_bool("core", None, "safecrlf") == Some(true) {
2357 ConvFlags::Die
2358 } else {
2359 ConvFlags::Off
2360 }
2361 }
2362 None => ConvFlags::Warn,
2363 }
2364 }
2365}
2366
2367pub(crate) fn check_safe_crlf(
2377 old_stats: &ConvertStats,
2378 new_stats: &ConvertStats,
2379 flags: ConvFlags,
2380 path: &[u8],
2381) -> Result<()> {
2382 if flags == ConvFlags::Off {
2383 return Ok(());
2384 }
2385 let display = String::from_utf8_lossy(path);
2386 if old_stats.crlf > 0 && new_stats.crlf == 0 {
2387 match flags {
2389 ConvFlags::Die => {
2390 eprintln!("fatal: CRLF would be replaced by LF in {display}");
2391 return Err(GitError::Exit(128));
2392 }
2393 ConvFlags::Warn => {
2394 eprintln!(
2395 "warning: in the working copy of '{display}', CRLF will be replaced by LF the next time Git touches it"
2396 );
2397 }
2398 ConvFlags::Off => unreachable!("handled above"),
2399 }
2400 } else if old_stats.lonelf > 0 && new_stats.lonelf == 0 {
2401 match flags {
2403 ConvFlags::Die => {
2404 eprintln!("fatal: LF would be replaced by CRLF in {display}");
2405 return Err(GitError::Exit(128));
2406 }
2407 ConvFlags::Warn => {
2408 eprintln!(
2409 "warning: in the working copy of '{display}', LF will be replaced by CRLF the next time Git touches it"
2410 );
2411 }
2412 ConvFlags::Off => unreachable!("handled above"),
2413 }
2414 }
2415 Ok(())
2416}
2417
2418pub(crate) fn convert_stats_ascii(content: &[u8]) -> &'static str {
2421 if content.is_empty() {
2422 return "none";
2423 }
2424 let stats = gather_convert_stats(content);
2425 if convert_is_binary(&stats) {
2426 return "-text";
2427 }
2428 match (stats.lonelf > 0, stats.crlf > 0) {
2429 (true, false) => "lf",
2430 (false, true) => "crlf",
2431 (true, true) => "mixed",
2432 (false, false) => "none",
2433 }
2434}
2435
2436pub(crate) fn convert_attr_ascii(checks: &[AttributeCheck]) -> &'static str {
2440 fn state_of<'a>(checks: &'a [AttributeCheck], name: &[u8]) -> Option<&'a AttributeState> {
2441 checks
2442 .iter()
2443 .find(|check| check.attribute == name)
2444 .and_then(|check| check.state.as_ref())
2445 }
2446
2447 #[derive(Clone, Copy, PartialEq)]
2451 enum Action {
2452 Undefined,
2453 Binary,
2454 Text,
2455 TextInput,
2456 TextCrlf,
2457 Auto,
2458 AutoCrlf,
2459 AutoInput,
2460 }
2461 fn check_crlf(state: Option<&AttributeState>) -> Action {
2462 match state {
2463 Some(AttributeState::Set) => Action::Text,
2464 Some(AttributeState::Unset) => Action::Binary,
2465 Some(AttributeState::Value(value)) if value == b"input" => Action::TextInput,
2466 Some(AttributeState::Value(value)) if value == b"auto" => Action::Auto,
2467 _ => Action::Undefined,
2469 }
2470 }
2471
2472 let mut action = check_crlf(state_of(checks, b"text"));
2475 if action == Action::Undefined {
2476 action = check_crlf(state_of(checks, b"crlf"));
2477 }
2478
2479 if action != Action::Binary {
2480 let eol = match state_of(checks, b"eol") {
2482 Some(AttributeState::Value(value)) if value == b"lf" => Some(false),
2483 Some(AttributeState::Value(value)) if value == b"crlf" => Some(true),
2484 _ => None,
2485 };
2486 action = match (action, eol) {
2487 (Action::Auto, Some(false)) => Action::AutoInput,
2488 (Action::Auto, Some(true)) => Action::AutoCrlf,
2489 (_, Some(false)) if action != Action::Auto => Action::TextInput,
2490 (_, Some(true)) if action != Action::Auto => Action::TextCrlf,
2491 _ => action,
2492 };
2493 }
2494
2495 match action {
2496 Action::Undefined => "",
2497 Action::Binary => "-text",
2498 Action::Text => "text",
2499 Action::TextInput => "text eol=lf",
2500 Action::TextCrlf => "text eol=crlf",
2501 Action::Auto => "text=auto",
2502 Action::AutoCrlf => "text=auto eol=crlf",
2503 Action::AutoInput => "text=auto eol=lf",
2504 }
2505}
2506
2507pub struct EolInfo {
2509 pub index: &'static str,
2511 pub worktree: &'static str,
2513 pub attr: &'static str,
2515}
2516
2517impl EolInfo {
2518 pub fn format_prefix(&self) -> String {
2520 format!(
2521 "i/{:<5} w/{:<5} attr/{:<17}\t",
2522 self.index, self.worktree, self.attr
2523 )
2524 }
2525}
2526
2527pub fn eol_info_for_path(
2535 worktree_root: impl AsRef<Path>,
2536 path: &[u8],
2537 index_content: Option<&[u8]>,
2538 attr_checks: &[AttributeCheck],
2539) -> EolInfo {
2540 let index = index_content.map(convert_stats_ascii).unwrap_or("");
2541
2542 let worktree_root = worktree_root.as_ref();
2543 let worktree = match repo_path_to_os_path(path) {
2544 Ok(rel) => {
2545 let absolute = worktree_root.join(rel);
2546 match fs::symlink_metadata(&absolute) {
2547 Ok(meta) if meta.file_type().is_file() => match fs::read(&absolute) {
2549 Ok(content) => convert_stats_ascii_owned(&content),
2550 Err(_) => "",
2551 },
2552 _ => "",
2553 }
2554 }
2555 Err(_) => "",
2556 };
2557
2558 let attr = convert_attr_ascii(attr_checks);
2559
2560 EolInfo {
2561 index,
2562 worktree,
2563 attr,
2564 }
2565}
2566
2567pub(crate) fn convert_stats_ascii_owned(content: &[u8]) -> &'static str {
2570 convert_stats_ascii(content)
2571}
2572
2573pub fn eol_attribute_checks(
2577 worktree_root: impl AsRef<Path>,
2578 path: &[u8],
2579) -> Result<Vec<AttributeCheck>> {
2580 filter_attribute_checks(worktree_root.as_ref(), path)
2581}