Repository navigation
ACP: std::fs::UserDirs or similar #830
Description
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 (anio::Resultof) 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
Nonevalues, 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/localanalogue, 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
programmerjake commentedon Jul 14, 2026
CAD97 commentedon Jul 15, 2026
joshtriplett commentedon Jul 21, 2026
We talked about this in today's @rust-lang/libs-api meeting.
Changes from that meeting's discussion:
emptyin favor ofDefault;Defaultis in the prelude, and we can document that people can useHomeDirs::default()to get an empty version.take. WithDefaultimplemented, you can usemem::takeif 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.
joshtriplett commentedon Jul 21, 2026
I do think we should include
state. I would suggest that when we document this, we give examples. For instance:configis for configuration the user created explicitly (whether directly by editing a config file, or indirectly by modifying a setting in the application).stateis 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 ofconfigbut notstate.joshtriplett commentedon Jul 21, 2026
Some additional things I noticed:
templateshould be in the portable version, because it exists on multiple platforms. Windows and UNIX both have it.std::os::unix::fxshould saystd::os::unix::fs.dirs/directoriesdistinguishes between versions ofdata_dirandconfig_dirthat (on Windows) returnFOLDERID_RoamingAppDataversusFOLDERID_LocalAppData. We should confirm which one we want to have the default name (dirs/directoriesuses 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.executable_dir(commonly~/.local/bin).And several bits of feedback on the the methods for
config_dirs/data_dirs:SplitPaths. That iterator has to usePathBuf, 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 internalOsString), and people canto_owned()if they need aPathBuf. More commonly, they might call.joinor similar.Optionof an iterator; return an empty iterator instead.config_dirsdoesn't includeconfig_dir).set_config_dirsandset_data_dirsshould useAsRef<Path>rather thanAsRef<OsStr>. This still allows usingOsStrif you want, but the individual entries in it are specifically paths.set_config_dirsandset_data_dirsthat accept a singleOsStringseparated 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_strtaking anOsStringandset_config_dirstaking animpl Iterator<Item = &Path>.joshtriplett commentedon Jul 21, 2026
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).
joshtriplett commentedon Jul 21, 2026
And finally, naming bikeshed:
I like
HomeDirsbetter thanBaseDirshere, so 👍 for that. I wonder if the other one should beMediaDirs, orUserDirs. It won't just be media.Mark-Simulacrum commentedon Jul 22, 2026
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.
joshtriplett commentedon Jul 22, 2026
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
sysdirand some will wantxdg).Defaultis 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
emptyandtakemethods seemed redundant compared to how we often useDefaultand would getmem::takeautomatically if we did. IIRC someone mentioned the possibility ofDefaultdoing 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 fromDefault.joshtriplett commentedon Jul 22, 2026
Related thought: as written, these two types could be in
alloc, and only the extension-trait methods need to be instd.CAD97 commentedon Jul 22, 2026
In response to @joshtriplett's feedback:
I explicitly avoided
Default/newbecause the user expectation may be for the default constructor to use the "default platform conventions," whatever those are. This is the behavior of thedirectoriescrate, for example. Naming the portable constructoremptyIMHO 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.
They are in the impl, I just elided that here for brevity, the same way I made up the
trait Trait for Selfsummary 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.In rust-lang/rust#158936, I now have:
Suggestions on what examples to add to cache/config/data are very welcome.
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" isstate_dir. Thedirectoriescrate here is conservative and only setsstate_diron 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.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$PATHenvironment variable, at an appropriate place." As such it maybe should be onHomeDirs? But also there's no discovery specified other than "use this path." Plus$XDG_BIN_HOMEis 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 personally lean towards directly using
std::env::split_pathsinstead 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 environmentPATHvariable3.It also means we don't have to expose some additional
std::os::unix::fs::XdgPaths(and potentiallystd::os::unix::fs::JoinXdgPathsError) if we just use thestd::envway of manipulatingPATH-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::envversion that isn't captured in a simple "this could iterate&Pathinstead ofPathBuf" note.std::env::join_pathsusesAsRef<OsStr>; I chose to match that. Without a good reason to divergejoin(like exists with ownership forsplit), it seems better to match that signature.It might make sense to only have that version and have the caller do
env::join_pathsif they don't have the joined string already.Footnotes
The only real reason I can see for wanting
config_dirto beLocalAppDatais 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_HOMEor$XDG_STATE_HOMEinstead of only altering how the Windows install does things. ↩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. ↩
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. ↩CAD97 commentedon Jul 22, 2026
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) oralloc(they don't require the presence of an operating system)2 remaining items
joshtriplett commentedon Jul 27, 2026
Responding here, and I'll post a second comment summarizing for libs-api.
That makes sense; I agree. Let's keep
emptyandtakethen.👍
Does this mean that
directoriesis just incorrect to map them to the same place?👍 for leaving it out for now.
I would add something like "or synchronized between systems" to the end of the first sentence here, but otherwise LGTM.
I do agree that we should have
state_diron 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.Valid; since it's in the base directory spec, it should be in
HomeDirs.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
HomeDirsExtsince 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 do agree that it's more code. To be clear, though, I was imagining adding a portable
split_paths_refand 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.
I think that's a really good point. We're not adding any value by having a function that just calls
join_pathsfor the user. Having just the versions that accept anOsStringseems fine. (And that makes the argument type issue moot.)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.
Yeah, the specification confirms that:
split_paths_refcould still provide the bulk of the implementation here, and then the rest is afilter.joshtriplett commentedon Jul 27, 2026
I think the only thing that needs libs-api review here is agreeing that we shouldn't have
Defaultafter all.joshtriplett commentedon Jul 28, 2026
We talked about this in today's libs-api meeting.
We agreed that we shouldn't have
Default, and should keep theemptymethod. Thanks for the feedback on that one!We also agreed that we don't need the
takemethod. We thought it'd be sufficient that people can callmem::replace, passingempty.With that changed, ACP accepted. Thank you! Looking forward to having this in std.
Please close this once you've opened a tracking issue.
CAD97 commentedon Aug 31, 2026
Tracking issues created:
std::fs::HomeDirs(feature(fs_home_dirs)) rust#162082std::fs::MediaDirs(feature(fs_media_dirs)) rust#162083I split the home and media dirs tracking since while they share a lot, they differ significantly in the "should std carry these" discussion.
Rollup merge of rust-lang#158936 - CAD97:dirs, r=aapoalas
Auto merge of #158936 - CAD97:dirs, r=aapoalas
Rollup merge of rust-lang#158936 - CAD97:dirs, r=aapoalas
Rollup merge of rust-lang#158936 - CAD97:dirs, r=aapoalas
Rollup merge of rust-lang#158936 - CAD97:dirs, r=aapoalas
Rollup merge of rust-lang#158936 - CAD97:dirs, r=aapoalas
Unrolled build for #158936 in rollup 163522