Skip to content

Commit 43c1f8b

Browse files
committed
Use app_dirs to determine prefs location, harden resolution of preferences keys to file paths
1 parent 1251b99 commit 43c1f8b

File tree

3 files changed

+58
-174
lines changed

3 files changed

+58
-174
lines changed

Cargo.toml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "preferences"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
authors = ["Andy Barron <[email protected]>"]
55

66
description = "Read and write user-specific application data (in stable Rust)"
@@ -11,8 +11,5 @@ keywords = ["preferences", "user", "data", "persistent", "storage"]
1111
license = "MIT"
1212

1313
[dependencies]
14-
# TODO conditional dependencies once Rust 1.8.0 hits stable
15-
rustc-serialize = "^0.3.18"
16-
winapi = "^0.2.5"
17-
ole32-sys = "^0.2.0"
18-
shell32-sys = "^0.1.1"
14+
app_dirs = "^0.1.0"
15+
rustc-serialize = "^0.3.19"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ _Read and write user-specific application data in Rust_
1111
## Installation
1212
Add the following to your `Cargo.toml`:
1313

14-
`preferences = "^0.5.0"`
14+
`preferences = "^0.6.0"`

src/lib.rs

Lines changed: 54 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,9 @@
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
//!
@@ -166,147 +164,20 @@
166164
167165
#![warn(missing_docs)]
168166

167+
extern crate app_dirs;
169168
extern 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};
178171
use rustc_serialize::{Encodable, Decodable};
179172
use rustc_serialize::json::{self, EncoderError, DecoderError};
180173
use std::collections::HashMap;
181-
use std::env;
182174
use std::fs::{File, create_dir_all};
183175
use std::io::{ErrorKind, Read, Write};
184-
use std::path::{Path, PathBuf};
176+
use std::path::PathBuf;
185177
use std::string::FromUtf8Error;
186178

187179
type 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)]
326197
pub 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.
375250
pub 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.
442318
pub 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)]
467354
mod 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

Comments
 (0)