first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
@@ -1,14 +1,17 @@
//! Implementation of the [`SecureStorage`] service for the Linux platform.
use std::{cell::OnceCell, collections::HashMap, path::PathBuf};
use std::cell::OnceCell;
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::io::Write as _;
use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _};
use std::path::PathBuf;
use anyhow::{anyhow, Context};
use rand::RngCore;
use ring::aead;
use secret_service::{
blocking::{Item, SecretService},
EncryptionType,
};
use secret_service::blocking::{Item, SecretService};
use secret_service::EncryptionType;
use super::Error;
@@ -227,10 +230,43 @@ impl SecureStorage {
let fallback_file = self.fallback_file(key)?;
let encrypted = self.fallback_encrypt(value)?;
std::fs::write(fallback_file, encrypted).map_err(|err| Error::Unknown(err.into()))
}
fn write_owner_only_fallback_value(&self, key: &str, value: &str) -> Result<(), Error> {
let fallback_file = self.fallback_file(key)?;
let encrypted = self.fallback_encrypt(value)?;
let Some(fallback_dir) = fallback_file.parent() else {
return Err(Error::Unknown(anyhow!(
"Invalid fallback secure-storage directory"
)));
};
std::fs::create_dir_all(fallback_dir).map_err(|err| Error::Unknown(err.into()))?;
let mut dir_permissions = std::fs::metadata(fallback_dir)
.map_err(|err| Error::Unknown(err.into()))?
.permissions();
dir_permissions.set_mode(0o700);
std::fs::set_permissions(fallback_dir, dir_permissions)
.map_err(|err| Error::Unknown(err.into()))?;
let mut file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.mode(0o600)
.open(&fallback_file)
.map_err(|err| Error::Unknown(err.into()))?;
file.write_all(&encrypted)
.map_err(|err| Error::Unknown(err.into()))?;
let mut file_permissions = file
.metadata()
.map_err(|err| Error::Unknown(err.into()))?
.permissions();
file_permissions.set_mode(0o600);
file.set_permissions(file_permissions)
.map_err(|err| Error::Unknown(err.into()))
}
fn read_fallback_value(&self, key: &str) -> Result<String, Error> {
let fallback_file = self.fallback_file(key)?;
@@ -260,6 +296,17 @@ impl super::SecureStorage for SecureStorage {
Err(_) => self.write_fallback_value(key, value),
}
}
fn write_value_with_owner_only_fallback(&self, key: &str, value: &str) -> Result<(), Error> {
let secret_result = self.write_secret_value(key, value);
match secret_result {
Ok(_) => {
let _ = self.delete_fallback_value(key);
Ok(())
}
Err(_) => self.write_owner_only_fallback_value(key, value),
}
}
fn read_value(&self, key: &str) -> Result<String, Error> {
let secret_result = self.with_item(key, |item| {
@@ -341,5 +388,5 @@ impl Collection {
}
#[cfg(test)]
#[path = "linux_test.rs"]
#[path = "linux_tests.rs"]
mod tests;
@@ -1,5 +1,4 @@
use super::Error;
use super::SecureStorage;
use super::{Error, SecureStorage};
#[test]
fn test_encrypt_decrypt_returns_same_value() {
@@ -44,3 +43,37 @@ fn test_decrypt_fails_on_malformed_data() {
);
}
}
#[test]
fn fallback_value_is_owner_only() {
use std::os::unix::fs::PermissionsExt as _;
let temp_dir = tempfile::tempdir().expect("temp dir");
let fallback_dir = temp_dir.path().join("secure-storage");
let storage = SecureStorage::new_with_fallback("darmok", fallback_dir.clone());
storage
.write_owner_only_fallback_value("key", "value")
.expect("fallback write");
let dir_mode = std::fs::metadata(&fallback_dir)
.expect("directory metadata")
.permissions()
.mode()
& 0o777;
let file_mode = std::fs::metadata(storage.fallback_file("key").expect("fallback file"))
.expect("file metadata")
.permissions()
.mode()
& 0o777;
assert_eq!(dir_mode, 0o700);
assert_eq!(file_mode, 0o600);
}
#[test]
fn default_fallback_does_not_create_missing_directory() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let fallback_dir = temp_dir.path().join("secure-storage");
let storage = SecureStorage::new_with_fallback("darmok", fallback_dir.clone());
assert!(storage.write_fallback_value("key", "value").is_err());
assert!(!fallback_dir.exists());
}
@@ -0,0 +1,79 @@
use super::{Error, SecureStorage};
#[test]
fn test_encrypt_decrypt_returns_same_value() {
let storage = SecureStorage::new("darmok");
let input = String::from("darmok and jalad at tanagra");
let encrypted = storage.fallback_encrypt(&input).unwrap();
let output = storage.fallback_decrypt(&encrypted).unwrap();
assert_eq!(input, output)
}
#[test]
fn test_encrypt_decrypt_works_across_storage_instances() {
let storage_1 = SecureStorage::new("darmok");
let storage_2 = SecureStorage::new("jalad");
let input = String::from("shaka when the walls fell");
let encrypted = storage_1.fallback_encrypt(&input).unwrap();
let output = storage_2.fallback_decrypt(&encrypted).unwrap();
assert_eq!(input, output)
}
#[test]
fn test_decrypt_fails_on_malformed_data() {
let storage = SecureStorage::new("darmok");
let bad_datas: [&[u8]; 4] = [&[], &[0; 1], &[0; 11], &[0; 12]];
for bad_data in bad_datas {
let result = storage.fallback_decrypt(bad_data);
assert!(result.is_err());
let error = result.unwrap_err();
let Error::Unknown(err) = error else {
panic!("Expected error variant to be Error::Unknown, but found {error:?}")
};
assert_eq!(
format!("{err}"),
"Attempting to decrypt too small value for fallback decryption"
);
}
}
#[test]
fn fallback_value_is_owner_only() {
use std::os::unix::fs::PermissionsExt as _;
let temp_dir = tempfile::tempdir().expect("temp dir");
let fallback_dir = temp_dir.path().join("secure-storage");
let storage = SecureStorage::new_with_fallback("darmok", fallback_dir.clone());
storage
.write_owner_only_fallback_value("key", "value")
.expect("fallback write");
let dir_mode = std::fs::metadata(&fallback_dir)
.expect("directory metadata")
.permissions()
.mode()
& 0o777;
let file_mode = std::fs::metadata(storage.fallback_file("key").expect("fallback file"))
.expect("file metadata")
.permissions()
.mode()
& 0o777;
assert_eq!(dir_mode, 0o700);
assert_eq!(file_mode, 0o600);
}
#[test]
fn default_fallback_does_not_create_missing_directory() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let fallback_dir = temp_dir.path().join("secure-storage");
let storage = SecureStorage::new_with_fallback("darmok", fallback_dir.clone());
assert!(storage.write_fallback_value("key", "value").is_err());
assert!(!fallback_dir.exists());
}
@@ -1,9 +1,9 @@
//! Implementations of the [`SecureStorage`] service for the macOS platform.
use anyhow::anyhow;
use security_framework::os::macos::{
keychain::SecKeychain, keychain_item::SecKeychainItem, passwords::SecKeychainItemPassword,
};
use security_framework::os::macos::keychain::SecKeychain;
use security_framework::os::macos::keychain_item::SecKeychainItem;
use security_framework::os::macos::passwords::SecKeychainItemPassword;
use super::Error;
@@ -6,10 +6,11 @@
#[cfg(not(target_family = "wasm"))]
#[cfg_attr(target_os = "macos", path = "mac.rs")]
#[cfg_attr(target_os = "linux", path = "linux.rs")]
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), path = "linux.rs")]
#[cfg_attr(target_os = "windows", path = "windows.rs")]
mod imp;
mod noop;
mod unavailable;
// Treat this as a noop on web, as there is no backing storage which is "secure".
#[cfg(target_family = "wasm")]
@@ -27,7 +28,7 @@ use windows_only::*;
/// app context, enabling usage such as:
///
/// ```
/// use galaxyui::{App, SingletonEntity};
/// use galaxyui_core::{App, SingletonEntity};
/// use galaxyui_extras::secure_storage;
///
/// App::test((), |mut app| async move {
@@ -51,20 +52,27 @@ pub type Model = Box<dyn SecureStorage>;
/// is recommended that this be a unique identifier for the application; one
/// common scheme is reverse-DNS notation (e.g.: "dev.warp.Warp").
#[cfg(not(target_os = "windows"))]
pub fn register(service_name: &str, ctx: &mut galaxyui::AppContext) {
pub fn register(service_name: &str, ctx: &mut galaxyui_core::AppContext) {
ctx.add_singleton_model(|_| -> Model { Box::new(imp::SecureStorage::new(service_name)) });
}
/// Registers a no-op Secure Storage provider with the application.
pub fn register_noop(service_name: &str, ctx: &mut galaxyui::AppContext) {
pub fn register_noop(service_name: &str, ctx: &mut galaxyui_core::AppContext) {
ctx.add_singleton_model(|_| -> Model { Box::new(noop::SecureStorage::new(service_name)) });
}
#[cfg(target_os = "linux")]
/// Registers an unavailable Secure Storage provider that deliberately does not persist values.
///
/// Reads report missing values, while writes and removals succeed without accessing storage.
pub fn register_unavailable(ctx: &mut galaxyui_core::AppContext) {
ctx.add_singleton_model(|_| -> Model { Box::new(unavailable::SecureStorage) });
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
pub fn register_with_fallback(
service_name: &str,
fallback_dir: std::path::PathBuf,
ctx: &mut galaxyui::AppContext,
ctx: &mut galaxyui_core::AppContext,
) {
ctx.add_singleton_model(|_| -> Model {
Box::new(imp::SecureStorage::new_with_fallback(
@@ -80,7 +88,7 @@ pub fn register_with_fallback(
pub fn register_with_dir(
service_name: &str,
storage_dir: std::path::PathBuf,
ctx: &mut galaxyui::AppContext,
ctx: &mut galaxyui_core::AppContext,
) {
ctx.add_singleton_model(|_| -> Model {
Box::new(imp::SecureStorage::new_with_path(service_name, storage_dir))
@@ -93,6 +101,14 @@ pub fn register_with_dir(
pub trait SecureStorage {
/// Writes a value at the given key.
fn write_value(&self, key: &str, value: &str) -> Result<(), Error>;
/// Writes a value while requiring any file fallback to be owner-only.
///
/// Platforms without a file fallback use their normal secure-storage write
/// path. Callers should opt into this only when they require the stronger
/// fallback behavior because it may create or change fallback permissions.
fn write_value_with_owner_only_fallback(&self, key: &str, value: &str) -> Result<(), Error> {
self.write_value(key, value)
}
/// Reads the value stored at the given key.
fn read_value(&self, key: &str) -> Result<String, Error>;
@@ -101,11 +117,11 @@ pub trait SecureStorage {
fn remove_value(&self, key: &str) -> Result<(), Error>;
}
impl galaxyui::Entity for Model {
impl galaxyui_core::Entity for Model {
type Event = ();
}
impl galaxyui::SingletonEntity for Model {}
impl galaxyui_core::SingletonEntity for Model {}
/// Enumerates the various errors that can occur when interacting with secure
/// storage.
@@ -154,7 +170,7 @@ impl From<FromUtf8Error> for Error {
/// An extension trait to make secure storage easier to use.
///
/// ```
/// use galaxyui::{App, SingletonEntity};
/// use galaxyui_core::{App, SingletonEntity};
/// use galaxyui_extras::secure_storage;
///
/// App::test((), |mut app| async move {
@@ -175,9 +191,9 @@ pub trait AppContextExt {
fn secure_storage(&self) -> &dyn SecureStorage;
}
impl AppContextExt for galaxyui::AppContext {
impl AppContextExt for galaxyui_core::AppContext {
fn secure_storage(&self) -> &dyn SecureStorage {
use galaxyui::SingletonEntity;
use galaxyui_core::SingletonEntity;
<Model as SingletonEntity>::as_ref(self).as_ref()
}
@@ -0,0 +1,23 @@
//! [`SecureStorage`] provider for processes that must not access persistent secrets.
use super::Error;
pub struct SecureStorage;
impl super::SecureStorage for SecureStorage {
fn write_value(&self, _key: &str, _value: &str) -> Result<(), Error> {
Ok(())
}
fn read_value(&self, _key: &str) -> Result<String, Error> {
Err(Error::NotFound)
}
fn remove_value(&self, _key: &str) -> Result<(), Error> {
Ok(())
}
}
#[cfg(test)]
#[path = "unavailable_tests.rs"]
mod tests;
@@ -0,0 +1,25 @@
use super::SecureStorage;
use crate::secure_storage::{Error, SecureStorage as _};
#[test]
fn read_value_returns_not_found() {
let storage = SecureStorage;
assert!(matches!(storage.read_value("key"), Err(Error::NotFound)));
}
#[test]
fn write_value_is_discarded() {
let storage = SecureStorage;
storage.write_value("key", "value").expect("write succeeds");
assert!(matches!(storage.read_value("key"), Err(Error::NotFound)));
}
#[test]
fn remove_value_succeeds() {
let storage = SecureStorage;
storage.remove_value("key").expect("remove succeeds");
}
@@ -1,10 +1,9 @@
use std::path::PathBuf;
use windows::{
core::BSTR,
Win32::{
Foundation::{LocalFree, HLOCAL},
Security::Cryptography::{CryptProtectData, CryptUnprotectData, CRYPT_INTEGER_BLOB},
},
use windows::core::BSTR;
use windows::Win32::Foundation::{LocalFree, HLOCAL};
use windows::Win32::Security::Cryptography::{
CryptProtectData, CryptUnprotectData, CRYPT_INTEGER_BLOB,
};
use super::Error;
@@ -105,5 +104,5 @@ impl super::SecureStorage for SecureStorage {
}
#[cfg(test)]
#[path = "windows_test.rs"]
#[path = "windows_tests.rs"]
mod test;
@@ -0,0 +1,30 @@
use super::SecureStorage;
#[test]
fn test_encrypt_decrypt_returns_same_value() {
let key = String::from("key");
let inputs = [
"freckles grain uncaring strict stumbling reappear basil uproar",
"ideology shifting overview cognition uniformed armory mummify editor",
"",
"{",
"\'",
"\"",
"{\"test\"}",
"defender french skating sweat neurotic extruding cadet mute headcount unaligned prognosis heroics geography deafening customer juicy scuttle blissful scrambler spleen embark engine shield banter botanist singing plutonium grafted carton playable approve astonish",
"{\"id_token\":{\"id_token\":\"This is an ID token.\",\"refresh_token\":\"This is a refresh token.\",\"expiration_time\":\"2025-10-22T15:26:51.091844800-04:00\"},\"refresh_token\":\"\",\"local_id\":\"test_user_uid\",\"email\":\"test_user@warp.dev\",\"display_name\":\"abcdef\",\"photo_url\":\"some-photo-url=\",\"is_onboarded\":true,\"needs_sso_link\":false,\"anonymous_user_type\":null,\"expires_at\":null,\"linked_at\":null,\"is_guaranteed_expired\":false,\"is_on_work_domain\":false}",
].map(String::from);
fn encrypt_then_decrypt(key: &str, input: String) -> String {
let encrypted = SecureStorage::encrypt(key, input).unwrap();
SecureStorage::decrypt(encrypted).unwrap()
}
for input in inputs {
assert_eq!(
encrypt_then_decrypt(&key, input.to_owned()),
input,
"Encrypting and decrypting {input:?}"
);
}
}
@@ -3,7 +3,8 @@
use std::path::{Path, PathBuf};
use super::{in_memory::InMemoryPreferences, Error};
use super::in_memory::InMemoryPreferences;
use super::Error;
/// An implementation of the [`UserPreferences`] trait using a file for
/// persistence.
@@ -1,4 +1,5 @@
use std::{cell::RefCell, collections::HashMap};
use std::cell::RefCell;
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
@@ -1,5 +1,7 @@
use gloo_storage::errors::StorageError;
use gloo_storage::{LocalStorage, Storage};
use super::UserPreferences;
use gloo_storage::{errors::StorageError, LocalStorage, Storage};
/// An implementation of the [`UserPreferences`] trait using the local storage
/// property of the Web Storage API for persistence.
@@ -1,10 +1,11 @@
use std::io;
use windows_registry::{Key, CURRENT_USER};
use windows_result::HRESULT;
/// Store user preferences in the Windows Registry.
/// Modeled after https://github.com/neovide/neovide/blob/main/src/windows_utils.rs .
use super::UserPreferences;
use windows_registry::{Key, CURRENT_USER};
use windows_result::HRESULT;
pub struct RegistryBackedPreferences {
app_key_path: String,
@@ -1,7 +1,7 @@
use super::*;
use toml_edit::Item;
use super::*;
#[test]
fn test_json_to_toml_round_trip_bool() {
let item = TomlBackedUserPreferences::json_value_to_toml_item("true", None);
@@ -220,7 +220,6 @@ fn test_write_and_read_with_hierarchy() {
#[test]
fn test_write_and_read_struct_value() {
use super::super::UserPreferences;
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("test_settings.toml");
@@ -257,7 +256,6 @@ fn test_write_and_read_struct_value() {
#[test]
fn test_new_with_invalid_toml_returns_error_and_recovers_on_reload() {
use super::super::UserPreferences;
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("broken_settings.toml");
@@ -298,7 +296,6 @@ fn test_new_with_invalid_toml_returns_error_and_recovers_on_reload() {
#[test]
fn test_writes_inhibited_when_file_initially_broken() {
use super::super::UserPreferences;
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("broken_settings.toml");
@@ -338,7 +335,6 @@ fn test_writes_inhibited_when_file_initially_broken() {
#[test]
fn test_string_value_for_numeric_setting_reads_as_json_string() {
use super::super::UserPreferences;
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("test_settings.toml");
@@ -370,7 +366,6 @@ fn test_string_value_for_numeric_setting_reads_as_json_string() {
#[test]
fn test_remove_with_hierarchy() {
use super::super::UserPreferences;
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("test_settings.toml");
@@ -397,7 +392,6 @@ fn test_remove_with_hierarchy() {
#[test]
fn test_per_key_write_inhibition_preserves_value_in_file() {
use super::super::UserPreferences;
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("settings.toml");
@@ -441,7 +435,6 @@ fn test_per_key_write_inhibition_preserves_value_in_file() {
#[test]
fn test_per_key_write_inhibition_blocks_remove() {
use super::super::UserPreferences;
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("settings.toml");
@@ -464,7 +457,6 @@ fn test_per_key_write_inhibition_blocks_remove() {
#[test]
fn test_reload_clears_per_key_inhibitions() {
use super::super::UserPreferences;
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("settings.toml");
@@ -491,7 +483,6 @@ fn test_reload_clears_per_key_inhibitions() {
#[test]
fn test_clear_all_write_inhibitions() {
use super::super::UserPreferences;
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("settings.toml");
@@ -592,7 +583,6 @@ fn write_and_read_file(
value_json: &str,
max_table_depth: Option<u32>,
) -> String {
use super::super::UserPreferences;
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("settings.toml");
@@ -1,14 +1,14 @@
//! Implementation of the [`UserPreferences`] trait using macOS user defaults.
#![allow(deprecated)]
use cocoa::base::{id, nil};
use objc::{class, msg_send, rc::StrongPtr, sel, sel_impl};
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2::AnyThread;
use objc2_foundation::{NSString, NSUserDefaults};
/// A user preferences store backed by macOS user defaults (`NSUserDefaults`).
pub struct UserDefaultsPreferencesStorage {
/// A strong reference to the `NSUserDefaults` backing store.
user_defaults: StrongPtr,
user_defaults: Retained<NSUserDefaults>,
}
impl UserDefaultsPreferencesStorage {
@@ -28,73 +28,53 @@ impl UserDefaultsPreferencesStorage {
///
/// If [`None`] is provided as the suite name, the standard user defaults
/// will be used (namespaced based on the current application).
fn user_defaults(suite_name: Option<String>) -> StrongPtr {
unsafe {
// Calling `[[NSUserDefaults alloc] initWithSuiteName]`` where the suite name is the
// application's bundle ID (the default `data_domain` if `data_profile` is unset)
// _should_ be equivalent to `[NSUserDefaults standardUserDefaults]`. However, in case
// the two ever deviate, we explicitly use `standardUserDefaults` below. The Apple docs
// also imply that `standardUserDefaults` is cached.
if let Some(suite_name) = &suite_name {
let defaults: id = msg_send![class!(NSUserDefaults), alloc];
let suite_name = util::make_nsstring(suite_name);
fn user_defaults(suite_name: Option<String>) -> Retained<NSUserDefaults> {
// Calling `[[NSUserDefaults alloc] initWithSuiteName]`` where the suite name is the
// application's bundle ID (the default `data_domain` if `data_profile` is unset)
// _should_ be equivalent to `[NSUserDefaults standardUserDefaults]`. However, in case
// the two ever deviate, we explicitly use `standardUserDefaults` below. The Apple docs
// also imply that `standardUserDefaults` is cached.
if let Some(suite_name) = &suite_name {
let suite_name = NSString::from_str(suite_name);
StrongPtr::new(msg_send![defaults, initWithSuiteName: *suite_name])
} else {
StrongPtr::retain(msg_send![class!(NSUserDefaults), standardUserDefaults])
}
// `initWithSuiteName:` only returns nil when the suite name is a reserved domain
// (NSGlobalDomain/NSArgumentDomain/NSRegistrationDomain) or the app's own bundle
// identifier; our `{app_id}-{profile}` suite name is never either of those, so this
// is unreachable in practice.
NSUserDefaults::initWithSuiteName(NSUserDefaults::alloc(), Some(&suite_name)).expect(
"initWithSuiteName: only returns nil for a reserved domain or the app's own bundle id",
)
} else {
NSUserDefaults::standardUserDefaults()
}
}
}
impl super::UserPreferences for UserDefaultsPreferencesStorage {
fn write_value(&self, key: &str, value: String) -> Result<(), super::Error> {
unsafe {
let key = util::make_nsstring(key);
let value = util::make_nsstring(&value);
let key = NSString::from_str(key);
let value = NSString::from_str(&value);
let value: &AnyObject = &value;
let _: () = msg_send![*self.user_defaults, setObject: *value forKey: *key];
Ok(())
// `setObject:forKey:` stores an arbitrary object; the value and key are
// both `NSString`s, which are valid property-list types.
unsafe {
self.user_defaults.setObject_forKey(Some(value), &key);
}
Ok(())
}
fn read_value(&self, key: &str) -> Result<Option<String>, super::Error> {
unsafe {
let key = util::make_nsstring(key);
let value: id = msg_send![*self.user_defaults, stringForKey: *key];
if value != nil {
Ok(Some(
galaxyui::platform::mac::utils::nsstring_as_str(value)?.to_owned(),
))
} else {
Ok(None)
}
let key = NSString::from_str(key);
match self.user_defaults.stringForKey(&key) {
Some(value) => Ok(Some(value.to_string())),
None => Ok(None),
}
}
fn remove_value(&self, key: &str) -> Result<(), super::Error> {
unsafe {
let key = util::make_nsstring(key);
let _: () = msg_send![*self.user_defaults, removeObjectForKey: *key];
Ok(())
}
}
}
mod util {
use cocoa::{base::nil, foundation::NSString};
use objc::rc::StrongPtr;
/// Creates a new `NSString` from the given `&str`, wrapped in a
/// [`StrongPtr`] so it is released when the `StrongPtr` is dropped.
///
/// **Important:** when passing the result to `msg_send!`, always
/// dereference it (e.g. `msg_send![obj, foo: *nsstring]`). Passing the
/// `StrongPtr` itself by value causes it to be moved into the
/// `unsafe extern fn` call that `msg_send!` transmutes to, and Rust will
/// not run the `StrongPtr`'s `Drop` glue after that call, leaking the
/// underlying `NSString`.
pub fn make_nsstring(value: &str) -> StrongPtr {
unsafe { StrongPtr::new(NSString::alloc(nil).init_str(value)) }
let key = NSString::from_str(key);
self.user_defaults.removeObjectForKey(&key);
Ok(())
}
}