1

I'm developing a function that returns the content of a particular file in a Zip archive. Since I know the location of the file, I try to access it with the ZipArchive.by_name method. However, it is possible that the name of the file is written in a different case. If this happens (FileNotFound), I need to iterate over all files in the archive and perform a case-insensitive comparison with the template. However, in this case I get two errors connected with the borrowing.

Here is the minimal example (I use BarcodeScanner Android app (../test_data/simple_apks/BarcodeScanner4.0.apk) but you can use any apk file, just substitute path to it. You can download one, e.g., on ApkMirror):

use std::{error::Error, fs::File, path::Path};

const MANIFEST_MF_PATH: &str = "META-INF/MANIFEST.MF";

fn main() {
    let apk_path = Path::new("../test_data/simple_apks/BarcodeScanner4.0.apk");
    let _manifest_data = get_manifest_data(apk_path);
}

fn get_manifest_data(apk_path: &Path) -> Result<String, Box<dyn Error>> {
    let f = File::open(apk_path)?;
    let mut apk_as_archive = zip::ZipArchive::new(f)?;

    let _manifest_entry = match apk_as_archive.by_name(MANIFEST_MF_PATH) {
        Ok(manifest) => manifest,
        Err(zip::result::ZipError::FileNotFound) => {
            let manifest_entry = apk_as_archive
                .file_names()
                .find(|f| f.eq_ignore_ascii_case(MANIFEST_MF_PATH));

            match manifest_entry {
                Some(entry) => apk_as_archive.by_name(entry).unwrap(),
                None => {
                    return Err(Box::new(zip::result::ZipError::FileNotFound));
                }
            }
        }
        Err(err) => {
            return Err(Box::new(err));
        }
    };

    Ok(String::new()) //stub
}

Cargo.toml:

[package]
name = "multiple_borrows"
version = "0.1.0"
authors = ["Yury"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
zip = "^0.5.8"

Here are the errors:

error[E0502]: cannot borrow `apk_as_archive` as immutable because it is also borrowed as mutable
  --> src/main.rs:17:34
   |
14 |     let _manifest_entry = match apk_as_archive.by_name(MANIFEST_MF_PATH) {
   |                                 ----------------------------------------
   |                                 |
   |                                 mutable borrow occurs here
   |                                 a temporary with access to the mutable borrow is created here ...
...
17 |             let manifest_entry = apk_as_archive
   |                                  ^^^^^^^^^^^^^^ immutable borrow occurs here
...
31 |     };
   |      - ... and the mutable borrow might be used here, when that temporary is dropped and runs the destructor for type `std::result::Result<ZipFile<'_>, ZipError>`

error[E0499]: cannot borrow `apk_as_archive` as mutable more than once at a time
  --> src/main.rs:22:32
   |
14 |     let _manifest_entry = match apk_as_archive.by_name(MANIFEST_MF_PATH) {
   |                                 ----------------------------------------
   |                                 |
   |                                 first mutable borrow occurs here
   |                                 a temporary with access to the first borrow is created here ...
...
22 |                 Some(entry) => apk_as_archive.by_name(entry).unwrap(),
   |                                ^^^^^^^^^^^^^^ second mutable borrow occurs here
...
31 |     };
   |      - ... and the first borrow might be used here, when that temporary is dropped and runs the destructor for type `std::result::Result<ZipFile<'_>, ZipError>`

I understand that these errors are connected with poor architectural decisions. What are the best practices in this case?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Yury
  • 20,618
  • 7
  • 58
  • 86
  • Your question might be answered by the answers of [Returning a reference from a HashMap or Vec causes a borrow to last beyond the scope it's in?](https://stackoverflow.com/q/38023871/155423); [Double mutable borrow error in a loop happens even with NLL on](https://stackoverflow.com/q/50519147/155423); [Is it safe to logically split a borrow to work around a limitation of the NLL-enabled borrow-checker?](https://stackoverflow.com/q/64902078/155423). If not, please **[edit]** your question to explain the differences. Otherwise, we can mark this question as already answered. – Shepmaster Nov 30 '20 at 15:42
  • I've added minimal working example (it's not that different from the previous one). Also, I've added my `Cargo.toml` file with zip dependency. As for the links @Shepmaster has provided, to my point of view this is not an issue of NLL so as I use the latest Rust version. – Yury Nov 30 '20 at 17:12
  • *this is not an issue of NLL so as I use the latest Rust version* — this clearly indicates that you did not completely read the links. All of the problems in the links manifest with the current **stable** compiler, Rust 1.48. – Shepmaster Nov 30 '20 at 17:35

1 Answers1

3

by_name() returns data that points inside the object that represents the archive. At the same time, it takes a &mut reference to the archive, presumably because it needs to update some internal data structures while reading. The unfortunate consequence is that you cannot call by_name() while holding on to data returned by a previous call to by_name(). In your case the arm of the match that goes on to access the archive doesn't contain references into the archive, but the borrow checker is not yet smart enough to detect that.

The usual workaround is to do it in two steps: first, determine whether the manifest file is present, and then either retrieve it by name or search for it among file_names(). In the latter case you will need to do another split, this time by cloning the name of the file before calling by_name() again. This is for the same reason: by_name() requires a mutable reference to the archive which cannot be obtained while you're holding on to the file name that refers to the data inside it. Cloning the name creates a fresh copy at a (usually negligible) run-time cost, allowing the second call to by_name().

Finally, you can combine the ok_or_else() combinator with the ? operator to simplify error propagation. With all these applied, the function could look like this:

fn get_manifest_data(apk_path: &Path) -> Result<String, Box<dyn Error>> {
    let f = File::open(apk_path)?;
    let mut apk_as_archive = zip::ZipArchive::new(f)?;

    let not_found = matches!(
        apk_as_archive.by_name(MANIFEST_MF_PATH),
        Err(zip::result::ZipError::FileNotFound)
    );
    let _manifest_entry = if not_found {
        let entry_name = apk_as_archive
            .file_names()
            .find(|f| f.eq_ignore_ascii_case(MANIFEST_MF_PATH))
            .ok_or_else(|| zip::result::ZipError::FileNotFound)?
            .to_owned();
        apk_as_archive.by_name(&entry_name).unwrap()
    } else {
        apk_as_archive.by_name(MANIFEST_MF_PATH)?
    };

    Ok(String::new()) //stub
}
user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • The root problem is that that the original code should be safe and accepted as written because the "second" mutable access is only done in case of an `Err` which _doesn't_ have a reference to `apk_as_archive`. That's what the proposed "Polonius" extension to the borrow checker would allow. – Shepmaster Nov 30 '20 at 20:04
  • 1
    @Shepmaster That's a good point, I've now amended the answer to mention it. Note, though, that the OP's code is a bit more complex than that, because it _also_ calls `by_name()` with the entry name extracted using `file_names()`, which won't be fixed by Polonius. This made it non-trivial to fix the code according to the instructions in previous answers, especially for a beginner. (The question is probably still a duplicate, just not a trivial one.) – user4815162342 Nov 30 '20 at 20:18
  • 1
    Thanks for the explanation and working code! Indeed, I am a novice in Rust: this is my first pet project in Rust, so I spend a lot of time to understand what are the errors and what cause them. So as Rust is quite different from the most of the languages, I try to understand how usual things are implemented in it. – Yury Nov 30 '20 at 21:03