dhcpv4/
stash.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::configuration::ServerParameters;
6use crate::protocol::identifier::ClientIdentifier;
7use crate::protocol::{DhcpOption, OptionCode};
8use crate::server::{ClientRecords, DataStore, LeaseRecord};
9use log::warn;
10use std::collections::{HashMap, HashSet};
11use std::str::FromStr as _;
12use std::string::ToString as _;
13
14/// A wrapper around a `fuchsia.stash.StoreAccessor` proxy.
15///
16/// Stash provides a simple API by which the DHCP `Server` can store and load client configuration
17/// data to persistent storage.
18///
19/// This wrapper stores client configuration as serialized JSON strings. The decision to use JSON
20/// derives from its use in other Stash clients, cf. commit e9c57a0, and the relative immaturity of
21/// more compact serde serialization formats, e.g. https://github.com/pyfisch/cbor/issues.
22#[derive(Clone, Debug)]
23pub struct Stash {
24    prefix: String,
25    proxy: fidl_fuchsia_stash::StoreAccessorProxy,
26}
27
28#[derive(Debug, thiserror::Error)]
29pub enum StashError {
30    #[error("stash initialized with empty prefix")]
31    EmptyPrefix,
32    #[error(
33        "stash initialized with prefix={prefix} containing invalid character(s) {invalid_chars:?}"
34    )]
35    InvalidPrefix { prefix: String, invalid_chars: HashSet<char> },
36    #[error("unexpected value variant stored in stash: {actual:?}, expected {expected:?}")]
37    UnexpectedStashValue { actual: fidl_fuchsia_stash::Value, expected: fidl_fuchsia_stash::Value },
38    #[error("failed to deserialize json string={0}")]
39    JsonDeserialization(String, #[source] serde_json::Error),
40    #[error("failed to serialize to json for key={0}")]
41    JsonSerialization(String, #[source] serde_json::Error),
42    #[error("stash does not contain value for key={0}")]
43    MissingValue(String),
44    #[error("FIDL call to stash failed: {0}")]
45    Fidl(#[from] fidl::Error),
46    #[error("connecting to stash failed")]
47    StashConnect(#[source] anyhow::Error),
48}
49
50impl DataStore for Stash {
51    type Error = StashError;
52
53    fn insert(
54        &mut self,
55        client_id: &ClientIdentifier,
56        client_record: &LeaseRecord,
57    ) -> Result<(), Self::Error> {
58        self.store(&self.client_key(client_id), client_record)
59    }
60
61    fn store_options(&mut self, opts: &[DhcpOption]) -> Result<(), Self::Error> {
62        self.store(OPTIONS_KEY, opts)
63    }
64
65    fn store_parameters(&mut self, params: &ServerParameters) -> Result<(), Self::Error> {
66        self.store(PARAMETERS_KEY, params)
67    }
68
69    fn delete(&mut self, client_id: &ClientIdentifier) -> Result<(), Self::Error> {
70        self.rm_key(&self.client_key(client_id))
71    }
72}
73
74const OPTIONS_KEY: &'static str = "options";
75const PARAMETERS_KEY: &'static str = "parameters";
76const CLIENT_KEY_PREFIX: &'static str = "client";
77
78impl Stash {
79    /// Instantiates a new `Stash` value.
80    ///
81    /// The newly instantiated value will use `id` to identify itself with the `fuchsia.stash`
82    /// service.
83    pub fn new(id: &str) -> Result<Self, StashError> {
84        Self::new_with_prefix(id, CLIENT_KEY_PREFIX)
85    }
86
87    fn new_with_prefix(id: &str, prefix: &str) -> Result<Self, StashError> {
88        if prefix.is_empty() {
89            return Err(StashError::EmptyPrefix);
90        }
91        let invalid_chars: HashSet<char> =
92            prefix.matches(&['-', ':'][..]).map(|s| char::from_str(s).unwrap()).collect();
93        if !invalid_chars.is_empty() {
94            return Err(StashError::InvalidPrefix { prefix: prefix.to_string(), invalid_chars });
95        }
96        let store_client = fuchsia_component::client::connect_to_protocol::<
97            fidl_fuchsia_stash::SecureStoreMarker,
98        >()
99        .map_err(StashError::StashConnect)?;
100        let () = store_client.identify(id)?;
101        let (proxy, accessor_server) =
102            fidl::endpoints::create_proxy::<fidl_fuchsia_stash::StoreAccessorMarker>();
103        let () = store_client.create_accessor(false, accessor_server)?;
104        let prefix = prefix.to_string();
105        Ok(Stash { prefix, proxy })
106    }
107
108    fn store<T>(&self, key: &str, v: &T) -> Result<(), StashError>
109    where
110        T: serde::Serialize + std::fmt::Debug + ?Sized,
111    {
112        let v = fidl_fuchsia_stash::Value::Stringval(
113            serde_json::to_string(v)
114                .map_err(|e| StashError::JsonSerialization(key.to_string(), e))?,
115        );
116        let () = self.proxy.set_value(key, v)?;
117        let () = self.proxy.commit()?;
118        Ok(())
119    }
120
121    /// Loads a `ClientRecords` map from data stored in `fuchsia.stash`.
122    ///
123    /// This function will retrieve all client configuration data from `fuchsia.stash`, deserialize
124    /// the JSON string values, and load the resulting structured data into a `ClientRecords`
125    /// hashmap. Any key-value pair which could not be parsed or deserialized will be removed and
126    /// skipped.
127    pub async fn load_client_records(&self) -> Result<ClientRecords, StashError> {
128        use futures::TryStreamExt as _;
129
130        let (iter, server) =
131            fidl::endpoints::create_proxy::<fidl_fuchsia_stash::GetIteratorMarker>();
132        let () = self.proxy.get_prefix(&self.prefix, server)?;
133        futures::stream::try_unfold(iter, |iter| async move {
134            let kvs = iter.get_next().await?;
135            let yielded = (!kvs.is_empty()).then_some((kvs, iter));
136            Result::<_, StashError>::Ok(yielded)
137        })
138        .map_ok(|kvs| futures::stream::iter(kvs.into_iter().map(Ok)))
139        .try_flatten()
140        .try_filter_map(|kv| async move {
141            let key = match kv.key.split("-").last() {
142                Some(v) => v,
143                None => {
144                    // Invalid key-value pair: remove the invalid pair and try the next one.
145                    warn!("failed to parse key string: {}", kv.key);
146                    let () = self.rm_key(&kv.key)?;
147                    return Ok(None);
148                }
149            };
150            let key = match ClientIdentifier::from_str(key) {
151                Ok(v) => v,
152                Err(e) => {
153                    warn!("client id from string conversion failed: {}", e);
154                    let () = self.rm_key(&kv.key)?;
155                    return Ok(None);
156                }
157            };
158            let val = match kv.val {
159                fidl_fuchsia_stash::Value::Stringval(v) => v,
160                v => {
161                    warn!("invalid value variant stored in stash: {:?}", v);
162                    let () = self.rm_key(&kv.key)?;
163                    return Ok(None);
164                }
165            };
166            let val: LeaseRecord = match serde_json::from_str(&val) {
167                Ok(v) => v,
168                Err(e) => {
169                    warn!("failed to parse JSON from string: {}", e);
170                    let () = self.rm_key(&kv.key)?;
171                    return Ok(None);
172                }
173            };
174            Ok(Some((key, val)))
175        })
176        .try_collect()
177        .await
178    }
179
180    /// Loads a map of `OptionCode`s to `DhcpOption`s from data stored in `fuchsia.stash`.
181    pub async fn load_options(&self) -> Result<HashMap<OptionCode, DhcpOption>, StashError> {
182        let val = self.proxy.get_value(OPTIONS_KEY).await?;
183        let val = match val {
184            Some(v) => v,
185            None => return Ok(HashMap::new()),
186        };
187        match *val {
188            fidl_fuchsia_stash::Value::Stringval(v) => {
189                Ok(serde_json::from_str::<Vec<DhcpOption>>(&v)
190                    .map_err(|e| StashError::JsonDeserialization(v.clone(), e))?
191                    .into_iter()
192                    .map(|opt| (opt.code(), opt))
193                    .collect())
194            }
195            v => Err(StashError::UnexpectedStashValue {
196                actual: v,
197                expected: fidl_fuchsia_stash::Value::Stringval(String::new()),
198            }),
199        }
200    }
201
202    /// Loads a new instance of `ServerParameters` from data stored in `fuchsia.stash`.
203    pub async fn load_parameters(&self) -> Result<ServerParameters, StashError> {
204        let val = self
205            .proxy
206            .get_value(PARAMETERS_KEY)
207            .await?
208            .ok_or(StashError::MissingValue(PARAMETERS_KEY.to_string()))?;
209        match *val {
210            fidl_fuchsia_stash::Value::Stringval(v) => Ok(serde_json::from_str(&v)
211                .map_err(|e| StashError::JsonDeserialization(v.clone(), e))?),
212            v => Err(StashError::UnexpectedStashValue {
213                actual: v,
214                expected: fidl_fuchsia_stash::Value::Stringval(String::new()),
215            }),
216        }
217    }
218
219    fn rm_key(&self, key: &str) -> Result<(), StashError> {
220        let () = self.proxy.delete_value(key)?;
221        let () = self.proxy.commit()?;
222        Ok(())
223    }
224
225    /// Clears all configuration data from `fuchsia.stash`.
226    ///
227    /// This function will delete all key-value pairs associated with the `Stash` value.
228    pub fn clear(&self) -> Result<(), StashError> {
229        let () = self.proxy.delete_prefix(&self.prefix)?;
230        let () = self.proxy.commit()?;
231        Ok(())
232    }
233
234    #[cfg(test)]
235    pub fn clone_proxy(&self) -> fidl_fuchsia_stash::StoreAccessorProxy {
236        self.proxy.clone()
237    }
238
239    pub(crate) fn client_key(&self, client_id: &ClientIdentifier) -> String {
240        format!("{}-{}", self.prefix, client_id)
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use crate::configuration::{LeaseLength, ManagedAddresses};
248    use net_declare::net::prefix_length_v4;
249    use net_declare::std::ip_v4;
250
251    /// Creates a new stash instance with a randomized identifier to prevent test flakes.
252    ///
253    /// `prefix` must not contain either '-' or ':' as they are used as field delimiters in stash
254    /// keys.
255    fn new_stash(test_prefix: &str) -> Result<(Stash, String), StashError> {
256        use rand::distributions::DistString as _;
257        let rand_id = rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 8);
258        let stash = Stash::new_with_prefix(&rand_id, test_prefix)?;
259        // Clear the Stash of any data leftover from the previous test.
260        let () = stash.proxy.delete_prefix(&stash.prefix)?;
261        let () = stash.proxy.commit()?;
262        Ok((stash, rand_id))
263    }
264
265    #[fuchsia_async::run_singlethreaded(test)]
266    async fn stash_new_with_prefix() {
267        assert_matches::assert_matches!(
268            Stash::new_with_prefix("stash_new", "valid"),
269            Ok(Stash { .. })
270        );
271        assert_matches::assert_matches!(
272            Stash::new_with_prefix("stash_new", "invalid-"),
273            Err(StashError::InvalidPrefix { .. })
274        );
275        assert_matches::assert_matches!(
276            Stash::new_with_prefix("stash_new", "invalid:"),
277            Err(StashError::InvalidPrefix { .. })
278        );
279        assert_matches::assert_matches!(
280            Stash::new_with_prefix("stash_new", ""),
281            Err(StashError::EmptyPrefix)
282        );
283        let () = match Stash::new_with_prefix("stash_new", "a-b-c-d") {
284            Err(StashError::InvalidPrefix { invalid_chars, prefix: _prefix }) => {
285                assert_eq!(invalid_chars, ['-'].iter().cloned().collect())
286            }
287            v => {
288                panic!("new_with_prefix returned {:?}, expected StashError::InvalidPrefix{{..}}", v)
289            }
290        };
291        let () = match Stash::new_with_prefix("stash_new", "a-b-c:d-e") {
292            Err(StashError::InvalidPrefix { invalid_chars, prefix: _prefix }) => {
293                assert_eq!(invalid_chars, ['-', ':'].iter().cloned().collect())
294            }
295            v => {
296                panic!("new_with_prefix returned {:?}, expected StashError::InvalidPrefix{{..}}", v)
297            }
298        };
299    }
300
301    #[fuchsia_async::run_singlethreaded(test)]
302    async fn store_client_succeeds() {
303        let (mut stash, id) =
304            new_stash("store_client_succeeds").expect("failed to create new stash");
305        let accessor_client = stash.proxy.clone();
306
307        // Store value in stash.
308        let client_id = ClientIdentifier::from(crate::server::tests::random_mac_generator());
309        let client_record = LeaseRecord::default();
310        let () = stash
311            .insert(&client_id, &client_record)
312            .unwrap_or_else(|err| panic!("failed to store client in {}: {:?}", id, err));
313
314        // Verify value actually stored in stash.
315        let value = accessor_client
316            .get_value(&stash.client_key(&client_id))
317            .await
318            .unwrap_or_else(|err| panic!("failed to get value from {}: {:?}", id, err));
319        let value = match *value.unwrap() {
320            fidl_fuchsia_stash::Value::Stringval(v) => v,
321            v => panic!("stored value is not a string: {:?}", v),
322        };
323        let value: LeaseRecord =
324            serde_json::from_str(&value).expect("failed to decode lease record");
325        assert_eq!(value, client_record);
326    }
327
328    #[fuchsia_async::run_singlethreaded(test)]
329    async fn store_options_succeeds() {
330        let (mut stash, id) = new_stash("store_options_succeeds").expect("failed to create stash");
331        let accessor_client = stash.proxy.clone();
332
333        let opts = vec![
334            DhcpOption::SubnetMask(prefix_length_v4!(24)),
335            DhcpOption::DomainNameServer([ip_v4!("1.2.3.4"), ip_v4!("4.3.2.1")].into()),
336        ];
337        let () = stash.store_options(&opts).expect("failed to store options in stash");
338        let value = accessor_client
339            .get_value(&OPTIONS_KEY.to_string())
340            .await
341            .unwrap_or_else(|err| panic!("failed to get value from {}: {:?}", id, err));
342
343        let value = match *value.unwrap() {
344            fidl_fuchsia_stash::Value::Stringval(v) => v,
345            v => panic!("stored value is not a string: {:?}", v),
346        };
347        let value: Vec<DhcpOption> = serde_json::from_str(&value)
348            .unwrap_or_else(|err| panic!("failed to deserialize from {}: {:?}", value, err));
349        assert_eq!(value, opts);
350    }
351
352    #[fuchsia_async::run_singlethreaded(test)]
353    async fn store_parameters_succeeds() {
354        let (mut stash, id) =
355            new_stash("store_parameters_succeeds").expect("failed to create stash");
356        let accessor_client = stash.proxy.clone();
357
358        let params = ServerParameters {
359            server_ips: vec![ip_v4!("192.168.0.1")],
360            lease_length: LeaseLength { default_seconds: 42, max_seconds: 100 },
361            managed_addrs: ManagedAddresses {
362                mask: crate::configuration::SubnetMask::new(prefix_length_v4!(24)),
363                pool_range_start: ip_v4!("192.168.0.10"),
364                pool_range_stop: ip_v4!("192.168.0.254"),
365            },
366            permitted_macs: crate::configuration::PermittedMacs(Vec::new()),
367            static_assignments: crate::configuration::StaticAssignments(HashMap::new()),
368            arp_probe: false,
369            bound_device_names: vec![],
370        };
371        let () = stash.store_parameters(&params).expect("failed to store parameters");
372        let value = accessor_client
373            .get_value(&PARAMETERS_KEY.to_string())
374            .await
375            .unwrap_or_else(|err| panic!("failed to get value from {}: {:?}", id, err));
376
377        let value = match *value.unwrap() {
378            fidl_fuchsia_stash::Value::Stringval(v) => v,
379            v => panic!("stored value is not a string: {:?}", v),
380        };
381        let value: ServerParameters = serde_json::from_str(&value)
382            .unwrap_or_else(|err| panic!("failed to deserialize from {}: {:?}", value, err));
383        assert_eq!(value, params);
384    }
385
386    #[fuchsia_async::run_singlethreaded(test)]
387    async fn load_clients_with_populated_stash_returns_cached_clients() {
388        let (stash, id) = new_stash("load_clients_with_populated_stash_returns_cached_clients")
389            .expect("failed to create stash");
390        let accessor = stash.proxy.clone();
391
392        let client_id = ClientIdentifier::from(crate::server::tests::random_mac_generator());
393        let client_record = LeaseRecord::default();
394        let serialized_client =
395            serde_json::to_string(&client_record).expect("serialization failed");
396        let client_key = stash.client_key(&client_id);
397        let client_val = fidl_fuchsia_stash::Value::Stringval(serialized_client);
398        let () = accessor
399            .set_value(&client_key, client_val)
400            .unwrap_or_else(|err| panic!("failed to set value in {}: {:?}", id, err));
401        let () = accessor.commit().unwrap_or_else(|err| {
402            panic!("failed to commit stash state change in {}: {:?}", id, err)
403        });
404
405        let loaded_cache = stash
406            .load_client_records()
407            .await
408            .unwrap_or_else(|err| panic!("failed to load map from stash in {}: {:?}", id, err));
409
410        let cached_clients = std::iter::once((client_id, client_record)).collect();
411        assert_eq!(loaded_cache, cached_clients);
412    }
413
414    #[fuchsia_async::run_singlethreaded(test)]
415    async fn load_options_with_stashed_options_returns_options() {
416        let (stash, id) = new_stash("load_options_with_stashed_options_returns_options")
417            .expect("failed to create stash");
418        let accessor = stash.proxy.clone();
419
420        let opts = vec![
421            DhcpOption::SubnetMask(prefix_length_v4!(24)),
422            DhcpOption::DomainNameServer([ip_v4!("1.2.3.4"), ip_v4!("4.3.2.1")].into()),
423        ];
424        let serialized_opts = serde_json::to_string(&opts).expect("serialization failed");
425        let opts = opts.into_iter().map(|o| (o.code(), o)).collect();
426        let () = accessor
427            .set_value(
428                &OPTIONS_KEY.to_string(),
429                fidl_fuchsia_stash::Value::Stringval(serialized_opts),
430            )
431            .unwrap_or_else(|err| {
432                panic!("failed to set value in stash for key={}: {:?}", OPTIONS_KEY, err)
433            });
434        let () = accessor.commit().unwrap_or_else(|err| {
435            panic!("failed to commit stash state change in {}: {:?}", id, err)
436        });
437
438        let loaded_opts = stash.load_options().await.expect("failed to load options");
439        assert_eq!(loaded_opts, opts);
440    }
441
442    #[fuchsia_async::run_singlethreaded(test)]
443    async fn load_options_with_no_stashed_options_returns_empty_map() {
444        let (stash, _id) = new_stash("load_options_with_no_stashed_options_returns_empty_vec")
445            .expect("failed to create stash");
446
447        let opts = stash.load_options().await.expect("failed to load options");
448        assert_eq!(opts, HashMap::new());
449    }
450
451    #[fuchsia_async::run_singlethreaded(test)]
452    async fn load_parameters_with_stashed_parameters_returns_parameters() {
453        let (stash, id) = new_stash("load_parameters_with_stashed_parameters_returns_parameters")
454            .expect("faield to create stash");
455        let accessor = stash.proxy.clone();
456
457        let params = ServerParameters {
458            server_ips: vec![ip_v4!("192.168.0.1")],
459            lease_length: LeaseLength { default_seconds: 42, max_seconds: 100 },
460            managed_addrs: ManagedAddresses {
461                mask: crate::configuration::SubnetMask::new(prefix_length_v4!(24)),
462                pool_range_start: ip_v4!("192.168.0.10"),
463                pool_range_stop: ip_v4!("192.168.0.254"),
464            },
465            permitted_macs: crate::configuration::PermittedMacs(Vec::new()),
466            static_assignments: crate::configuration::StaticAssignments(HashMap::new()),
467            arp_probe: false,
468            bound_device_names: vec![],
469        };
470        let serialized_params = serde_json::to_string(&params).expect("serialization failed");
471        let () = accessor
472            .set_value(
473                &PARAMETERS_KEY.to_string(),
474                fidl_fuchsia_stash::Value::Stringval(serialized_params),
475            )
476            .unwrap_or_else(|err| {
477                panic!("failed to set value in stash for key={}: {:?}", OPTIONS_KEY, err)
478            });
479        let () = accessor.commit().unwrap_or_else(|err| {
480            panic!("failed to commit stash state change in {}: {:?}", id, err)
481        });
482
483        let loaded_params = stash.load_parameters().await.expect("failed to load parameters");
484        assert_eq!(loaded_params, params);
485    }
486
487    #[fuchsia_async::run_singlethreaded(test)]
488    async fn load_parameters_with_no_stashed_parameters_returns_err() {
489        let (stash, _id) = new_stash("load_parameters_with_no_stashed_parameters_returns_err")
490            .expect("failed to create stash");
491        assert_matches::assert_matches!(
492            stash.load_parameters().await.expect_err("load_parameters should have returned err"),
493            StashError::MissingValue(String { .. })
494        );
495    }
496
497    #[fuchsia_async::run_singlethreaded(test)]
498    async fn load_clients_with_stash_containing_invalid_entries_returns_empty_cache() {
499        let (stash, id) =
500            new_stash("load_clients_with_stash_containing_invalid_entries_returns_empty_cache")
501                .expect("failed to create stash");
502        let accessor = stash.proxy.clone();
503
504        let client_id = ClientIdentifier::from(crate::server::tests::random_mac_generator());
505        let client_record = LeaseRecord::default();
506        let serialized_client =
507            serde_json::to_string(&client_record).expect("serialization failed");
508        let invalid_key = "invalid_key";
509        let client_stringval = fidl_fuchsia_stash::Value::Stringval(serialized_client);
510        let () = accessor.set_value(invalid_key, client_stringval).unwrap_or_else(|err| {
511            panic!("failed to set value in stash for key={}: {:?}", OPTIONS_KEY, err)
512        });
513        let client_key = stash.client_key(&client_id);
514        let client_intval = fidl_fuchsia_stash::Value::Intval(42);
515        let () = accessor.set_value(&client_key, client_intval).unwrap_or_else(|err| {
516            panic!("failed to set value in stash for key={}: {:?}", OPTIONS_KEY, err)
517        });
518        let () = accessor.commit().unwrap_or_else(|err| {
519            panic!("failed to commit stash state change in {}: {:?}", id, err)
520        });
521
522        let loaded_cache = stash
523            .load_client_records()
524            .await
525            .unwrap_or_else(|err| panic!("failed to load map from stash in {}: {:?}", id, err));
526
527        let empty_cache = HashMap::new();
528        assert_eq!(loaded_cache, empty_cache);
529    }
530
531    #[fuchsia_async::run_singlethreaded(test)]
532    async fn delete_client_succeeds() {
533        let (mut stash, id) = new_stash("delete_client_succeeds").expect("failed to create stash");
534        let accessor = stash.proxy.clone();
535
536        // Store value in stash.
537        let client_id = ClientIdentifier::from(crate::server::tests::random_mac_generator());
538        let client_record = LeaseRecord::default();
539        let () = stash
540            .insert(&client_id, &client_record)
541            .unwrap_or_else(|err| panic!("failed to store client in {}: {:?}", id, err));
542
543        // Verify value actually stored in stash.
544        let client_key = stash.client_key(&client_id);
545        let value = accessor
546            .get_value(&client_key)
547            .await
548            .unwrap_or_else(|err| panic!("failed to get value from {}: {:?}", id, err));
549        assert!(value.is_some());
550
551        // Delete value and verify its absence.
552        let () = stash
553            .delete(&client_id)
554            .unwrap_or_else(|err| panic!("failed to delete client in {}: {:?}", id, err));
555        let value = accessor
556            .get_value(&client_key)
557            .await
558            .unwrap_or_else(|err| panic!("failed to get value from {}: {:?}", id, err));
559        assert!(value.is_none());
560    }
561
562    #[fuchsia_async::run_singlethreaded(test)]
563    async fn clear_with_populated_stash_clears_stash() {
564        let (stash, id) =
565            new_stash("clear_with_populated_stash_clears_stash").expect("failed to create stash");
566        let accessor = stash.proxy.clone();
567
568        // Store a value in the stash.
569        let client_mac = ClientIdentifier::from(crate::server::tests::random_mac_generator());
570        let client_record = LeaseRecord::default();
571        let serialized_client =
572            serde_json::to_string(&client_record).expect("serialization failed");
573        let client_key = stash.client_key(&client_mac);
574        let client_val = fidl_fuchsia_stash::Value::Stringval(serialized_client);
575        let () = accessor.set_value(&client_key, client_val).unwrap_or_else(|err| {
576            panic!("failed to set value in stash for key={}: {:?}", OPTIONS_KEY, err)
577        });
578        let () = accessor.commit().unwrap_or_else(|err| {
579            panic!("failed to commit stash state change in {}: {:?}", id, err)
580        });
581
582        // Clear the stash.
583        let () = stash
584            .clear()
585            .unwrap_or_else(|err| panic!("failed to clear stash in {}: {:?}", id, err));
586
587        // Verify that the stash is actually empty.
588        let (iter, server) =
589            fidl::endpoints::create_proxy::<fidl_fuchsia_stash::GetIteratorMarker>();
590        let () = accessor.get_prefix(&stash.prefix, server).expect("failed to get prefix iterator");
591        let stash_contents = iter.get_next().await.expect("failed to get next item for iterator");
592        assert_eq!(stash_contents.len(), 0);
593    }
594}