143
143
//!
144
144
//! # Under the hood
145
145
//! 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(..)`.
151
149
//!
152
150
//! The data is stored in JSON format. This has several advantages:
153
151
//!
166
164
167
165
#![ warn( missing_docs) ]
168
166
167
+ extern crate app_dirs;
169
168
extern crate rustc_serialize;
170
169
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} ;
178
171
use rustc_serialize:: { Encodable , Decodable } ;
179
172
use rustc_serialize:: json:: { self , EncoderError , DecoderError } ;
180
173
use std:: collections:: HashMap ;
181
- use std:: env;
182
174
use std:: fs:: { File , create_dir_all} ;
183
175
use std:: io:: { ErrorKind , Read , Write } ;
184
- use std:: path:: { Path , PathBuf } ;
176
+ use std:: path:: PathBuf ;
185
177
use std:: string:: FromUtf8Error ;
186
178
187
179
type IoError = std:: io:: Error ;
188
180
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
-
310
181
/// Generic key-value store for user data.
311
182
///
312
183
/// 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>;
324
195
/// Error type representing the errors that can occur when saving or loading user data.
325
196
#[ derive( Debug ) ]
326
197
pub enum PreferencesError {
327
- /// An error occurred during JSON ( serialization.
198
+ /// An error occurred during JSON serialization.
328
199
Serialize ( EncoderError ) ,
329
200
/// An error occurred during JSON deserialization.
330
201
Deserialize ( DecoderError ) ,
@@ -365,30 +236,33 @@ impl From<std::io::Error> for PreferencesError {
365
236
/// `Decodable` (from `rustc-serialize`). However, you are encouraged to use the provided type,
366
237
/// [`PreferencesMap`](type.PreferencesMap.html).
367
238
///
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
369
240
/// *highly* recommended that you use the format
370
241
/// `[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:
372
243
///
373
244
/// * `fun-games-inc/awesome-game-2/options`
374
245
/// * `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.
375
250
pub trait Preferences {
376
251
/// Saves the current state of this object. Implementation is platform-dependent, but the data
377
252
/// will be local to the active user. For more details, see
378
253
/// [the module documentation](index.html).
379
254
///
380
255
/// # 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
385
259
/// an instance method which completely overwrites the object's state with the serialized
386
260
/// data. Thus, it is recommended that you call this method immediately after instantiating
387
261
/// the preferences object.
388
262
///
389
263
/// # 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`.
392
266
fn load < S > ( & mut self , path : S ) -> Result < ( ) , PreferencesError > where S : AsRef < str > ;
393
267
/// Same as `save`, but writes the serialized preferences to an arbitrary writer.
394
268
fn save_to < W > ( & self , writer : & mut W ) -> Result < ( ) , PreferencesError > where W : Write ;
@@ -402,15 +276,17 @@ impl<T> Preferences for T
402
276
fn save < S > ( & self , path : S ) -> Result < ( ) , PreferencesError >
403
277
where S : AsRef < str >
404
278
{
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" ) ;
406
281
path. parent ( ) . map ( create_dir_all) ;
407
282
let mut file = try!( File :: create ( path) ) ;
408
283
self . save_to ( & mut file)
409
284
}
410
285
fn load < S > ( & mut self , path : S ) -> Result < ( ) , PreferencesError >
411
286
where S : AsRef < str >
412
287
{
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" ) ;
414
290
let mut file = try!( File :: open ( path) ) ;
415
291
self . load_from ( & mut file)
416
292
}
@@ -437,36 +313,47 @@ impl<T> Preferences for T
437
313
/// Get full path to the base directory for preferences.
438
314
///
439
315
/// 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
441
317
/// or is not available on the current platform.
442
318
pub fn prefs_base_dir ( ) -> Option < PathBuf > {
443
- get_prefs_base_path ( )
319
+ get_app_data_root ( AppDataType :: UserConfig ) . ok ( )
444
320
}
445
321
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" ) ) ,
461
350
}
462
- base_path. push ( name_path) ;
463
- Result :: Ok ( base_path)
464
351
}
465
352
466
353
#[ cfg( test) ]
467
354
mod tests {
468
355
use { Preferences , PreferencesMap } ;
469
- static TEST_PREFIX : & ' static str = "rust_user_prefs_test " ;
356
+ static TEST_PREFIX : & ' static str = "preferences-rs/tests " ;
470
357
fn gen_test_name ( name : & str ) -> String {
471
358
TEST_PREFIX . to_owned ( ) + "/" + name
472
359
}
@@ -480,7 +367,7 @@ mod tests {
480
367
}
481
368
#[ test]
482
369
fn test_save_load ( ) {
483
- let name = gen_test_name ( "/save_load " ) ;
370
+ let name = gen_test_name ( "save-load " ) ;
484
371
let sample = gen_sample_prefs ( ) ;
485
372
let save_result = sample. save ( & name) ;
486
373
println ! ( "Save result: {:?}" , save_result) ;
0 commit comments