Rust: Reusing Variable In Loop

The following chunk of code tries to parse a git index file. The Index::read_entry() function is meant to be reading out each file name and it’s respective SHA form the git index. The code was actually reading out the first file entry over and over, for the number of files in the index. The first file happend to be the .gitignore file.

pub fn new(path: &Path) -> Result<Index, GitStatusError> {
    let oid: [u8; 20] = [0; 20];
    let mut buffer: Vec<u8> = Vec::new();
    File::open(&path).and_then(|mut f| f.read_to_end(&mut buffer))?;
    let (contents, header) = Index::read_header(&buffer)?;
    let mut entries = vec![];
    for _ in 0..header.entries {
        let (contents, entry) = Index::read_entry(&contents)?;
        entries.push(entry);
    }
    let index = Index {
        path: String::from(path.to_str().unwrap()),
        oid,
        header,
        entries,
    };
    Ok(index)
}

The following line is meant to return the slice after the next entry and the next entry. However each time it was processed in the loop it did not appear to be updating contents to the location after the next entry.

let (contents, entry) = Index::read_entry(&contents)?; 

I had a couple of tests around Index::read_entry(), but was missing a test that ensured the returned value was after the next entry, so I added it. I haven’t had a chance to clean up the tests, there is a bit too much setup repeated in each one. The important thing to look at is the suffix variable. It is placed after the file name in the u8 vector, and the assert assures that the function returns the slice for the suffix, as well as the file entry.

#[test]
fn test_read_of_file_entry_leaves_remainder() {
    let name= b"a/file";
    let sha = b"ab7ca9aba237a18e3f8a";
    let mut stream: Vec<u8> = vec![0; 40];
    stream.extend(sha);
    let name_length: u16 = name.len() as u16;
    stream.extend(&name_length.to_be_bytes());
    stream.extend(name);
    let suffix = b"what";
    stream.extend(suffix);
    let read = Index::read_entry(&stream);
    assert_eq!(
        read,
        Ok((&suffix[..], Entry {sha: *sha, name: String::from_utf8(name.to_vec()).unwrap()}))
    );
}

This test passed, so clearly the function was returning the &[u8] slice after the next entry. I decided to focus on how I had written my attempt at updating the value of contents.

let (contents, header) = Index::read_header(&buffer)?;
let mut entries = vec![];
for _ in 0..header.entries {
    let (contents, entry) = Index::read_entry(&contents)?;
    entries.push(entry);
}

With a bit of investigation I finally realized the issue is the let keyword. let declares new variables. Since the contents of the for loop is in a new block there is a new contents variable declared which shadows the original one. However each iteration the for loop’s contents variable hasn’t been created yet so it re-uses the outer scope’s contents variable in the Index::read_entry.

I tried to resolve this issue by declaring entry and then assigning:

for _ in 0..header.entries {
    let entry;
    (contents, entry) = Index::read_entry(&contents)?;
    entries.push(entry);
}

However this resulted in the following compilation error:

   |
88 |             (contents, entry) = Index::read_entry(&contents)?;
   |             ----------------- ^
   |             |
   |             cannot assign to this expression
   |
   = note: destructuring assignments are not currently supported
   = note: for more information, see https://github.com/rust-lang/rfcs/issues/372

It’s nice that the compilation error directly links to the issue tracking the implementation of destructuring assignments.

The final solution, create a local variable in the assignment and then assign that to the contents variable.

for _ in 0..header.entries {
    let (local_contents, entry) = Index::read_entry(&contents)?;
    entries.push(entry);
    contents = local_contents;
}