Skip to content

ACP: std::fs::UserDirs or similar #830

Closed

Description

@CAD97

Proposal

Add functionality to fetch common platform user directories defined by the host operating system to std.

Problem statement

We talked about this extensively in today's @rust-lang/libs-api meeting.

@nia-e can follow-up with more details, but here's the rough sketch we wanted:

* This needs to handle failure, using `io::Result`, as well as things being missing, via `Option`.

* This should cache the results, and represent that as a struct. For instance, the `directories` crate provides a set of types like `UserDirs`, that in turn have methods returning `Option`.

So, we'd like to have have portable types (with private fields) for the set of directories, with methods on those types returning Option<PathBuf>, optionally target-specific extension traits for non-portable fields, and then target-specific functions to return (an io::Result of) the struct. That way, UNIX platforms can have a function to get the XDG version, macOS can have a function to get the macOS version (so macOS has both and users can decide), and so on.

We could also have a portable method that creates one with entirely None values, so that people can fill one in themselves.

Things we left open in the design:

* Should we have private fields with `set_` methods, or non-exhaustive public fields? The latter are easier to work with, but the former means we can add fields via target-specific extension traits.

I'm opening this here mostly to track that this needs meaningful API review from T-libs-api as to whether this is the right shape for std.

Solution sketch

// mod std::fs
struct HomeDirs { /* ... */ }
impl HomeDirs {
    fn empty() -> Self;
    fn take(&mut self) -> Self;

    fn config_home(&self) -> Option<&Path>;
    fn data_home(&self) -> Option<&Path>;
    fn state_home(&self) -> Option<&Path>;
    fn cache_home(&self) -> Option<&Path>;

    fn set_config_home(&mut self, path: PathBuf) -> &mut Self;
    fn set_data_home(&mut self, path: PathBuf) -> &mut Self;
    fn set_state_home(&mut self, path: PathBuf) -> &mut Self;
    fn set_cache_home(&mut self, path: PathBuf) -> &mut Self;
}

struct MediaDirs { /* ... */ }
impl MediaDirs {
    fn empty() -> Self;
    fn take(&mut self) -> Self;

    fn desktop(&self) -> Option<&Path>;
    fn documents(&self) -> Option<&Path>;
    fn downloads(&self) -> Option<&Path>;
    fn music(&self) -> Option<&Path>;
    fn pictures(&self) -> Option<&Path>;
    fn videos(&self) -> Option<&Path>;

    fn set_desktop(&mut self, path: PathBuf) -> &mut Self;
    fn set_documents(&mut self, path: PathBuf) -> &mut Self;
    fn set_downloads(&mut self, path: PathBuf) -> &mut Self;
    fn set_music(&mut self, path: PathBuf) -> &mut Self;
    fn set_pictures(&mut self, path: PathBuf) -> &mut Self;
    fn set_videos(&mut self, path: PathBuf) -> &mut Self;
}

// mod std::os::darwin::fs
trait HomeDirsExt for HomeDirs {
    fn sysdir() -> io::Result<Self>;
}

trait MediaDirsExt for MediaDirs {
    fn sysdir() -> io::Result<Self>;
}

// mod std::os::unix::fs
trait HomeDirsExt for HomeDirs {
    fn xdg() -> io::Result<Self>;

    fn runtime_home(&self) -> Option<&Path>;
    fn config_dirs(&self) -> Option<env::SplitPaths<'_>>;
    fn data_dirs(&self) -> Option<env::SplitPaths<'_>>;

    fn set_runtime_home(&mut self, path: PathBuf) -> &mut Self;
    fn set_config_dirs(&mut self, paths: impl IntoIterator<Item: AsRef<OsStr>>) -> Result<&mut Self, env::JoinPathsError>;
    fn set_data_dirs(&mut self, paths: impl IntoIterator<Item: AsRef<OsStr>>) -> Result<&mut Self, env::JoinPathsError>;
}

trait MediaDirsExt for MediaDirs {
    fn xdg() -> io::Result<Self>;

    fn templates(&self) -> Option<&Path>;
    fn set_templates(&mut self, path: PathBuf) -> &mut Self;
}

// mod std::os::windows::fs
trait HomeDirsExt for HomeDirs {
    fn known_folders() -> io::Result<Self>;
}

trait MediaDirsExt for MediaDirs {
    fn known_folders() -> io::Result<Self>;
}

Alternatives

  • Don't put this in std, tell people to use ecosystem crates. Works, but perpetuates the idea that you need to pull in a huge number of dependencies to do anything nontrivial in Rust.
  • Provide this functionality in some sort of project-blessed standard library extensions meta-package that has the ability to make breaking changes, unlike std.
  • Only include the user base directories (cache/config/data/state) and leave media directories to the ecosystem. Application auxiliary files is a concern to almost all nontrivial applications, but media directories typically only matter to graphical applications using a file picker.
  • Don't consider "state" a portable concept; the directories crate doesn't. @CAD97's counterargument: Yes, state is a recent addition to XDG (2021), doesn't have a /usr/local analogue, and is a logical subtype of data. But the split being whether the files are meaningful to the user makes sense, and that translates nicely to treating state as local (non-roaming) data on Windows.

Links and related work

What happens now?

This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.

Activity

  1. programmerjake commented on Jul 14, 2026

    @programmerjake
  2. CAD97 commented on Jul 15, 2026

    @CAD97
    Author
  3. joshtriplett commented on Jul 21, 2026

    @joshtriplett
    Member

    We talked about this in today's @rust-lang/libs-api meeting.

    Changes from that meeting's discussion:

    • Remove empty in favor of Default; Default is in the prelude, and we can document that people can use HomeDirs::default() to get an empty version.
    • Remove take. With Default implemented, you can use mem::take if you need it, and we don't expect it to be common enough to warrant a more dedicated method.

    I'll post a separate comment with things I noticed after the meeting.

  4. joshtriplett commented on Jul 21, 2026

    @joshtriplett
    Member

    I do think we should include state. I would suggest that when we document this, we give examples. For instance:

    • config is for configuration the user created explicitly (whether directly by editing a config file, or indirectly by modifying a setting in the application).
    • state is for transient state that should persist from run-to-run, but that the user didn't explicitly create; for instance, "what was the last position of my window" or "what audio device am I playing music to". A user might, for instance, choose to synchronize the contents of config but not state.
  5. joshtriplett commented on Jul 21, 2026

    @joshtriplett
    Member

    Some additional things I noticed:

    • All the extension traits should be sealed, so that we can extend them later.
    • template should be in the portable version, because it exists on multiple platforms. Windows and UNIX both have it.
    • Typo: std::os::unix::fx should say std::os::unix::fs.
    • dirs/directories distinguishes between versions of data_dir and config_dir that (on Windows) return FOLDERID_RoamingAppData versus FOLDERID_LocalAppData. We should confirm which one we want to have the default name (dirs/directories uses the former), and whether we want to add a portable version of the latter so that it doesn't only exist in the Windows extension trait.
    • MediaDirsExt for UNIX systems should have executable_dir (commonly ~/.local/bin).

    And several bits of feedback on the the methods for config_dirs/data_dirs:

    • I don't think the iterators should use SplitPaths. That iterator has to use PathBuf, because it's reading from the environment, so it needs to avoid having internal references. I think this iterator can safely return &Path (referencing the internal OsString), and people can to_owned() if they need a PathBuf. More commonly, they might call .join or similar.
    • Those methods shouldn't return Option of an iterator; return an empty iterator instead.
    • We should document that the directories are returned in order of precedence (highest precedence first), and that they don't include the base directory (e.g. config_dirs doesn't include config_dir).
    • I think set_config_dirs and set_data_dirs should use AsRef<Path> rather than AsRef<OsStr>. This still allows using OsStr if you want, but the individual entries in it are specifically paths.
    • I think we should have versions of set_config_dirs and set_data_dirs that accept a single OsString separated by the platform path separator; that'll be the common case for many things in the ecosystem that are setting up one of these. I'd suggest, for instance, set_config_dirs_str taking an OsString and set_config_dirs taking an impl Iterator<Item = &Path>.
  6. joshtriplett commented on Jul 21, 2026

    @joshtriplett
    Member

    One more issue to consider:

    Typically, at least on XDG systems, obtaining media dirs is going to require obtaining home dirs. This would mean if you need both, you'd have to retrieve the home dirs twice. That isn't too expensive of an operation, though, compared to all the memory allocation here. Are we okay with declaring that we don't care about that minor inefficiency? IIRC, the operations involved in the base directory retrieval are just getenv and a bit of string manipulation; it's media dirs that require more work (reading user-dirs files).

  7. joshtriplett commented on Jul 21, 2026

    @joshtriplett
    Member

    And finally, naming bikeshed:

    I like HomeDirs better than BaseDirs here, so 👍 for that. I wonder if the other one should be MediaDirs, or UserDirs. It won't just be media.

  8. Mark-Simulacrum commented on Jul 22, 2026

    @Mark-Simulacrum
    Member

    Remove empty in favor of Default; Default is in the prelude, and we can document that people can use HomeDirs::default() to get an empty version.

    Is Default being empty really the expected thing? It seems pretty surprising to me for default to not do a reasonable default thing (e.g., provide a platform-specific recommendation or similar). empty() seems like a much clearer name than ::default() for this.

    What was the rationale in the meeting for this decision? I don't see meeting notes linked or a summary of why a decision was made here and that seems like critical information to include when summarizing such discussion.

  9. added
    I-libs-nominatedIndicates that an issue has been nominated for discussion during a team meeting.
    on Jul 22, 2026
  10. joshtriplett commented on Jul 22, 2026

    @joshtriplett
    Member

    Remove empty in favor of Default; Default is in the prelude, and we can document that people can use HomeDirs::default() to get an empty version.

    Is Default being empty really the expected thing? It seems pretty surprising to me for default to not do a reasonable default thing (e.g., provide a platform-specific recommendation or similar). empty() seems like a much clearer name than ::default() for this.

    We don't want to provide a platform-specific recommendation as the default because on some platforms (e.g. macOS) we don't have one clear platform-specific recommendation (because some programs will want sysdir and some will want xdg).

    Default is pretty commonly empty, but you make a reasonable point that people might misinterpret it. Ill re-nominate to discuss that.

    The rationale in the meeting was pretty light, mostly the observation that the empty and take methods seemed redundant compared to how we often use Default and would get mem::take automatically if we did. IIRC someone mentioned the possibility of Default doing the platform-specific thing, but leaving aside the above issue that some platforms won't have a default we want to recommend (which we didn't observe explicitly in the meeting but it was one of our original rationales leading to this design), in general we didn't want to call platform-specific code from Default.

  11. joshtriplett commented on Jul 22, 2026

    @joshtriplett
    Member

    Related thought: as written, these two types could be in alloc, and only the extension-trait methods need to be in std.

  12. CAD97 commented on Jul 22, 2026

    @CAD97
    Author

    In response to @joshtriplett's feedback:

    Remove empty in favor of Default;

    I explicitly avoided Default/new because the user expectation may be for the default constructor to use the "default platform conventions," whatever those are. This is the behavior of the directories crate, for example. Naming the portable constructor empty IMHO creates a pit of success in that it creates an affordance for the user to discover that there isn't a consistent answer for "default platform conventions" and to consider what conventions they want to use explicitly.

    If libs-api disagrees that this is a significant factor, I'd be happy to make the types default-empty.

    All the extension traits should be sealed

    They are in the impl, I just elided that here for brevity, the same way I made up the trait Trait for Self summary syntax.

    template should be in the portable version

    {FOLDERID_Templates} is a PERUSER folder. But I have zero confidence that the semantics of the "templates" folder is meaningfully similar between XDG and Windows. I even think that templates need to be added to the registry on Windows and the PERUSER templates folder is only really used to override template contents from the COMMON templates folder.

    We would need to ask a few Windows experts what the purpose of {FOLDERID_Templates} is before I'd be confident saying it's similar enough to the XDG concept to expose it in the portable interface.

    I would suggest that when we document this, we give examples [of what to put where].

    In rust-lang/rust#158936, I now have:

    • "Cache" files are temporary data that can be used to cache redundant work of an application, but which can be discarded arbitrarily and recreated as necessary. Files in this directory may potentially be automatically purged any time they are not currently open, or they may not, depending on system configuration. A robust application should ensure that its caches do not grow unboundedly.
    • "Config" files are configuration managed by the user, either by editing the files directly or through a managing application. Configuration is generally expected to be meaningful to the user and portable enough to back up and synchronize across the same user's account on multiple systems.
    • "Data" files are application-specific data that is meaningful to the user in some way and does not implicitly rely on system configuration or details of how the application is installed otherwise irrelevant to the user. As such, data makes sense to back up and synchronize between the same user's account on multiple systems. If a file is specific to a single machine, it's probably [state](Self::state_home).
    • "State" files are data that should persist between application restarts, but which is not important nor portable enough to the user to be stored in the [data](Self::data_home) directory. Common examples include history (such as logs, recently used files, etc) and any current state of the application that should be reused (such as view, layout, open files, undo history, etc).

    Suggestions on what examples to add to cache/config/data are very welcome.

    versions of data_dir and config_dir that (on Windows) return FOLDERID_RoamingAppData versus FOLDERID_LocalAppData. We should confirm which one we want to have the default name (dirs/directories uses the former), and whether we want to add a portable version of the latter so that it doesn't only exist in the Windows extension trait.

    It might make sense for data_dir1, since the Windows folders are "Roaming App Data" and "Local App Data." But my argument is that "local_data_dir" is state_dir. The directories crate here is conservative and only sets state_dir on Linux systems, but IMHO if an application has a reason to put data in the non-roaming AppData folder, that should also be a reason to put it in the state directory on XDG systems. Especially since OS-level roaming of Windows AppData doesn't exist anymore2, and it's on the system administrator to set up if they want it.

    MediaDirsExt for UNIX systems should have executable_dir

    The XDG Base Directory Specification says "User-specific executable files may be stored in $HOME/.local/bin. Distributions should ensure this directory shows up in the UNIX $PATH environment variable, at an appropriate place." As such it maybe should be on HomeDirs? But also there's no discovery specified other than "use this path." Plus $XDG_BIN_HOME is a somewhat-common extension to XDG to allow customizing the path (with default of $HOME/.local/bin.

    This is just far enough out of the "portable applications will be interested in this" scope into "where/how to install" that I don't think it should be part of the initial scope for this API, like how neither appid directory subpaths nor the $datadir/$sysconfdir-relative installation are in scope, despite instructions for such being in the XDG spec.

    I don't think the iterators should use SplitPaths. That iterator has to use PathBuf,

    I personally lean towards directly using std::env::split_paths instead of duplicating the logic to provide a &Path-iterating form on Unix. Everything abut the XDG spec is theoretically functional on non-UNIX operating systems, and this is defined to join/split paths as with the environment PATH variable3.

    It also means we don't have to expose some additional std::os::unix::fs::XdgPaths (and potentially std::os::unix::fs::JoinXdgPathsError) if we just use the std::env way of manipulating PATH-like values.

    If libs-api would prefer a separate iterator, I'm not strongly against it, I just think there's value in reusing the std::env version that isn't captured in a simple "this could iterate &Path instead of PathBuf" note.

    I think set_config_dirs and set_data_dirs should use AsRef<Path> rather than AsRef<OsStr>.

    std::env::join_paths uses AsRef<OsStr>; I chose to match that. Without a good reason to diverge join (like exists with ownership for split), it seems better to match that signature.

    I think we should have versions of set_config_dirs and set_data_dirs that accept a single OsString

    It might make sense to only have that version and have the caller do env::join_paths if they don't have the joined string already.

    Footnotes

    1. The only real reason I can see for wanting config_dir to be LocalAppData is to keep everything in one directory on Windows instead of split across two. And in such a case, I expect the same desire would apply equally to an XDG system and they'd prefer to just stick everything in $XDG_DATA_HOME or $XDG_STATE_HOME instead of only altering how the Windows install does things. ↩

    2. Even with the behavior of new Windows installs pushing users to locate their media directories in the OneDrive synced location, that's only done to the media folder and not either AppData folder. ↩

    3. With a bit of an asterisk: the versioned 0.8 revision of the spec states that XDG_{CONFIG|DATA}_DIRS "should be seperated with a colon :" whereas the unversioned latest revision of the spec states that they "should be separated with the separator used for $PATH on the platform (typically this is a colon :)" instead, specifically to include the in-practice usage of the XDG base directories on Windows, despite never being designed for usage on a non-UNIX system. ↩

  13. CAD97 commented on Jul 22, 2026

    @CAD97
    Author

    as written, these two types could be in alloc,

    While true, I don't think these types are meaningful without an operating system. So it depends on policy to decide whether they should go in std (they imply the presence of an operating system) or alloc (they don't require the presence of an operating system)

  14. 2 remaining items

  15. joshtriplett commented on Jul 27, 2026

    @joshtriplett
    Member

    Responding here, and I'll post a second comment summarizing for libs-api.

    I explicitly avoided Default/new because the user expectation may be for the default constructor to use the "default platform conventions," whatever those are. This is the behavior of the directories crate, for example. Naming the portable constructor empty IMHO creates a pit of success in that it creates an affordance for the user to discover that there isn't a consistent answer for "default platform conventions" and to consider what conventions they want to use explicitly.

    That makes sense; I agree. Let's keep empty and take then.

    They are in the impl, I just elided that here for brevity, the same way I made up the trait Trait for Self summary syntax.

    👍

    {FOLDERID_Templates} is a PERUSER folder. But I have zero confidence that the semantics of the "templates" folder is meaningfully similar between XDG and Windows. I even think that templates need to be added to the registry on Windows and the PERUSER templates folder is only really used to override template contents from the COMMON templates folder.

    We would need to ask a few Windows experts what the purpose of {FOLDERID_Templates} is before I'd be confident saying it's similar enough to the XDG concept to expose it in the portable interface.

    Yes, templates are added to the shell using the registry. Adding a file to the templates directory does nothing on its own, Also there's often no need to create a template file if the new file should be blank or only contain relatively few bytes or the template is in the application's install directory.

    Does this mean that directories is just incorrect to map them to the same place?

    👍 for leaving it out for now.

    "State" files are data that should persist between application restarts, but which is not important nor portable enough to the user to be stored in the data directory. Common examples include history (such as logs, recently used files, etc) and any current state of the application that should be reused (such as view, layout, open files, undo history, etc).

    I would add something like "or synchronized between systems" to the end of the first sentence here, but otherwise LGTM.

    It might make sense for data_dir1, since the Windows folders are "Roaming App Data" and "Local App Data." But my argument is that "local_data_dir" is state_dir. The directories crate here is conservative and only sets state_dir on Linux systems, but IMHO if an application has a reason to put data in the non-roaming AppData folder, that should also be a reason to put it in the state directory on XDG systems. Especially since OS-level roaming of Windows AppData doesn't exist anymore2, and it's on the system administrator to set up if they want it.

    I do agree that we should have state_dir on all platforms.

    I think I buy the argument that we don't necessarily need to add the local_ variants, at least not right away. And I think I agree that the defaults should be the "remote/roaming" ones.

    The XDG Base Directory Specification says "User-specific executable files may be stored in $HOME/.local/bin. Distributions should ensure this directory shows up in the UNIX $PATH environment variable, at an appropriate place." As such it maybe should be on HomeDirs?

    Valid; since it's in the base directory spec, it should be in HomeDirs.

    But also there's no discovery specified other than "use this path." Plus $XDG_BIN_HOME is a somewhat-common extension to XDG to allow customizing the path (with default of $HOME/.local/bin.

    That variable is non-standard and not part of the upstream spec; I think we should ignore that variable.

    I think, since the directory is commonly used and in the specification, it'd be worth making it available, albeit in the unix HomeDirsExt since there's unfortunately no equivalent on other platforms. But if there's some other reason we shouldn't include it, it's certainly not critical.

    I personally lean towards directly using std::env::split_paths instead of duplicating the logic to provide a &Path-iterating form on Unix. Everything abut the XDG spec is theoretically functional on non-UNIX operating systems, and this is defined to join/split paths as with the environment PATH variable3.

    It also means we don't have to expose some additional std::os::unix::fs::XdgPaths (and potentially std::os::unix::fs::JoinXdgPathsError) if we just use the std::env way of manipulating PATH-like values.

    I do agree that it's more code. To be clear, though, I was imagining adding a portable split_paths_ref and using that, rather than something XDG-specific.

    I think there's a bit of value in avoiding the unnecessary allocations. I don't personally feel that strongly about it, but I think we should in general avoid forcing allocations on the user when they aren't necessary.

    It might make sense to only have that version and have the caller do env::join_paths if they don't have the joined string already.

    I think that's a really good point. We're not adding any value by having a function that just calls join_paths for the user. Having just the versions that accept an OsString seems fine. (And that makes the argument type issue moot.)

    While true, I don't think these types are meaningful without an operating system. So it depends on policy to decide whether they should go in std (they imply the presence of an operating system) or alloc (they don't require the presence of an operating system)

    The main reason would be if someone wanted to make some code portable for both std and no-std, using the option to fill it in themselves. shrug, not particularly important.

    I've realized that there's another reason to not directly use SplitPaths: we should probably be filtering out any non-absolute paths in the list.

    Yeah, the specification confirms that:

    All paths set in these environment variables must be absolute. If an implementation encounters a relative path in any of these variables it should consider the path invalid and ignore it.

    split_paths_ref could still provide the bulk of the implementation here, and then the rest is a filter.

  16. joshtriplett commented on Jul 27, 2026

    @joshtriplett
    Member

    I think the only thing that needs libs-api review here is agreeing that we shouldn't have Default after all.

  17. added
    ACP-acceptedAPI Change Proposal is accepted (seconded with no objections)
    and removed
    I-libs-nominatedIndicates that an issue has been nominated for discussion during a team meeting.
    on Jul 28, 2026
  18. joshtriplett commented on Jul 28, 2026

    @joshtriplett
    Member

    We talked about this in today's libs-api meeting.

    We agreed that we shouldn't have Default, and should keep the empty method. Thanks for the feedback on that one!

    We also agreed that we don't need the take method. We thought it'd be sufficient that people can call mem::replace, passing empty.

    With that changed, ACP accepted. Thank you! Looking forward to having this in std.

    Please close this once you've opened a tracking issue.

  19. CAD97 commented on Aug 31, 2026

    @CAD97
    Author

    Tracking issues created:

    I split the home and media dirs tracking since while they share a lot, they differ significantly in the "should std carry these" discussion.

  20. added a commit that references this issue on Sep 28, 2026
  21. added a commit that references this issue on Sep 28, 2026
  22. added a commit that references this issue on Sep 29, 2026
  23. added 2 commits that reference this issue on Sep 29, 2026
  24. added a commit that references this issue on Sep 30, 2026
  25. added a commit that references this issue on Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ACP-acceptedAPI Change Proposal is accepted (seconded with no objections)api-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions