tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

mod.rs (15968B)


      1 mod fetcher;
      2 pub use fetcher::FileFetcher;
      3 pub use fluent_fallback::types::{ResourceId, ToResourceId};
      4 
      5 use crate::env::ErrorReporter;
      6 use crate::errors::L10nRegistryError;
      7 use crate::fluent::FluentResource;
      8 
      9 use std::{
     10    borrow::Borrow,
     11    cell::RefCell,
     12    fmt,
     13    hash::{Hash, Hasher},
     14    pin::Pin,
     15    rc::Rc,
     16    task::Poll,
     17 };
     18 
     19 use futures::{future::Shared, Future, FutureExt};
     20 use rustc_hash::FxHashMap;
     21 use unic_langid::LanguageIdentifier;
     22 
     23 pub type RcResource = Rc<FluentResource>;
     24 
     25 /// An option type whose None variant is either optional or required.
     26 ///
     27 /// This behaves similarly to the standard-library [`Option`] type
     28 /// except that there are two [`None`]-like variants:
     29 /// [`ResourceOption::MissingOptional`] and [`ResourceOption::MissingRequired`].
     30 #[derive(Clone, Debug)]
     31 pub enum ResourceOption {
     32    /// An available resource.
     33    Some(RcResource),
     34    /// A missing optional resource.
     35    MissingOptional,
     36    /// A missing required resource.
     37    MissingRequired,
     38 }
     39 
     40 impl ResourceOption {
     41    /// Creates a resource option that is either [`ResourceOption::MissingRequired`]
     42    /// or [`ResourceOption::MissingOptional`] based on whether the given [`ResourceId`]
     43    /// is required or optional.
     44    pub fn missing_resource(resource_id: &ResourceId) -> Self {
     45        if resource_id.is_required() {
     46            Self::MissingRequired
     47        } else {
     48            Self::MissingOptional
     49        }
     50    }
     51 
     52    /// Returns [`true`] if this option contains a recource, otherwise [`false`].
     53    pub fn is_some(&self) -> bool {
     54        matches!(self, Self::Some(_))
     55    }
     56 
     57    /// Resource [`true`] if this option is missing a resource of any type, otherwise [`false`].
     58    pub fn is_none(&self) -> bool {
     59        matches!(self, Self::MissingOptional | Self::MissingRequired)
     60    }
     61 
     62    /// Returns [`true`] if this option is missing a required resource, otherwise [`false`].
     63    pub fn is_required_and_missing(&self) -> bool {
     64        matches!(self, Self::MissingRequired)
     65    }
     66 }
     67 
     68 impl From<ResourceOption> for Option<RcResource> {
     69    fn from(other: ResourceOption) -> Self {
     70        match other {
     71            ResourceOption::Some(id) => Some(id),
     72            _ => None,
     73        }
     74    }
     75 }
     76 
     77 pub type ResourceFuture = Shared<Pin<Box<dyn Future<Output = ResourceOption>>>>;
     78 
     79 #[derive(Debug, Clone)]
     80 pub enum ResourceStatus {
     81    /// The resource is missing.  Don't bother trying to fetch.
     82    MissingRequired,
     83    MissingOptional,
     84    /// The resource is loading and future will deliver the result.
     85    Loading(ResourceFuture),
     86    /// The resource is loaded and parsed.
     87    Loaded(RcResource),
     88 }
     89 
     90 impl From<ResourceOption> for ResourceStatus {
     91    fn from(input: ResourceOption) -> Self {
     92        match input {
     93            ResourceOption::Some(res) => Self::Loaded(res),
     94            ResourceOption::MissingOptional => Self::MissingOptional,
     95            ResourceOption::MissingRequired => Self::MissingRequired,
     96        }
     97    }
     98 }
     99 
    100 impl Future for ResourceStatus {
    101    type Output = ResourceOption;
    102 
    103    fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
    104        use ResourceStatus::*;
    105 
    106        let this = &mut *self;
    107 
    108        match this {
    109            MissingRequired => ResourceOption::MissingRequired.into(),
    110            MissingOptional => ResourceOption::MissingOptional.into(),
    111            Loaded(res) => ResourceOption::Some(res.clone()).into(),
    112            Loading(res) => Pin::new(res).poll(cx),
    113        }
    114    }
    115 }
    116 
    117 /// `FileSource` provides a generic fetching and caching of fluent resources.
    118 /// The user of `FileSource` provides a [`FileFetcher`](trait.FileFetcher.html)
    119 /// implementation and `FileSource` takes care of the rest.
    120 #[derive(Clone)]
    121 pub struct FileSource {
    122    /// Name of the FileSource, e.g. "browser"
    123    pub name: String,
    124    /// Pre-formatted path for the FileSource, e.g. "/browser/data/locale/{locale}/"
    125    pub pre_path: String,
    126    /// Metasource name for the FileSource, e.g. "app", "langpack"
    127    /// Only sources from the same metasource are passed into the solver.
    128    pub metasource: String,
    129    /// The locales for which data is present in the FileSource, e.g. ["en-US", "pl"]
    130    locales: Vec<LanguageIdentifier>,
    131    shared: Rc<Inner>,
    132    index: Option<Vec<String>>,
    133    pub options: FileSourceOptions,
    134 }
    135 
    136 struct Inner {
    137    fetcher: Box<dyn FileFetcher>,
    138    error_reporter: Option<RefCell<Box<dyn ErrorReporter>>>,
    139    entries: RefCell<FxHashMap<String, ResourceStatus>>,
    140 }
    141 
    142 impl fmt::Display for FileSource {
    143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    144        write!(f, "{}", self.name)
    145    }
    146 }
    147 
    148 impl PartialEq<FileSource> for FileSource {
    149    fn eq(&self, other: &Self) -> bool {
    150        self.name == other.name && self.metasource == other.metasource
    151    }
    152 }
    153 
    154 impl Eq for FileSource {}
    155 
    156 impl Hash for FileSource {
    157    fn hash<H: Hasher>(&self, state: &mut H) {
    158        self.name.hash(state)
    159    }
    160 }
    161 
    162 #[derive(PartialEq, Clone, Debug, Default)]
    163 pub struct FileSourceOptions {
    164    pub allow_override: bool,
    165 }
    166 
    167 impl FileSource {
    168    /// Create a `FileSource` using the provided [`FileFetcher`](../trait.FileFetcher.html).
    169    pub fn new(
    170        name: String,
    171        metasource: Option<String>,
    172        locales: Vec<LanguageIdentifier>,
    173        pre_path: String,
    174        options: FileSourceOptions,
    175        fetcher: impl FileFetcher + 'static,
    176    ) -> Self {
    177        FileSource {
    178            name,
    179            metasource: metasource.unwrap_or_default(),
    180            pre_path,
    181            locales,
    182            index: None,
    183            shared: Rc::new(Inner {
    184                entries: RefCell::new(FxHashMap::default()),
    185                fetcher: Box::new(fetcher),
    186                error_reporter: None,
    187            }),
    188            options,
    189        }
    190    }
    191 
    192    pub fn new_with_index(
    193        name: String,
    194        metasource: Option<String>,
    195        locales: Vec<LanguageIdentifier>,
    196        pre_path: String,
    197        options: FileSourceOptions,
    198        fetcher: impl FileFetcher + 'static,
    199        index: Vec<String>,
    200    ) -> Self {
    201        FileSource {
    202            name,
    203            metasource: metasource.unwrap_or_default(),
    204            pre_path,
    205            locales,
    206            index: Some(index),
    207            shared: Rc::new(Inner {
    208                entries: RefCell::new(FxHashMap::default()),
    209                fetcher: Box::new(fetcher),
    210                error_reporter: None,
    211            }),
    212            options,
    213        }
    214    }
    215 
    216    pub fn set_reporter(&mut self, reporter: impl ErrorReporter + 'static) {
    217        let shared = Rc::get_mut(&mut self.shared).unwrap();
    218        shared.error_reporter = Some(RefCell::new(Box::new(reporter)));
    219    }
    220 }
    221 
    222 fn calculate_pos_in_source(source: &str, idx: usize) -> (usize, usize) {
    223    let mut ptr = 0;
    224    let mut result = (1, 1);
    225    for line in source.lines() {
    226        let bytes = line.as_bytes().len();
    227        if ptr + bytes < idx {
    228            ptr += bytes + 1;
    229            result.0 += 1;
    230        } else {
    231            result.1 = idx - ptr + 1;
    232            break;
    233        }
    234    }
    235    result
    236 }
    237 
    238 impl FileSource {
    239    fn get_path(&self, locale: &LanguageIdentifier, resource_id: &ResourceId) -> String {
    240        format!(
    241            "{}{}",
    242            self.pre_path.replace("{locale}", &locale.to_string()),
    243            resource_id.value,
    244        )
    245    }
    246 
    247    fn fetch_sync(&self, resource_id: &ResourceId) -> ResourceOption {
    248        self.shared
    249            .fetcher
    250            .fetch_sync(resource_id)
    251            .ok()
    252            .map(|source| match FluentResource::try_new(source) {
    253                Ok(res) => ResourceOption::Some(Rc::new(res)),
    254                Err((res, errors)) => {
    255                    if let Some(reporter) = &self.shared.error_reporter {
    256                        reporter.borrow().report_errors(
    257                            errors
    258                                .into_iter()
    259                                .map(|e| L10nRegistryError::FluentError {
    260                                    resource_id: resource_id.clone(),
    261                                    loc: Some(calculate_pos_in_source(res.source(), e.pos.start)),
    262                                    error: e.into(),
    263                                })
    264                                .collect(),
    265                        );
    266                    }
    267                    ResourceOption::Some(Rc::new(res))
    268                }
    269            })
    270            .unwrap_or_else(|| ResourceOption::missing_resource(resource_id))
    271    }
    272 
    273    /// Attempt to synchronously fetch resource for the combination of `locale`
    274    /// and `path`. Returns `Some(ResourceResult)` if the resource is available,
    275    /// else `None`.
    276    pub fn fetch_file_sync(
    277        &self,
    278        locale: &LanguageIdentifier,
    279        resource_id: &ResourceId,
    280        overload: bool,
    281    ) -> ResourceOption {
    282        use ResourceStatus::*;
    283 
    284        if self.has_file(locale, resource_id) == Some(false) {
    285            return ResourceOption::missing_resource(resource_id);
    286        }
    287 
    288        let full_path_id = self
    289            .get_path(locale, resource_id)
    290            .to_resource_id(resource_id.resource_type);
    291 
    292        let res = self.shared.lookup_resource(full_path_id.clone(), || {
    293            self.fetch_sync(&full_path_id).into()
    294        });
    295 
    296        match res {
    297            MissingRequired => ResourceOption::MissingRequired,
    298            MissingOptional => ResourceOption::MissingOptional,
    299            Loaded(res) => ResourceOption::Some(res),
    300            Loading(..) if overload => {
    301                // A sync load has been requested for the same resource that has
    302                // a pending async load in progress. How do we handle this?
    303                //
    304                // Ideally, we would sync load and resolve all the pending
    305                // futures with the result. With the current Futures and
    306                // combinators, it's unclear how to proceed. One potential
    307                // solution is to store a oneshot::Sender and
    308                // Shared<oneshot::Receiver>. When the async loading future
    309                // resolves it would check that the state is still `Loading`,
    310                // and if so, send the result. The sync load would do the same
    311                // send on the oneshot::Sender.
    312                //
    313                // For now, we warn and return the resource, paying the cost of
    314                // duplication of the resource.
    315                self.fetch_sync(&full_path_id)
    316            }
    317            Loading(..) => {
    318                panic!("[l10nregistry] Attempting to synchronously load file {} while it's being loaded asynchronously.", &full_path_id.value);
    319            }
    320        }
    321    }
    322 
    323    /// Attempt to fetch resource for the combination of `locale` and `path`.
    324    /// Returns [`ResourceStatus`](enum.ResourceStatus.html) which is
    325    /// a `Future` that can be polled.
    326    pub fn fetch_file(
    327        &self,
    328        locale: &LanguageIdentifier,
    329        resource_id: &ResourceId,
    330    ) -> ResourceStatus {
    331        use ResourceStatus::*;
    332 
    333        if self.has_file(locale, resource_id) == Some(false) {
    334            return ResourceOption::missing_resource(resource_id).into();
    335        }
    336 
    337        let full_path_id = self
    338            .get_path(locale, resource_id)
    339            .to_resource_id(resource_id.resource_type);
    340 
    341        self.shared.lookup_resource(full_path_id.clone(), || {
    342            let shared = self.shared.clone();
    343            Loading(read_resource(full_path_id, shared).boxed_local().shared())
    344        })
    345    }
    346 
    347    /// Determine if the `FileSource` has a loaded resource for the combination
    348    /// of `locale` and `path`. Returns `Some(true)` if the file is loaded, else
    349    /// `Some(false)`. `None` is returned if there is an outstanding async fetch
    350    /// pending and the status is yet to be determined.
    351    pub fn has_file<L: Borrow<LanguageIdentifier>>(
    352        &self,
    353        locale: L,
    354        path: &ResourceId,
    355    ) -> Option<bool> {
    356        let locale = locale.borrow();
    357        if !self.locales.contains(locale) {
    358            Some(false)
    359        } else {
    360            let full_path = self.get_path(locale, path);
    361            if let Some(index) = &self.index {
    362                return Some(index.iter().any(|p| p == &full_path));
    363            }
    364            self.shared.has_file(&full_path)
    365        }
    366    }
    367 
    368    pub fn locales(&self) -> &[LanguageIdentifier] {
    369        &self.locales
    370    }
    371 
    372    pub fn get_index(&self) -> Option<&Vec<String>> {
    373        self.index.as_ref()
    374    }
    375 }
    376 
    377 impl std::fmt::Debug for FileSource {
    378    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
    379        if let Some(index) = &self.index {
    380            f.debug_struct("FileSource")
    381                .field("name", &self.name)
    382                .field("metasource", &self.metasource)
    383                .field("locales", &self.locales)
    384                .field("pre_path", &self.pre_path)
    385                .field("index", index)
    386                .finish()
    387        } else {
    388            f.debug_struct("FileSource")
    389                .field("name", &self.name)
    390                .field("metasource", &self.metasource)
    391                .field("locales", &self.locales)
    392                .field("pre_path", &self.pre_path)
    393                .finish()
    394        }
    395    }
    396 }
    397 
    398 impl Inner {
    399    fn lookup_resource<F>(&self, resource_id: ResourceId, f: F) -> ResourceStatus
    400    where
    401        F: FnOnce() -> ResourceStatus,
    402    {
    403        let mut lock = self.entries.borrow_mut();
    404        lock.entry(resource_id.value).or_insert_with(f).clone()
    405    }
    406 
    407    fn update_resource(&self, resource_id: ResourceId, resource: ResourceOption) -> ResourceOption {
    408        let mut lock = self.entries.borrow_mut();
    409        let entry = lock.get_mut(&resource_id.value);
    410        match entry {
    411            Some(entry) => *entry = resource.clone().into(),
    412            _ => panic!("Expected "),
    413        }
    414        resource
    415    }
    416 
    417    pub fn has_file(&self, full_path: &str) -> Option<bool> {
    418        match self.entries.borrow().get(full_path) {
    419            Some(ResourceStatus::MissingRequired) => Some(false),
    420            Some(ResourceStatus::MissingOptional) => Some(false),
    421            Some(ResourceStatus::Loaded(_)) => Some(true),
    422            Some(ResourceStatus::Loading(_)) | None => None,
    423        }
    424    }
    425 }
    426 
    427 async fn read_resource(resource_id: ResourceId, shared: Rc<Inner>) -> ResourceOption {
    428    let resource = shared
    429        .fetcher
    430        .fetch(&resource_id)
    431        .await
    432        .ok()
    433        .map(|source| match FluentResource::try_new(source) {
    434            Ok(res) => ResourceOption::Some(Rc::new(res)),
    435            Err((res, errors)) => {
    436                if let Some(reporter) = &shared.error_reporter {
    437                    reporter.borrow().report_errors(
    438                        errors
    439                            .into_iter()
    440                            .map(|e| L10nRegistryError::FluentError {
    441                                resource_id: resource_id.clone(),
    442                                loc: Some(calculate_pos_in_source(res.source(), e.pos.start)),
    443                                error: e.into(),
    444                            })
    445                            .collect(),
    446                    );
    447                }
    448                ResourceOption::Some(Rc::new(res))
    449            }
    450        })
    451        .unwrap_or_else(|| ResourceOption::missing_resource(&resource_id));
    452    // insert the resource into the cache
    453    shared.update_resource(resource_id, resource)
    454 }
    455 
    456 #[cfg(test)]
    457 mod tests {
    458    use super::*;
    459 
    460    #[test]
    461    fn calculate_source_pos() {
    462        let source = r#"
    463 key = Value
    464 
    465 key2 = Value 2
    466 "#
    467        .trim();
    468        let result = calculate_pos_in_source(source, 0);
    469        assert_eq!(result, (1, 1));
    470 
    471        let result = calculate_pos_in_source(source, 1);
    472        assert_eq!(result, (1, 2));
    473 
    474        let result = calculate_pos_in_source(source, 12);
    475        assert_eq!(result, (2, 1));
    476 
    477        let result = calculate_pos_in_source(source, 13);
    478        assert_eq!(result, (3, 1));
    479    }
    480 }