Make code portable between platforms

I'm implementing a file manager. It supposes to work on Windows and Linux. Since Windows file system doesn't care about cases of characters of path names, I convert all names in upper case on Windows. So I use a conditional compilation to handle that. However, I do not know how to make it concise and clear and asked AI for that. It answered like:


Concise takeaway

Use the second version. It keeps the control‑flow simple:

  • On Windows → define mut filename and uppercase it
  • On non‑Windows → define a normal filename
    No unused‑mut, no duplicated logic, no conditional mutation.

:puzzle_piece: Why the second version is better

1. No need for #[allow(unused_mut)]

In your first snippet:

#[allow(unused_mut)]
let mut filename = ...
#[cfg(target_os = "windows")]
filename.make_ascii_uppercase();

On non‑Windows, filename is never mutated, so mut is unused.
You silence the warning, but the warning is correct: the variable shouldn’t be mut on non‑Windows.

The second version avoids this entirely.


2. No duplicated variable definitions

Your first version defines filename once, but the mutation is conditional.
Your second version defines filename differently depending on the OS:

#[cfg(not(target_os = "windows"))]
let filename = ...;

#[cfg(target_os = "windows")]
let mut filename = ...;
#[cfg(target_os = "windows")]
filename.make_ascii_uppercase();

This is clearer: each platform gets the correct variable type.


3. More idiomatic conditional compilation

Rust’s #[cfg] is meant to select whole blocks of code, not sprinkle tiny conditionals everywhere.
The second version respects that.


:star: Best possible version (even cleaner)

If you want the most compact and idiomatic form:

let filename = {
    let mut name = entry_path.file_name()?.to_str()?.to_string();
    #[cfg(target_os = "windows")]
    name.make_ascii_uppercase();
    name
};

Advantages:

  • One variable definition
  • Mutation only happens on Windows
  • No unused_mut
  • No duplicated code
  • Very readable

:wrench: Recommendation

Use the block version above. It’s the cleanest and avoids all the pitfalls.

If you want, I can help you refactor the surrounding code too.

Do you agree with AI suggestion? I'm still uncertain.

I don't know how feature-conformant you're aiming to be, but I'll note that case-sensitivity is possible on Windows. (I believe it's supported on a per-directory basis (though I'd be surprised if it worked on FAT)).

It's NTFS, however Mac has the feature as well.

That's wrong. NTFS does not care about path name casing but other drives, e.g. network, may well be case sensitive. Also there's per-directory attributes apparently to even change this for NTFS. Rust's std uses FILE_FLAG_POSIX_SEMANTICS for a few operations on paths that would be dangerous to select different files accidentally, e.g. delete and rename. You can set that flag yourself for OpenOptions via OpenOptionsExt.

In neither case should you uppercase the filename. If the fs is case insensitive it'll do its thing regardless of casing, if it is case sensitive the make_ascii_uppercase is dangerously incorrect. If this originates from an attempt to compare files by their paths, reconsider. That's almost never the correct intended semantics.


As for the unused question, I'd consider doing the main work in a separate function which is defined for all platforms but a no-op on other OS's. This could be a trait but it does not need to be.

#[cfg(target_os = "windows")]
fn prepare_file(file: &mut OsStr) { // questionable code here
}

#[cfg(not(target_os = "windows"))]
fn prepare_file(_: &mut OsStr) {}

let mut filename = …
prepare_file(&mut filename); // This is now always mut.

The advantage here is that the signature is a consistent maximum requirement on ownership. So you can't accidentally paint yourself into a corner by having one platform have a structure that requires a shared references to exist; which would then bite you when you try to implement windows and can't create a mutable reference.

It's a very good input, but I need something more practical. For example, a user typed

DEL *.TXT

The directory contains files:

last_work.txt
resume.txt
Visit.TXT

Which files should I delete?

Another problem, a user decided to create file - Resume.txt. Will (s)he overwrite file resume.txt? Or another file will be created?

Note that this specific question depends on implementation of whatever you use for expanding a glob. Both case sensitivity of the file system and bytes used to store paths in PathBuf are somewhat[1] orthogonal to that.

Zsh, for example, has CASE_GLOB setting, which, if unset, will allow globbing to be case-insensitive on linux.

I would suggest going for such a setting too, but have different defaults on different systems. The better question would be whether you should treat visit.txt as a glob or not.

I would suggest not trying to be smart here. Just create file with create_new and ask user if it fails with an error which indicates that file exists, then try with truncate if user confirms that they indeed want to destroy existing file.

Do not try predicting a failure, do the requested action and handle failure if it happens. Or, in general, do exactly the same thing you do to handle TOCTOU problem – you do have an answer for what to do if Resume.txt appeared between you listing files in a directory and user requesting to create it while not knowing that it did?

If asking for confirmation is not an option then let user specify in advance what they want to do in such case.


  1. This and especially data in PathBuf may make one or the other implementation simpler. ↩︎

Thank you guys. You excavated a pretty serious problem in my implementation. Say more, Rust std can't retrieve a file name you used at its creation, and it's happy with any name you want to give it. So AI pointed me directly to Windows API to resolve it. Now AI works hard to address all Windows problems. Thanks again.