Skip to main content

emergency_lib/
bss_resolver.rs

1// Copyright 2020 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::bss_cache::{Bss, BssId};
6use async_trait::async_trait;
7use fidl::{Error as FidlError, Socket as FidlSocket};
8use fidl_fuchsia_location_position::{Position, PositionExtras};
9use fidl_fuchsia_mem::Buffer as MemBuffer;
10use fidl_fuchsia_net_http::{
11    Body as HttpBody, LoaderProxyInterface, Request as HttpRequest, Response as HttpResponse,
12};
13
14use futures::Future;
15use futures::io::AsyncReadExt;
16use itertools::Itertools;
17use serde_json::json;
18use serde_json::map::Map as JsonMap;
19use serde_json::value::Value as JsonValue;
20use static_assertions::assert_eq_size_val;
21use std::borrow::Borrow;
22
23// Geographic constants.
24const LATITUDE_RANGE: std::ops::RangeInclusive<f64> = -90.0..=90.0;
25const LONGITUDE_RANGE: std::ops::RangeInclusive<f64> = -180.0..=180.0;
26
27// Google Maps constants.
28const SERVICE_URL: &'static str = "https://www.googleapis.com/geolocation/v1/geolocate";
29const MAC_ADDR_KEY: &'static str = "macAddress";
30const AP_LIST_KEY: &'static str = "wifiAccessPoints";
31const RSSI_KEY: &'static str = "signalStrength";
32const LOCATION_KEY: &'static str = "location";
33const LATITUDE_KEY: &'static str = "lat";
34const LONGITUDE_KEY: &'static str = "lng";
35const ACCURACY_KEY: &'static str = "accuracy";
36
37// HTTP constants.
38const HTTP_METHOD_POST: &'static str = "POST";
39
40#[async_trait(?Send)]
41pub trait BssResolver {
42    /// Resolves WLAN BSS (base-station) meta-data to a `Position`.
43    async fn resolve<'a, I, T, U>(&self, bsses: I) -> Result<Position, ResolverError>
44    where
45        I: IntoIterator,
46        I::Item: Borrow<(T, U)>,
47        T: Borrow<BssId>,
48        U: Borrow<Bss>;
49}
50
51/// A service for resolving WLAN BSS (base-station) meta-data to `Position`s.
52pub struct RealBssResolver<L: LoaderProxyInterface> {
53    http_loader: L,
54    api_key: String,
55}
56
57#[derive(Clone, Copy, Debug, PartialEq)]
58pub enum ResolverError {
59    NoBsses,
60    Internal,
61    Lookup,
62}
63
64impl<L: LoaderProxyInterface> RealBssResolver<L> {
65    pub fn new(http_loader: L, api_key: impl Into<String>) -> Self {
66        Self { http_loader, api_key: api_key.into() }
67    }
68}
69
70#[async_trait(?Send)]
71impl<L: LoaderProxyInterface> BssResolver for RealBssResolver<L> {
72    async fn resolve<'a, I, T, U>(&self, bsses: I) -> Result<Position, ResolverError>
73    where
74        I: IntoIterator,
75        I::Item: Borrow<(T, U)>,
76        T: Borrow<BssId>,
77        U: Borrow<Bss>,
78    {
79        let mut bsses = bsses.into_iter().peekable();
80        if bsses.peek().is_none() {
81            return Err(ResolverError::NoBsses);
82        }
83        parse_response(send_query(bsses, &self.api_key, &self.http_loader)?.await).await
84    }
85}
86
87fn send_query<'a, I, T, U>(
88    bsses: I,
89    api_key: impl AsRef<str>,
90    http_loader: &impl LoaderProxyInterface,
91) -> Result<impl Future<Output = Result<HttpResponse, FidlError>>, ResolverError>
92where
93    I: Iterator,
94    I::Item: Borrow<(T, U)>,
95    T: Borrow<BssId>,
96    U: Borrow<Bss>,
97{
98    let api_key = api_key.as_ref();
99    let request_body = serialize_bsses(bsses);
100    let request_size = {
101        let body_size = request_body.len();
102        assert_eq_size_val!(body_size, 0u64);
103        u64::try_from(body_size).expect("failed to convert usize to u64")
104    };
105    let request_vmo = zx::Vmo::create(request_size).map_err(|_| ResolverError::Internal)?;
106    let _ = request_vmo.write(&request_body.as_bytes(), 0).map_err(|_| ResolverError::Internal)?;
107    Ok(http_loader.fetch(HttpRequest {
108        method: Some(HTTP_METHOD_POST.to_owned()),
109        url: Some(format!("{}?key={}", SERVICE_URL, api_key)),
110        headers: None,
111        body: Some(HttpBody::Buffer(MemBuffer { vmo: request_vmo, size: request_size })),
112        deadline: None,
113        ..Default::default()
114    }))
115}
116
117async fn parse_response(
118    response: Result<HttpResponse, FidlError>,
119) -> Result<Position, ResolverError> {
120    let response: HttpResponse = response.map_err(|_fidl_error| ResolverError::Internal)?;
121    let response: FidlSocket = response.body.ok_or(ResolverError::Lookup)?;
122    let response: String = read_socket(response).await.ok_or(ResolverError::Lookup)?;
123    json_to_position(serde_json::from_str(&response).map_err(|_| ResolverError::Lookup)?)
124}
125
126fn serialize_bsses<'a, I, T, U>(bsses: I) -> String
127where
128    I: Iterator,
129    I::Item: Borrow<(T, U)>,
130    T: Borrow<BssId>,
131    U: Borrow<Bss>,
132{
133    let bsses = bsses
134        .map(|item| bss_to_json(item.borrow().0.borrow(), item.borrow().1.borrow()))
135        .collect::<Vec<_>>();
136    json!({ AP_LIST_KEY: bsses }).to_string()
137}
138
139fn bss_to_json(bss_id: impl Borrow<BssId>, bss: impl Borrow<Bss>) -> JsonValue {
140    let mut json = JsonMap::new();
141    json.insert(
142        MAC_ADDR_KEY.to_owned(),
143        json!(bss_id.borrow().iter().map(|bss_byte| format!("{:02x}", bss_byte)).join(":")),
144    );
145    if let Some(rssi) = bss.borrow().rssi {
146        json.insert(RSSI_KEY.to_owned(), json!(rssi));
147    };
148    JsonValue::from(json)
149}
150
151async fn read_socket(socket: zx::Socket) -> Option<String> {
152    let mut buf = Vec::new();
153    let mut socket = fuchsia_async::Socket::from_socket(socket);
154    match socket.read_to_end(&mut buf).await {
155        Ok(_num_bytes_read) => String::from_utf8(buf).ok(),
156        Err(_) => None,
157    }
158}
159
160fn json_to_position(response: JsonValue) -> Result<Position, ResolverError> {
161    let latitude = response[LOCATION_KEY][LATITUDE_KEY].as_f64().ok_or(ResolverError::Lookup)?;
162    if !LATITUDE_RANGE.contains(&latitude) {
163        return Err(ResolverError::Lookup);
164    }
165
166    let longitude = response[LOCATION_KEY][LONGITUDE_KEY].as_f64().ok_or(ResolverError::Lookup)?;
167    if !LONGITUDE_RANGE.contains(&longitude) {
168        return Err(ResolverError::Lookup);
169    }
170
171    let extras = match response[ACCURACY_KEY].as_f64() {
172        Some(accuracy) => PositionExtras {
173            accuracy_meters: Some(accuracy),
174            altitude_meters: None,
175            ..Default::default()
176        },
177        None => {
178            PositionExtras { accuracy_meters: None, altitude_meters: None, ..Default::default() }
179        }
180    };
181
182    Ok(Position { latitude, longitude, extras })
183}
184
185#[cfg(test)]
186mod tests {
187    mod request_generation {
188        use super::super::test_doubles::HttpRequestValidator;
189        use super::super::*;
190
191        #[fuchsia::test(allow_stalls = false)]
192        async fn request_uses_post_method() {
193            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: Some(-30), frequency: Some(2412) })];
194            let mut request_method = None;
195            let http_loader = HttpRequestValidator::new(|request| {
196                request_method = Some(request.method.expect("request had no method"));
197            });
198            let _ = RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await;
199            assert_eq!(request_method.expect("request was never issued"), "POST".to_owned());
200        }
201
202        #[fuchsia::test(allow_stalls = false)]
203        async fn request_url_is_correct() {
204            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: Some(-30), frequency: Some(2412) })];
205            let mut request_url = None;
206            let http_loader = HttpRequestValidator::new(|request| {
207                request_url = Some(request.url.expect("request had no url"));
208            });
209            let _ = RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await;
210            assert_eq!(
211                request_url.expect("request was never issued"),
212                "https://www.googleapis.com/geolocation/v1/geolocate?key=fake_key".to_owned()
213            );
214        }
215
216        #[fuchsia::test(allow_stalls = false)]
217        async fn request_body_contains_all_bsses() {
218            let bsses = vec![
219                ([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None }),
220                ([1, 1, 1, 1, 1, 1], Bss { rssi: None, frequency: None }),
221            ];
222            let mut request_body = None;
223            let http_loader = HttpRequestValidator::new(|request| {
224                request_body = Some(request.body.expect("request had no body"));
225            });
226            let _ = RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await;
227            assert_eq!(
228                strip_whitespace(read_request_body(
229                    request_body.expect("request was never issued")
230                )),
231                strip_whitespace(
232                    r#"{
233                    "wifiAccessPoints":[
234                        {"macAddress":"00:00:00:00:00:00"},
235                        {"macAddress":"01:01:01:01:01:01"}
236                    ]
237                }"#
238                )
239            );
240        }
241
242        #[fuchsia::test(allow_stalls = false)]
243        async fn request_body_includes_rssi() {
244            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: Some(-30), frequency: None })];
245            let mut request_body = None;
246            let http_loader = HttpRequestValidator::new(|request| {
247                request_body = Some(request.body.expect("request had no body"));
248            });
249            let _ = RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await;
250            assert_eq!(
251                strip_whitespace(read_request_body(
252                    request_body.expect("request was never issued")
253                )),
254                strip_whitespace(
255                    r#"{
256                    "wifiAccessPoints":[
257                        {"macAddress":"00:00:00:00:00:00",
258                         "signalStrength": -30}
259                    ]
260                }"#
261                )
262            )
263        }
264
265        #[fuchsia::test(allow_stalls = false)]
266        async fn skips_api_query_when_bsses_is_empty() {
267            let http_loader =
268                HttpRequestValidator::new(|_request| panic!("should not issue API query"));
269            let _ = RealBssResolver::new(http_loader, "fake_key")
270                .resolve(std::iter::empty::<(BssId, Bss)>())
271                .await;
272        }
273
274        #[fuchsia::test(allow_stalls = false)]
275        async fn returns_no_bsses_error_when_bsses_is_empty() {
276            let http_loader = HttpRequestValidator::new(|_| ());
277            assert_eq!(
278                RealBssResolver::new(http_loader, "fake_key")
279                    .resolve(std::iter::empty::<(BssId, Bss)>())
280                    .await,
281                Err(ResolverError::NoBsses)
282            );
283        }
284
285        fn read_request_body(body: HttpBody) -> String {
286            match body {
287                HttpBody::Buffer(shared_buf) => {
288                    let mut local_buf = vec![
289                        0_u8;
290                        usize::try_from(shared_buf.size).expect(
291                            "internal error: failed to convert u64 to usize"
292                        )
293                    ];
294                    shared_buf
295                        .vmo
296                        .read(&mut local_buf, 0)
297                        .expect("internal error: failed to read from VMO");
298                    String::from_utf8(local_buf)
299                        .expect("internal error: failed to convert buffer to UTF-8")
300                }
301                HttpBody::Stream(_) => panic!("internal error: stream bodies not supported"),
302            }
303        }
304
305        fn strip_whitespace<S: AsRef<str>>(input: S) -> String {
306            input.as_ref().replace("\n", "").replace(" ", "")
307        }
308    }
309
310    mod response_error_handling {
311        use super::super::test_doubles::{HttpByteResponder, HttpFidlResponder};
312        use super::super::*;
313
314        #[fuchsia::test(allow_stalls = false)]
315        async fn returns_internal_error_on_fidl_error() {
316            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
317            let http_loader = HttpFidlResponder::new(|| Err(FidlError::InvalidHeader));
318            assert_eq!(
319                RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
320                Err(ResolverError::Internal)
321            );
322        }
323
324        #[fuchsia::test(allow_stalls = false)]
325        async fn returns_lookup_error_when_response_has_no_body() {
326            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
327            let http_loader = HttpFidlResponder::new(|| {
328                Ok(HttpResponse {
329                    error: None,
330                    body: None,
331                    final_url: None,
332                    status_code: None,
333                    status_line: None,
334                    headers: None,
335                    redirect: None,
336                    ..Default::default()
337                })
338            });
339            assert_eq!(
340                RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
341                Err(ResolverError::Lookup)
342            );
343        }
344
345        #[fuchsia::test(allow_stalls = false)]
346        async fn returns_lookup_error_when_body_is_unreadable() {
347            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
348            let http_loader = HttpFidlResponder::new(|| {
349                Ok(HttpResponse {
350                    error: None,
351                    body: Some(zx::Socket::from(zx::NullableHandle::invalid())),
352                    final_url: None,
353                    status_code: None,
354                    status_line: None,
355                    headers: None,
356                    redirect: None,
357                    ..Default::default()
358                })
359            });
360            assert_eq!(
361                RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
362                Err(ResolverError::Lookup)
363            );
364        }
365
366        #[fuchsia::test(allow_stalls = false)]
367        async fn returns_lookup_error_when_body_is_not_valid_json() {
368            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
369            let http_loader = HttpByteResponder::new(b"hello world".to_vec());
370            assert_eq!(
371                RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
372                Err(ResolverError::Lookup)
373            );
374        }
375
376        #[fuchsia::test(allow_stalls = false)]
377        async fn returns_lookup_error_when_body_is_not_a_dictionary() {
378            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
379            let http_loader = HttpByteResponder::new(b"42".to_vec());
380            assert_eq!(
381                RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
382                Err(ResolverError::Lookup)
383            );
384        }
385
386        #[fuchsia::test(allow_stalls = false)]
387        async fn returns_lookup_error_when_body_is_missing_location() {
388            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
389            let http_loader = HttpByteResponder::new(b"{}".to_vec());
390            assert_eq!(
391                RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
392                Err(ResolverError::Lookup)
393            );
394        }
395
396        #[fuchsia::test(allow_stalls = false)]
397        async fn returns_lookup_error_when_body_is_missing_latitude() {
398            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
399            let http_loader = HttpByteResponder::new(
400                br#"{
401                        "location": {
402                          "lng": -0.1
403                        },
404                        "accuracy": 1200.4
405                }"#
406                .to_vec(),
407            );
408            assert_eq!(
409                RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
410                Err(ResolverError::Lookup)
411            );
412        }
413
414        #[fuchsia::test(allow_stalls = false)]
415        async fn returns_lookup_error_when_body_is_missing_longitude() {
416            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
417            let http_loader = HttpByteResponder::new(
418                br#"{
419                        "location": {
420                            "lat": 51.0,
421                        },
422                        "accuracy": 1200.4
423                }"#
424                .to_vec(),
425            );
426            assert_eq!(
427                RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
428                Err(ResolverError::Lookup)
429            );
430        }
431
432        #[fuchsia::test(allow_stalls = false)]
433        async fn returns_lookup_error_when_latitude_is_too_high() {
434            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
435            let http_loader = HttpByteResponder::new(
436                br#"{
437                        "location": {
438                            "lat": 90.1,
439                            "lng": 0.0
440                        }
441                }"#
442                .to_vec(),
443            );
444            assert_eq!(
445                RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
446                Err(ResolverError::Lookup)
447            );
448        }
449
450        #[fuchsia::test(allow_stalls = false)]
451        async fn returns_lookup_error_when_latitude_is_too_low() {
452            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
453            let http_loader = HttpByteResponder::new(
454                br#"{
455                        "location": {
456                            "lat": -90.1,
457                            "lng": 0.0
458                        }
459                }"#
460                .to_vec(),
461            );
462            assert_eq!(
463                RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
464                Err(ResolverError::Lookup)
465            );
466        }
467
468        #[fuchsia::test(allow_stalls = false)]
469        async fn returns_lookup_error_when_longitude_is_too_high() {
470            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
471            let http_loader = HttpByteResponder::new(
472                br#"{
473                        "location": {
474                            "lat": 0.0,
475                            "lng": 180.1
476                        }
477                }"#
478                .to_vec(),
479            );
480            assert_eq!(
481                RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
482                Err(ResolverError::Lookup)
483            );
484        }
485
486        #[fuchsia::test(allow_stalls = false)]
487        async fn returns_lookup_error_when_longitude_is_too_low() {
488            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
489            let http_loader = HttpByteResponder::new(
490                br#"{
491                        "location": {
492                            "lat": 0.0,
493                            "lng": -180.1
494                        }
495                }"#
496                .to_vec(),
497            );
498            assert_eq!(
499                RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
500                Err(ResolverError::Lookup)
501            );
502        }
503    }
504
505    mod response_success_reporting {
506        use super::super::test_doubles::HttpByteResponder;
507        use super::super::*;
508        use assert_matches::assert_matches;
509
510        #[fuchsia::test(allow_stalls = false)]
511        async fn returns_success_when_all_fields_are_present() {
512            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
513            let http_loader = HttpByteResponder::new(
514                br#"{
515                        "location": {
516                            "lat": 51.0,
517                            "lng": -0.1
518                        },
519                        "accuracy": 1200.4
520                }"#
521                .to_vec(),
522            );
523            assert_matches!(
524                RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
525                Ok(_)
526            );
527        }
528
529        #[fuchsia::test(allow_stalls = false)]
530        async fn returns_success_when_all_fields_except_accuracy_are_present() {
531            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
532            let http_loader = HttpByteResponder::new(
533                br#"{
534                        "location": {
535                            "lat": 51.0,
536                            "lng": -0.1
537                       }
538                }"#
539                .to_vec(),
540            );
541            assert_matches!(
542                RealBssResolver::new(http_loader, "fake_key").resolve(bsses).await,
543                Ok(_)
544            );
545        }
546    }
547
548    mod response_success_contents {
549        use super::super::test_doubles::HttpByteResponder;
550        use super::super::*;
551        use assert_matches::assert_matches;
552
553        #[fuchsia::test(allow_stalls = false)]
554        async fn provides_precise_latitude() {
555            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
556            let http_loader = HttpByteResponder::new(
557                br#"{
558                        "location": {
559                            "lat": 89.0,
560                            "lng": 0
561                       }
562                }"#
563                .to_vec(),
564            );
565            assert_matches!(
566                RealBssResolver::new(http_loader, "fake_key")
567                    .resolve(bsses)
568                    .await.expect("position is none").latitude,
569                // One degree of latitude is approximately 111 kilometers. By limiting rounding
570                // error to 1/10,000,000 of a degree, we limit rounding error to
571                // 111/10000 = 0.011 meters, or  1.1 cm.
572                latitude if (88.999_999_9..89.000_000_1).contains(&latitude)
573            );
574        }
575
576        #[fuchsia::test(allow_stalls = false)]
577        async fn provides_precise_longitude() {
578            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
579            let http_loader = HttpByteResponder::new(
580                br#"{
581                        "location": {
582                            "lat": 0,
583                            "lng": 179.0
584                       }
585                }"#
586                .to_vec(),
587            );
588            assert_matches!(
589                RealBssResolver::new(http_loader, "fake_key")
590                    .resolve(bsses)
591                    .await.expect("no position").longitude,
592                // At the equator, one degree of longitude is approximately 111 kilometers.
593                // By limiting rounding error to 1/10,000,000 of a degree, we limit rounding error
594                // to 111/10000 = 0.011 meters, or  1.1 cm.
595                longitude if (178.999_999_9..179.099_999_9).contains(&longitude)
596            );
597        }
598
599        #[fuchsia::test(allow_stalls = false)]
600        async fn provides_precise_accuracy_when_present() {
601            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
602            let http_loader = HttpByteResponder::new(
603                br#"{
604                        "location": {
605                            "lat": 51.0,
606                            "lng": -0.1
607                        },
608                        "accuracy": 1200.4
609                }"#
610                .to_vec(),
611            );
612            assert_matches!(
613                RealBssResolver::new(http_loader, "fake_key")
614                    .resolve(bsses)
615                    .await.expect("no position").extras.accuracy_meters.expect("accuracy is none"),
616                accuracy if (1200.39..1200.41).contains(&accuracy)
617            );
618        }
619
620        #[fuchsia::test(allow_stalls = false)]
621        async fn does_not_fabricate_accuracy() {
622            let bsses = vec![([0, 0, 0, 0, 0, 0], Bss { rssi: None, frequency: None })];
623            let http_loader = HttpByteResponder::new(
624                br#"{
625                        "location": {
626                            "lat": 51.0,
627                            "lng": -0.1
628                        }
629                }"#
630                .to_vec(),
631            );
632            assert_eq!(
633                RealBssResolver::new(http_loader, "fake_key")
634                    .resolve(bsses)
635                    .await
636                    .expect("no position")
637                    .extras
638                    .accuracy_meters,
639                None
640            );
641        }
642    }
643}
644
645#[cfg(test)]
646mod test_doubles {
647    use super::*;
648    use fidl::endpoints::ClientEnd;
649    use fidl_fuchsia_net_http::LoaderClientMarker;
650    use fuchsia_sync::RwLock;
651    use futures::future::{Ready, ready};
652
653    const HTTP_OK: u32 = 200;
654
655    type FetchResponse = Result<HttpResponse, FidlError>;
656
657    // Test double that
658    // 1) invokes `validate` to run test assertions, and
659    // 2) returns HTTP_OK, assuming `validate` did not abort
660    pub(super) struct HttpRequestValidator<F>
661    where
662        F: FnMut(HttpRequest) + Send + Sync,
663    {
664        // RwLock is needed because
665        // a) we need interior mutability for F to be FnMut, and
666        // b) we need to implement Send and Sync.
667        validate: RwLock<F>,
668    }
669
670    // Test double that invokes a function which yields `FetchResponse`,
671    // and returns that as a FIDL response. Useful for testing error
672    // handling.
673    pub(super) struct HttpFidlResponder<F>
674    where
675        F: Fn() -> FetchResponse + Send + Sync,
676    {
677        fetch: F,
678    }
679
680    // Test double that invokes a function which yields a `Vec<u8>`,
681    // and returns the Vec as the HTTP response. Useful for testing
682    // response parsing.
683    pub(super) struct HttpByteResponder {
684        response: Vec<u8>,
685    }
686
687    impl<F> HttpRequestValidator<F>
688    where
689        F: FnMut(HttpRequest) + Send + Sync,
690    {
691        pub(super) fn new(validate: F) -> Self {
692            Self { validate: RwLock::new(validate) }
693        }
694    }
695
696    impl<F> LoaderProxyInterface for HttpRequestValidator<F>
697    where
698        F: FnMut(HttpRequest) + Send + Sync,
699    {
700        type FetchResponseFut = Ready<Result<HttpResponse, FidlError>>;
701
702        fn fetch(&self, request: HttpRequest) -> Self::FetchResponseFut {
703            // Note: the `&mut *` here is due to https://github.com/rust-lang/rust/issues/65489
704            let validate = &mut *self.validate.write();
705            let final_url = request.url.clone();
706            validate(request);
707            ready(Ok(HttpResponse {
708                error: None,
709                body: None,
710                final_url,
711                status_code: Some(HTTP_OK),
712                status_line: None,
713                headers: None,
714                redirect: None,
715                ..Default::default()
716            }))
717        }
718
719        fn start(
720            &self,
721            _request: HttpRequest,
722            _client: ClientEnd<LoaderClientMarker>,
723        ) -> Result<(), FidlError> {
724            panic!("internal error: this fake does not implement `start()`");
725        }
726    }
727
728    impl<F> HttpFidlResponder<F>
729    where
730        F: Fn() -> FetchResponse + Send + Sync,
731    {
732        pub(super) fn new(fetch: F) -> Self {
733            Self { fetch }
734        }
735    }
736
737    impl<F> LoaderProxyInterface for HttpFidlResponder<F>
738    where
739        F: Fn() -> FetchResponse + Send + Sync,
740    {
741        type FetchResponseFut = Ready<FetchResponse>;
742
743        fn fetch(&self, _request: HttpRequest) -> Self::FetchResponseFut {
744            ready((self.fetch)())
745        }
746
747        fn start(
748            &self,
749            _request: HttpRequest,
750            _client: ClientEnd<LoaderClientMarker>,
751        ) -> Result<(), FidlError> {
752            panic!("internal error: this stub does not implement `start()`");
753        }
754    }
755
756    impl HttpByteResponder {
757        pub(super) fn new(response: Vec<u8>) -> Self {
758            Self { response }
759        }
760    }
761
762    impl LoaderProxyInterface for HttpByteResponder {
763        type FetchResponseFut = Ready<FetchResponse>;
764
765        fn fetch(&self, _request: HttpRequest) -> Self::FetchResponseFut {
766            let (local_socket, remote_socket) = zx::Socket::create_stream();
767            local_socket.write(&self.response).expect("internal error");
768            ready(Ok(HttpResponse {
769                error: None,
770                body: Some(remote_socket),
771                final_url: None,
772                status_code: Some(HTTP_OK),
773                status_line: None,
774                headers: None,
775                redirect: None,
776                ..Default::default()
777            }))
778        }
779
780        fn start(
781            &self,
782            _request: HttpRequest,
783            _client: ClientEnd<LoaderClientMarker>,
784        ) -> Result<(), FidlError> {
785            panic!("internal error: this stub does not implement `start()`");
786        }
787    }
788}