Skip to main content

git_internal/internal/
index.rs

1//! Minimal Git index (.git/index) reader/writer that maps working tree metadata to `IndexEntry`
2//! records, including POSIX timestamp handling and hash serialization helpers.
3
4#[cfg(unix)]
5use std::os::unix::fs::MetadataExt;
6#[cfg(unix)]
7use std::time::Duration;
8use std::{
9    collections::BTreeMap,
10    fmt::{Display, Formatter},
11    fs::{self, File},
12    io,
13    io::{BufReader, Read, Write},
14    path::{Path, PathBuf},
15    time::{SystemTime, UNIX_EPOCH},
16};
17
18use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
19
20use crate::{
21    errors::GitError,
22    hash::{ObjectHash, get_hash_kind},
23    internal::pack::wrapper::Wrapper,
24    utils::{self, HashAlgorithm},
25};
26
27/// POSIX time with seconds and nanoseconds
28#[derive(PartialEq, Eq, Debug, Clone)]
29pub struct Time {
30    seconds: u32,
31    nanos: u32,
32}
33impl Time {
34    /// Read Time from stream
35    pub fn from_stream(stream: &mut impl Read) -> Result<Self, GitError> {
36        let seconds = stream.read_u32::<BigEndian>()?;
37        let nanos = stream.read_u32::<BigEndian>()?;
38        Ok(Time { seconds, nanos })
39    }
40
41    /// Convert to SystemTime
42    #[allow(dead_code)]
43    fn to_system_time(&self) -> SystemTime {
44        UNIX_EPOCH + std::time::Duration::new(self.seconds.into(), self.nanos)
45    }
46
47    /// Create Time from SystemTime
48    pub fn from_system_time(system_time: SystemTime) -> Self {
49        match system_time.duration_since(UNIX_EPOCH) {
50            Ok(duration) => {
51                let seconds = duration
52                    .as_secs()
53                    .try_into()
54                    .expect("Time is too far in the future");
55                let nanos = duration.subsec_nanos();
56                Time { seconds, nanos }
57            }
58            Err(_) => panic!("Time is before the UNIX epoch"),
59        }
60    }
61}
62impl Display for Time {
63    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
64        write!(f, "{}:{}", self.seconds, self.nanos)
65    }
66}
67
68#[cfg(unix)]
69fn index_ctime(meta: &fs::Metadata) -> SystemTime {
70    unix_metadata_time(meta.ctime(), meta.ctime_nsec())
71}
72
73#[cfg(not(unix))]
74fn index_ctime(meta: &fs::Metadata) -> SystemTime {
75    meta.created()
76        .or_else(|_| meta.modified())
77        .unwrap_or(UNIX_EPOCH)
78}
79
80#[cfg(unix)]
81fn index_mtime(meta: &fs::Metadata) -> SystemTime {
82    unix_metadata_time(meta.mtime(), meta.mtime_nsec())
83}
84
85#[cfg(not(unix))]
86fn index_mtime(meta: &fs::Metadata) -> SystemTime {
87    meta.modified()
88        .or_else(|_| meta.created())
89        .unwrap_or(UNIX_EPOCH)
90}
91
92#[cfg(unix)]
93fn unix_metadata_time(seconds: i64, nanos: i64) -> SystemTime {
94    if seconds < 0 {
95        return UNIX_EPOCH;
96    }
97
98    let nanos = u32::try_from(nanos)
99        .ok()
100        .filter(|nanos| *nanos < 1_000_000_000)
101        .unwrap_or(0);
102
103    UNIX_EPOCH + Duration::new(seconds as u64, nanos)
104}
105
106/// 16 bits
107#[derive(Debug)]
108pub struct Flags {
109    pub assume_valid: bool,
110    pub extended: bool,   // must be 0 in v2
111    pub stage: u8,        // 2-bit during merge
112    pub name_length: u16, // 12-bit
113}
114
115impl From<u16> for Flags {
116    fn from(flags: u16) -> Self {
117        Flags {
118            assume_valid: flags & 0x8000 != 0,
119            extended: flags & 0x4000 != 0,
120            stage: ((flags & 0x3000) >> 12) as u8,
121            name_length: flags & 0xFFF,
122        }
123    }
124}
125
126impl TryInto<u16> for &Flags {
127    type Error = &'static str;
128    fn try_into(self) -> Result<u16, Self::Error> {
129        let mut flags = 0u16;
130        if self.assume_valid {
131            flags |= 0x8000; // 16
132        }
133        if self.extended {
134            flags |= 0x4000; // 15
135        }
136        flags |= (self.stage as u16) << 12; // 13-14
137        if self.name_length > 0xFFF {
138            return Err("Name length is too long");
139        }
140        flags |= self.name_length; // 0-11
141        Ok(flags)
142    }
143}
144
145impl Flags {
146    pub fn new(name_len: u16) -> Self {
147        Flags {
148            assume_valid: true,
149            extended: false,
150            stage: 0,
151            name_length: name_len,
152        }
153    }
154}
155
156/// An entry in the Git index file.
157pub struct IndexEntry {
158    pub ctime: Time,
159    pub mtime: Time,
160    pub dev: u32,  // 0 for windows
161    pub ino: u32,  // 0 for windows
162    pub mode: u32, // 0o100644 // 4-bit object type + 3-bit unused + 9-bit unix permission
163    pub uid: u32,  // 0 for windows
164    pub gid: u32,  // 0 for windows
165    pub size: u32,
166    pub hash: ObjectHash,
167    pub flags: Flags,
168    pub name: String,
169}
170impl Display for IndexEntry {
171    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
172        write!(
173            f,
174            "IndexEntry {{ ctime: {}, mtime: {}, dev: {}, ino: {}, mode: {:o}, uid: {}, gid: {}, size: {}, hash: {}, flags: {:?}, name: {} }}",
175            self.ctime,
176            self.mtime,
177            self.dev,
178            self.ino,
179            self.mode,
180            self.uid,
181            self.gid,
182            self.size,
183            self.hash,
184            self.flags,
185            self.name
186        )
187    }
188}
189
190impl IndexEntry {
191    /** Metadata must be got by [fs::symlink_metadata] to avoid following symlink */
192    pub fn new(meta: &fs::Metadata, hash: ObjectHash, name: String) -> Self {
193        let mut entry = IndexEntry {
194            ctime: Time::from_system_time(index_ctime(meta)),
195            mtime: Time::from_system_time(index_mtime(meta)),
196            dev: 0,
197            ino: 0,
198            uid: 0,
199            gid: 0,
200            size: meta.len() as u32,
201            hash,
202            flags: Flags::new(name.len() as u16),
203            name,
204            mode: 0o100644,
205        };
206        #[cfg(unix)]
207        {
208            entry.dev = meta.dev() as u32;
209            entry.ino = meta.ino() as u32;
210            entry.uid = meta.uid();
211            entry.gid = meta.gid();
212
213            entry.mode = match meta.mode() & 0o170000/* file mode */ {
214                0o100000 => {
215                    match meta.mode() & 0o111 {
216                        0 => 0o100644, // no execute permission
217                        _ => 0o100755, // with execute permission
218                    }
219                }
220                0o120000 => 0o120000, // symlink
221                _ =>  entry.mode, // keep the original mode
222            }
223        }
224        #[cfg(windows)]
225        {
226            if meta.is_symlink() {
227                entry.mode = 0o120000;
228            }
229        }
230        entry
231    }
232
233    /// - `file`: **to workdir path**
234    /// - `workdir`: absolute or relative path
235    pub fn new_from_file(file: &Path, hash: ObjectHash, workdir: &Path) -> io::Result<Self> {
236        let name = file.to_str().unwrap().to_string();
237        let file_abs = workdir.join(file);
238        let meta = fs::symlink_metadata(file_abs)?; // without following symlink
239        let index = IndexEntry::new(&meta, hash, name);
240        Ok(index)
241    }
242
243    /// Create IndexEntry from blob object
244    pub fn new_from_blob(name: String, hash: ObjectHash, size: u32) -> Self {
245        IndexEntry {
246            ctime: Time {
247                seconds: 0,
248                nanos: 0,
249            },
250            mtime: Time {
251                seconds: 0,
252                nanos: 0,
253            },
254            dev: 0,
255            ino: 0,
256            mode: 0o100644,
257            uid: 0,
258            gid: 0,
259            size,
260            hash,
261            flags: Flags::new(name.len() as u16),
262            name,
263        }
264    }
265}
266
267/// see [index-format](https://git-scm.com/docs/index-format)
268/// <br> to Working Dir relative path
269pub struct Index {
270    entries: BTreeMap<(String, u8), IndexEntry>,
271}
272
273impl Index {
274    pub fn new() -> Self {
275        Index {
276            entries: BTreeMap::new(),
277        }
278    }
279
280    fn check_header(file: &mut impl Read) -> Result<u32, GitError> {
281        let mut magic = [0; 4];
282        file.read_exact(&mut magic)?;
283        if magic != *b"DIRC" {
284            return Err(GitError::InvalidIndexHeader(
285                String::from_utf8_lossy(&magic).to_string(),
286            ));
287        }
288
289        let version = file.read_u32::<BigEndian>()?;
290        // only support v2 now
291        if version != 2 {
292            return Err(GitError::InvalidIndexHeader(version.to_string()));
293        }
294
295        let entries = file.read_u32::<BigEndian>()?;
296        Ok(entries)
297    }
298
299    pub fn size(&self) -> usize {
300        self.entries.len()
301    }
302
303    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, GitError> {
304        let file = File::open(path.as_ref())?; // read-only
305        let total_size = file.metadata()?.len();
306        let file = &mut Wrapper::new(BufReader::new(file)); // TODO move Wrapper & utils to a common module
307
308        let num = Index::check_header(file)?;
309        let mut index = Index::new();
310
311        for _ in 0..num {
312            let mut entry = IndexEntry {
313                ctime: Time::from_stream(file)?,
314                mtime: Time::from_stream(file)?,
315                dev: file.read_u32::<BigEndian>()?, //utils::read_u32_be(file)?,
316                ino: file.read_u32::<BigEndian>()?,
317                mode: file.read_u32::<BigEndian>()?,
318                uid: file.read_u32::<BigEndian>()?,
319                gid: file.read_u32::<BigEndian>()?,
320                size: file.read_u32::<BigEndian>()?,
321                hash: utils::read_sha(file)?,
322                flags: Flags::from(file.read_u16::<BigEndian>()?),
323                name: String::new(),
324            };
325            let name_len = entry.flags.name_length as usize;
326            let mut name = vec![0; name_len];
327            file.read_exact(&mut name)?;
328            // The exact encoding is undefined, but the '.' and '/' characters are encoded in 7-bit ASCII
329            entry.name =
330                String::from_utf8(name).map_err(|e| GitError::ConversionError(e.to_string()))?; // TODO check the encoding
331            index
332                .entries
333                .insert((entry.name.clone(), entry.flags.stage), entry);
334
335            // 1-8 nul bytes as necessary to pad the entry to a multiple of eight bytes
336            // while keeping the name NUL-terminated.
337            let hash_len = get_hash_kind().size();
338            let entry_len = hash_len + 2 + name_len;
339            let padding = 1 + ((8 - ((entry_len + 1) % 8)) % 8); // at least 1 byte nul
340            utils::read_bytes(file, padding)?;
341        }
342
343        // Extensions
344        while file.bytes_read() + get_hash_kind().size() < total_size as usize {
345            // The remaining bytes must be the pack checksum (size = get_hash_kind().size())
346            let sign = utils::read_bytes(file, 4)?;
347            println!(
348                "{:?}",
349                String::from_utf8(sign.clone())
350                    .map_err(|e| GitError::ConversionError(e.to_string()))?
351            );
352            // If the first byte is 'A'...'Z' the extension is optional and can be ignored.
353            if sign[0] >= b'A' && sign[0] <= b'Z' {
354                // Optional extension
355                let size = file.read_u32::<BigEndian>()?;
356                utils::read_bytes(file, size as usize)?; // Ignore the extension
357            } else {
358                // 'link' or 'sdir' extension
359                return Err(GitError::InvalidIndexFile(
360                    "Unsupported extension".to_string(),
361                ));
362            }
363        }
364
365        // check sum
366        let file_hash = file.final_hash();
367        let check_sum = utils::read_sha(file)?;
368        if file_hash != check_sum {
369            return Err(GitError::InvalidIndexFile("Check sum failed".to_string()));
370        }
371        assert_eq!(index.size(), num as usize);
372        Ok(index)
373    }
374
375    pub fn to_file(&self, path: impl AsRef<Path>) -> Result<(), GitError> {
376        let mut file = File::create(path)?;
377        let mut hash = HashAlgorithm::new();
378
379        let mut header = Vec::new();
380        header.write_all(b"DIRC")?;
381        header.write_u32::<BigEndian>(2u32)?; // version 2
382        header.write_u32::<BigEndian>(self.entries.len() as u32)?;
383        file.write_all(&header)?;
384        hash.update(&header);
385
386        for entry in self.entries.values() {
387            let mut entry_bytes = Vec::new();
388            entry_bytes.write_u32::<BigEndian>(entry.ctime.seconds)?;
389            entry_bytes.write_u32::<BigEndian>(entry.ctime.nanos)?;
390            entry_bytes.write_u32::<BigEndian>(entry.mtime.seconds)?;
391            entry_bytes.write_u32::<BigEndian>(entry.mtime.nanos)?;
392            entry_bytes.write_u32::<BigEndian>(entry.dev)?;
393            entry_bytes.write_u32::<BigEndian>(entry.ino)?;
394            entry_bytes.write_u32::<BigEndian>(entry.mode)?;
395            entry_bytes.write_u32::<BigEndian>(entry.uid)?;
396            entry_bytes.write_u32::<BigEndian>(entry.gid)?;
397            entry_bytes.write_u32::<BigEndian>(entry.size)?;
398            entry_bytes.write_all(entry.hash.as_ref())?;
399            entry_bytes.write_u16::<BigEndian>((&entry.flags).try_into().unwrap())?;
400            entry_bytes.write_all(entry.name.as_bytes())?;
401            let hash_len = get_hash_kind().size();
402            let entry_len = hash_len + 2 + entry.name.len();
403            let padding = 1 + ((8 - ((entry_len + 1) % 8)) % 8); // at least 1 byte nul
404            entry_bytes.write_all(&vec![0; padding])?;
405            file.write_all(&entry_bytes)?;
406            hash.update(&entry_bytes);
407        }
408
409        // Extensions
410
411        // check sum
412        let file_hash =
413            ObjectHash::from_bytes(&hash.finalize()).map_err(GitError::InvalidIndexFile)?;
414        file.write_all(file_hash.as_ref())?;
415        Ok(())
416    }
417
418    pub fn refresh(&mut self, file: impl AsRef<Path>, workdir: &Path) -> Result<bool, GitError> {
419        let path = file.as_ref();
420        let name = path
421            .to_str()
422            .ok_or(GitError::InvalidPathError(format!("{path:?}")))?;
423
424        if let Some(entry) = self.entries.get_mut(&(name.to_string(), 0)) {
425            let abs_path = workdir.join(path);
426            let meta = fs::symlink_metadata(&abs_path)?;
427            let new_ctime = Time::from_system_time(index_ctime(&meta));
428            let new_mtime = Time::from_system_time(index_mtime(&meta));
429            let new_size = meta.len() as u32;
430
431            // re-calculate SHA1/SHA256
432            let mut file = File::open(&abs_path)?;
433            let mut hasher = HashAlgorithm::new();
434            io::copy(&mut file, &mut hasher)?;
435            let new_hash = ObjectHash::from_bytes(&hasher.finalize()).unwrap();
436
437            // refresh index
438            if entry.ctime != new_ctime
439                || entry.mtime != new_mtime
440                || entry.size != new_size
441                || entry.hash != new_hash
442            {
443                entry.ctime = new_ctime;
444                entry.mtime = new_mtime;
445                entry.size = new_size;
446                entry.hash = new_hash;
447                return Ok(true);
448            }
449        }
450        Ok(false)
451    }
452}
453
454impl Default for Index {
455    fn default() -> Self {
456        Self::new()
457    }
458}
459
460impl Index {
461    /// Load index. If it does not exist, return an empty index.
462    pub fn load(index_file: impl AsRef<Path>) -> Result<Self, GitError> {
463        let path = index_file.as_ref();
464        if !path.exists() {
465            return Ok(Index::new());
466        }
467        Index::from_file(path)
468    }
469
470    pub fn update(&mut self, entry: IndexEntry) {
471        self.add(entry)
472    }
473
474    pub fn add(&mut self, entry: IndexEntry) {
475        self.entries
476            .insert((entry.name.clone(), entry.flags.stage), entry);
477    }
478
479    pub fn remove(&mut self, name: &str, stage: u8) -> Option<IndexEntry> {
480        self.entries.remove(&(name.to_string(), stage))
481    }
482
483    pub fn get(&self, name: &str, stage: u8) -> Option<&IndexEntry> {
484        self.entries.get(&(name.to_string(), stage))
485    }
486
487    pub fn tracked(&self, name: &str, stage: u8) -> bool {
488        self.entries.contains_key(&(name.to_string(), stage))
489    }
490
491    pub fn get_hash(&self, file: &str, stage: u8) -> Option<ObjectHash> {
492        self.get(file, stage).map(|entry| entry.hash)
493    }
494
495    pub fn verify_hash(&self, file: &str, stage: u8, hash: &ObjectHash) -> bool {
496        let inner_hash = self.get_hash(file, stage);
497        if let Some(inner_hash) = inner_hash {
498            &inner_hash == hash
499        } else {
500            false
501        }
502    }
503    /// is file modified after last `add` (need hash to confirm content change)
504    /// - `workdir` is used to rebuild absolute file path
505    pub fn is_modified(&self, file: &str, stage: u8, workdir: &Path) -> bool {
506        if let Some(entry) = self.get(file, stage) {
507            let path_abs = workdir.join(file);
508            let meta = path_abs.symlink_metadata().unwrap();
509            // TODO more fields
510            let same = entry.ctime == Time::from_system_time(index_ctime(&meta))
511                && entry.mtime == Time::from_system_time(index_mtime(&meta))
512                && entry.size == meta.len() as u32;
513
514            !same
515        } else {
516            panic!("File not found in index");
517        }
518    }
519
520    /// Get all entries with the same stage
521    pub fn tracked_entries(&self, stage: u8) -> Vec<&IndexEntry> {
522        // ? should use stage or not
523        self.entries
524            .iter()
525            .filter(|(_, entry)| entry.flags.stage == stage)
526            .map(|(_, entry)| entry)
527            .collect()
528    }
529
530    /// Get all tracked files(stage = 0)
531    pub fn tracked_files(&self) -> Vec<PathBuf> {
532        self.tracked_entries(0)
533            .iter()
534            .map(|entry| PathBuf::from(&entry.name))
535            .collect()
536    }
537
538    /// Judge if the file(s) of `dir` is in the index
539    /// - false if `dir` is a file
540    pub fn contains_dir_file(&self, dir: &str) -> bool {
541        let dir = Path::new(dir);
542        self.entries.iter().any(|((name, _), _)| {
543            let path = Path::new(name);
544            path.starts_with(dir) && path != dir // TODO change to is_sub_path!
545        })
546    }
547
548    /// remove all files in `dir` from index
549    /// - do nothing if `dir` is a file
550    pub fn remove_dir_files(&mut self, dir: &str) -> Vec<String> {
551        let dir = Path::new(dir);
552        let mut removed = Vec::new();
553        self.entries.retain(|(name, _), _| {
554            let path = Path::new(name);
555            if path.starts_with(dir) && path != dir {
556                removed.push(name.clone());
557                false
558            } else {
559                true
560            }
561        });
562        removed
563    }
564
565    /// saved to index file
566    pub fn save(&self, index_file: impl AsRef<Path>) -> Result<(), GitError> {
567        self.to_file(index_file)
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use std::io::Cursor;
574
575    use super::*;
576    use crate::hash::{HashKind, set_hash_kind_for_test};
577
578    /// Test Time conversion
579    #[test]
580    fn test_time() {
581        let time = Time {
582            seconds: 0,
583            nanos: 0,
584        };
585        let system_time = time.to_system_time();
586        let new_time = Time::from_system_time(system_time);
587        assert_eq!(time, new_time);
588    }
589
590    /// Test Flags conversion
591    #[test]
592    fn test_check_header() {
593        let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
594        source.push("tests/data/index/index-2");
595
596        let file = File::open(source).unwrap();
597        let entries = Index::check_header(&mut BufReader::new(file)).unwrap();
598        assert_eq!(entries, 2);
599    }
600
601    /// Test IndexEntry creation
602    #[test]
603    fn test_index() {
604        let _guard = set_hash_kind_for_test(HashKind::Sha1);
605        let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
606        source.push("tests/data/index/index-760");
607
608        let index = Index::from_file(source).unwrap();
609        assert_eq!(index.size(), 760);
610        for entry in index.entries.values() {
611            println!("{entry}");
612        }
613    }
614
615    /// Test IndexEntry creation with SHA256
616    #[test]
617    fn test_index_sha256() {
618        let _guard = set_hash_kind_for_test(HashKind::Sha256);
619        let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
620        source.push("tests/data/index/index-9-256");
621
622        let index = Index::from_file(source).unwrap();
623        assert_eq!(index.size(), 9);
624        for entry in index.entries.values() {
625            println!("{entry}");
626        }
627    }
628
629    /// Flags bit packing/unpacking covers all fields and enforces name length limit.
630    #[test]
631    fn flags_round_trip_and_length_limit() {
632        let mut flags = Flags {
633            assume_valid: true,
634            extended: true,
635            stage: 2,
636            name_length: 0x0ABC,
637        };
638        let packed: u16 = (&flags).try_into().expect("should pack");
639        let unpacked = Flags::from(packed);
640        assert_eq!(unpacked.assume_valid, flags.assume_valid);
641        assert_eq!(unpacked.extended, flags.extended);
642        assert_eq!(unpacked.stage, flags.stage);
643        assert_eq!(unpacked.name_length, flags.name_length);
644
645        flags.name_length = 0x1FFF;
646        let overflow: Result<u16, _> = (&flags).try_into();
647        assert!(overflow.is_err(), "length overflow should err");
648    }
649
650    /// IndexEntry::new_from_blob populates fields and sets flags length.
651    #[test]
652    fn index_entry_new_from_blob_populates_fields() {
653        let hash = ObjectHash::from_bytes(&[0u8; 20]).unwrap();
654        let entry = IndexEntry::new_from_blob("file.txt".to_string(), hash, 42);
655        assert_eq!(entry.name, "file.txt");
656        assert_eq!(entry.size, 42);
657        assert_eq!(entry.hash, hash);
658        assert_eq!(entry.flags.name_length, "file.txt".len() as u16);
659        assert_eq!(entry.mode, 0o100644);
660    }
661
662    /// Index container operations: add/get/tracked/dir helpers.
663    #[test]
664    fn index_add_and_query_helpers() {
665        let _guard = set_hash_kind_for_test(HashKind::Sha1);
666        let mut index = Index::new();
667        let hash = ObjectHash::from_bytes(&[1u8; 20]).unwrap();
668        let entry = IndexEntry::new_from_blob("a/b.txt".to_string(), hash, 10);
669        index.add(entry);
670
671        // get finds stage-0 by name
672        let got = index.get("a/b.txt", 0).expect("entry exists");
673        assert_eq!(got.hash, hash);
674
675        // tracked_entries/files return stage-0 paths
676        let tracked = index.tracked_entries(0);
677        assert_eq!(tracked.len(), 1);
678        let files = index.tracked_files();
679        assert_eq!(files, vec![PathBuf::from("a/b.txt")]);
680
681        // contains_dir_file true for subpath, false for exact file
682        assert!(index.contains_dir_file("a"));
683        assert!(!index.contains_dir_file("a/b.txt"));
684
685        // remove_dir_files removes under dir and returns removed names
686        let removed = index.remove_dir_files("a");
687        assert_eq!(removed, vec!["a/b.txt".to_string()]);
688        assert!(index.get("a/b.txt", 0).is_none());
689    }
690
691    /// check_header should reject bad magic/versions and accept valid header.
692    #[test]
693    fn check_header_validation() {
694        // valid header: "DIRC" + version 2 + 0 entries
695        let mut valid = Cursor::new(b"DIRC\0\0\0\x02\0\0\0\0".to_vec());
696        let entries = Index::check_header(&mut valid).expect("valid header");
697        assert_eq!(entries, 0);
698
699        // bad magic
700        let mut bad_magic = Cursor::new(b"XXXX\0\0\0\x02\0\0\0\0".to_vec());
701        assert!(Index::check_header(&mut bad_magic).is_err());
702
703        // bad version
704        let mut bad_version = Cursor::new(b"DIRC\0\0\0\x01\0\0\0\0".to_vec());
705        assert!(Index::check_header(&mut bad_version).is_err());
706    }
707
708    /// Test saving Index to file
709    #[test]
710    fn test_index_to_file() {
711        let temp_dir = tempfile::tempdir().unwrap();
712        let temp_path = temp_dir.path().join("index-760");
713
714        let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
715        source.push("tests/data/index/index-760");
716
717        let index = Index::from_file(source).unwrap();
718        index.to_file(&temp_path).unwrap();
719        let new_index = Index::from_file(temp_path).unwrap();
720        assert_eq!(index.size(), new_index.size());
721    }
722
723    /// Test IndexEntry creation from file
724    #[test]
725    fn test_index_entry_create() {
726        let _guard = set_hash_kind_for_test(HashKind::Sha1);
727        let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
728        source.push("Cargo.toml");
729
730        let file = Path::new(source.as_path()); // use as a normal file
731        let hash = ObjectHash::from_bytes(&[0; 20]).unwrap();
732        let workdir = Path::new("../");
733        let entry = IndexEntry::new_from_file(file, hash, workdir).unwrap();
734        println!("{entry}");
735    }
736
737    /// Test IndexEntry creation from file with SHA256
738    #[test]
739    fn test_index_entry_create_sha256() {
740        let _guard = set_hash_kind_for_test(HashKind::Sha256);
741        let mut source = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
742        source.push("Cargo.toml");
743
744        let file = Path::new(source.as_path());
745        let hash = ObjectHash::from_bytes(&[0u8; 32]).unwrap();
746        let workdir = Path::new("../");
747        let entry = IndexEntry::new_from_file(file, hash, workdir).unwrap();
748        println!("{entry}");
749    }
750}