143143//!
144144//! # Under the hood
145145//! Data is written to flat files under the active user's home directory in a location specific to
146- //! the operating system.
147- //!
148- //! * Mac OS X: `~/Library/Application Support`
149- //! * Other Unix/Linux: `$XDG_CONFIG_HOME`, defaulting to `~/.config` if not set
150- //! * Windows: `%APPDATA%`, defaulting to `<std::env::home_dir()>\AppData\Roaming` if not set
146+ //! the operating system. This location is decided by the `app_dirs` crate with the data type
147+ //! `UserConfig`. Within the data directory, the files are stored in a folder hierarchy that maps
148+ //! to a sanitized version of the preferences key passed to `save(..)`.
151149//!
152150//! The data is stored in JSON format. This has several advantages:
153151//!
166164
167165#![ warn( missing_docs) ]
168166
167+ extern crate app_dirs;
169168extern crate rustc_serialize;
170169
171- #[ cfg( windows) ]
172- extern crate winapi;
173- #[ cfg( windows) ]
174- extern crate shell32;
175- #[ cfg( windows) ]
176- extern crate ole32;
177-
170+ use app_dirs:: { AppDataType , get_app_data_root} ;
178171use rustc_serialize:: { Encodable , Decodable } ;
179172use rustc_serialize:: json:: { self , EncoderError , DecoderError } ;
180173use std:: collections:: HashMap ;
181- use std:: env;
182174use std:: fs:: { File , create_dir_all} ;
183175use std:: io:: { ErrorKind , Read , Write } ;
184- use std:: path:: { Path , PathBuf } ;
176+ use std:: path:: PathBuf ;
185177use std:: string:: FromUtf8Error ;
186178
187179type IoError = std:: io:: Error ;
188180
189- #[ cfg( target_os="macos" ) ]
190- fn get_prefs_base_path ( ) -> Option < PathBuf > {
191- env:: home_dir ( ) . map ( |mut dir| {
192- dir. push ( "Library/Application Support" ) ;
193- dir
194- } )
195- }
196-
197- #[ cfg( all( unix, not( target_os="macos" ) ) ) ]
198- fn get_prefs_base_path ( ) -> Option < PathBuf > {
199- match env:: var ( "XDG_CONFIG_HOME" ) {
200- Ok ( path_str) => Some ( path_str. into ( ) ) ,
201- Err ( ..) => {
202- env:: home_dir ( ) . map ( |mut dir| {
203- dir. push ( ".config" ) ;
204- dir
205- } )
206- }
207- }
208- }
209-
210- #[ cfg( windows) ]
211- mod windows {
212- use std:: slice;
213- use std:: ptr;
214- use std:: ffi:: OsString ;
215- use std:: os:: windows:: ffi:: OsStringExt ;
216-
217- use winapi;
218- use shell32:: SHGetKnownFolderPath ;
219- use ole32;
220-
221- // This value is not currently exported by any of the winapi crates, but
222- // its exact value is specified in the MSDN documentation.
223- // https://msdn.microsoft.com/en-us/library/dd378457.aspx#FOLDERID_RoamingAppData
224- #[ allow( non_upper_case_globals) ]
225- static FOLDERID_RoamingAppData : winapi:: GUID = winapi:: GUID {
226- Data1 : 0x3EB685DB ,
227- Data2 : 0x65F9 ,
228- Data3 : 0x4CF6 ,
229- Data4 : [ 0xA0 , 0x3A , 0xE3 , 0xEF , 0x65 , 0x72 , 0x9F , 0x3D ] ,
230- } ;
231-
232- // Retrieves the OsString for AppData using the proper Win32
233- // function without relying on environment variables
234- pub fn get_appdata ( ) -> Result < OsString , ( ) > {
235- unsafe {
236- // A Wide c-style string pointer which will be filled by
237- // SHGetKnownFolderPath. We are responsible for freeing
238- // this value if the call succeeds
239- let mut raw_path: winapi:: PWSTR = ptr:: null_mut ( ) ;
240-
241- // Get RoamingAppData's path
242- let result = SHGetKnownFolderPath ( & FOLDERID_RoamingAppData ,
243- 0 , // No extra flags are neccesary
244- ptr:: null_mut ( ) , // user context, null = current user
245- & mut raw_path) ;
246-
247- // SHGetKnownFolderPath returns an HRESULT, which represents
248- // failure states by being negative. This should not fail, but
249- // we should be prepared should it fail some day.
250- if result < 0 {
251- return Err ( ( ) ) ;
252- }
253-
254- // Since SHGetKnownFolderPath succeeded, we must ensure that we
255- // free the memory even if allocating an OsString fails later on.
256- // To do this, we will use a nested struct with a Drop implementation
257- let _cleanup = {
258- struct FreeStr ( winapi:: PWSTR ) ;
259- impl Drop for FreeStr {
260- fn drop ( & mut self ) {
261- unsafe { ole32:: CoTaskMemFree ( self . 0 as * mut _ ) } ;
262- }
263- }
264- FreeStr ( raw_path)
265- } ;
266-
267- // libstd does not contain a wide-char strlen as far as I know,
268- // so we'll have to make do calculating it ourselves.
269- let mut strlen = 0 ;
270- for i in 0 .. {
271- if * raw_path. offset ( i) == 0 {
272- // isize -> usize is always safe here because we know
273- // that an isize can hold the positive length, as each
274- // char is 2 bytes long, and so could only be half of
275- // the memory space even theoretically.
276- strlen = i as usize ;
277- break ;
278- }
279- }
280-
281- // Now that we know the length of the string, we can
282- // convert it to a &[u16]
283- let wpath = slice:: from_raw_parts ( raw_path, strlen) ;
284- // Window's OsStringExt has the function from_wide for
285- // converting a &[u16] into an OsString.
286- let path = OsStringExt :: from_wide ( wpath) ;
287-
288- // raw_path will be automatically freed by _cleanup, regardless of
289- // whether any of the previous functions panic.
290-
291- Ok ( path)
292- }
293- }
294- }
295-
296- #[ cfg( windows) ]
297- fn get_prefs_base_path ( ) -> Option < PathBuf > {
298- match windows:: get_appdata ( ) {
299- Ok ( path_str) => Some ( path_str. into ( ) ) ,
300- Err ( ..) => {
301- env:: home_dir ( ) . map ( |mut dir| {
302- dir. push ( "AppData" ) ;
303- dir. push ( "Roaming" ) ;
304- dir
305- } )
306- }
307- }
308- }
309-
310181/// Generic key-value store for user data.
311182///
312183/// This is actually a wrapper type around [`std::collections::HashMap<String, T>`][hashmap-api]
@@ -324,7 +195,7 @@ pub type PreferencesMap<T = String> = HashMap<String, T>;
324195/// Error type representing the errors that can occur when saving or loading user data.
325196#[ derive( Debug ) ]
326197pub enum PreferencesError {
327- /// An error occurred during JSON ( serialization.
198+ /// An error occurred during JSON serialization.
328199 Serialize ( EncoderError ) ,
329200 /// An error occurred during JSON deserialization.
330201 Deserialize ( DecoderError ) ,
@@ -365,30 +236,33 @@ impl From<std::io::Error> for PreferencesError {
365236/// `Decodable` (from `rustc-serialize`). However, you are encouraged to use the provided type,
366237/// [`PreferencesMap`](type.PreferencesMap.html).
367238///
368- /// The `path ` parameter of `save(..)` and `load(..)` should be a valid, relative file path . It is
239+ /// The `key ` parameter of `save(..)` and `load(..)` should be an application-unique string . It is
369240/// *highly* recommended that you use the format
370241/// `[company or author]/[application name]/[data description]`. For example, a game might use
371- /// the following paths for player options and save data, respectively:
242+ /// the following keys for player options and save data, respectively:
372243///
373244/// * `fun-games-inc/awesome-game-2/options`
374245/// * `fun-games-inc/awesome-game-2/saves`
246+ ///
247+ /// Under the hood, the key string is sanitized and converted into a directory hierarchy.
248+ /// Following the suggested key format and sticking to alphanumeric characters and hypens will
249+ /// make the user preferences easier to find in case they need to be manually edited or backed up.
375250pub trait Preferences {
376251 /// Saves the current state of this object. Implementation is platform-dependent, but the data
377252 /// will be local to the active user. For more details, see
378253 /// [the module documentation](index.html).
379254 ///
380255 /// # Failures
381- /// If a serialization or file I/O error occurs (e.g. permission denied), or if the provided
382- /// `path` argument is invalid.
383- fn save < S > ( & self , path : S ) -> Result < ( ) , PreferencesError > where S : AsRef < str > ;
384- /// Loads this object's state from previously saved user data with the same `path`. This is
256+ /// If a serialization or file I/O error (e.g. permission denied) occurs.
257+ fn save < S > ( & self , key : S ) -> Result < ( ) , PreferencesError > where S : AsRef < str > ;
258+ /// Loads this object's state from previously saved user data with the same `key`. This is
385259 /// an instance method which completely overwrites the object's state with the serialized
386260 /// data. Thus, it is recommended that you call this method immediately after instantiating
387261 /// the preferences object.
388262 ///
389263 /// # Failures
390- /// If a deserialization or file I/O error occurs (e.g. permission denied), if the provided
391- /// `path` argument is invalid, or if no user data exists at that `path`.
264+ /// If a deserialization or file I/O error (e.g. permission denied) occurs, or if no user data
265+ /// exists at that `path`.
392266 fn load < S > ( & mut self , path : S ) -> Result < ( ) , PreferencesError > where S : AsRef < str > ;
393267 /// Same as `save`, but writes the serialized preferences to an arbitrary writer.
394268 fn save_to < W > ( & self , writer : & mut W ) -> Result < ( ) , PreferencesError > where W : Write ;
@@ -402,15 +276,17 @@ impl<T> Preferences for T
402276 fn save < S > ( & self , path : S ) -> Result < ( ) , PreferencesError >
403277 where S : AsRef < str >
404278 {
405- let path = try!( path_buf_from_name ( path. as_ref ( ) ) ) ;
279+ let mut path = try!( path_buf_from_key ( path. as_ref ( ) ) ) ;
280+ path. set_extension ( "json" ) ;
406281 path. parent ( ) . map ( create_dir_all) ;
407282 let mut file = try!( File :: create ( path) ) ;
408283 self . save_to ( & mut file)
409284 }
410285 fn load < S > ( & mut self , path : S ) -> Result < ( ) , PreferencesError >
411286 where S : AsRef < str >
412287 {
413- let path = try!( path_buf_from_name ( path. as_ref ( ) ) ) ;
288+ let mut path = try!( path_buf_from_key ( path. as_ref ( ) ) ) ;
289+ path. set_extension ( "json" ) ;
414290 let mut file = try!( File :: open ( path) ) ;
415291 self . load_from ( & mut file)
416292 }
@@ -437,36 +313,47 @@ impl<T> Preferences for T
437313/// Get full path to the base directory for preferences.
438314///
439315/// This makes no guarantees that the specified directory path actually *exists* (though you can
440- /// easily use `std::fs::create_dir_all(..)`). Returns `None` if the directory cannot be found
316+ /// easily use `std::fs::create_dir_all(..)`). Returns `None` if the directory cannot be determined
441317/// or is not available on the current platform.
442318pub fn prefs_base_dir ( ) -> Option < PathBuf > {
443- get_prefs_base_path ( )
319+ get_app_data_root ( AppDataType :: UserConfig ) . ok ( )
444320}
445321
446- fn path_buf_from_name ( name : & str ) -> Result < PathBuf , IoError > {
447-
448- let msg_not_found = "Could not find home directory for user data storage" ;
449- let err_not_found = IoError :: new ( ErrorKind :: NotFound , msg_not_found) ;
450-
451- let msg_bad_name = "Invalid preferences name: " . to_owned ( ) + name;
452- let err_bad_name = Result :: Err ( IoError :: new ( ErrorKind :: Other , msg_bad_name) ) ;
453-
454- if name. starts_with ( "../" ) || name. ends_with ( "/.." ) || name. contains ( "/../" ) {
455- return err_bad_name;
456- }
457- let mut base_path = try!( get_prefs_base_path ( ) . ok_or ( err_not_found) ) ;
458- let name_path = Path :: new ( name) ;
459- if !name_path. is_relative ( ) {
460- return err_bad_name;
322+ fn path_buf_from_key ( name : & str ) -> Result < PathBuf , IoError > {
323+ match prefs_base_dir ( ) {
324+ Some ( mut buf) => {
325+ let keys: Vec < String > = name. split ( "/" ) . map ( |s| s. into ( ) ) . collect ( ) ;
326+ for key in keys. iter ( ) {
327+ let mut safe_key = String :: new ( ) ;
328+ if key == "" {
329+ safe_key. push ( '_' ) ;
330+ } else {
331+ for c in key. chars ( ) {
332+ let n = c as u32 ;
333+ let is_lower = 'a' as u32 <= n && n <= 'z' as u32 ;
334+ let is_upper = 'A' as u32 <= n && n <= 'Z' as u32 ;
335+ let is_number = '0' as u32 <= n && n <= '9' as u32 ;
336+ let is_space = c == ' ' ;
337+ let is_hyphen = c == '-' ;
338+ if is_upper || is_lower || is_number || is_space || is_hyphen {
339+ safe_key. push ( c) ;
340+ } else {
341+ safe_key. push_str ( & format ! ( "_{}_" , n) ) ;
342+ }
343+ }
344+ }
345+ buf. push ( safe_key) ;
346+ }
347+ Ok ( buf)
348+ }
349+ None => Err ( IoError :: new ( ErrorKind :: NotFound , "Preferences directory unavailable" ) ) ,
461350 }
462- base_path. push ( name_path) ;
463- Result :: Ok ( base_path)
464351}
465352
466353#[ cfg( test) ]
467354mod tests {
468355 use { Preferences , PreferencesMap } ;
469- static TEST_PREFIX : & ' static str = "rust_user_prefs_test " ;
356+ static TEST_PREFIX : & ' static str = "preferences-rs/tests " ;
470357 fn gen_test_name ( name : & str ) -> String {
471358 TEST_PREFIX . to_owned ( ) + "/" + name
472359 }
@@ -480,7 +367,7 @@ mod tests {
480367 }
481368 #[ test]
482369 fn test_save_load ( ) {
483- let name = gen_test_name ( "/save_load " ) ;
370+ let name = gen_test_name ( "save-load " ) ;
484371 let sample = gen_sample_prefs ( ) ;
485372 let save_result = sample. save ( & name) ;
486373 println ! ( "Save result: {:?}" , save_result) ;
0 commit comments