Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/intl/l10n/rust/l10nregistry-rs/src/source/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 15 kB image not shown  

Quelle  mod.rs   Sprache: unbekannt

 
mod fetcher;
pub use fetcher::FileFetcher;
pub use fluent_fallback::types::{ResourceId, ToResourceId};

use crate::env::ErrorReporter;
use crate::errors::L10nRegistryError;
use crate::fluent::FluentResource;

use std::{
    borrow::Borrow,
    cell::RefCell,
    fmt,
    hash::{Hash, Hasher},
    pin::Pin,
    rc::Rc,
    task::Poll,
};

use futures::{future::Shared, Future, FutureExt};
use rustc_hash::FxHashMap;
use unic_langid::LanguageIdentifier;

pub type RcResource = Rc<FluentResource>;

/// An option type whose None variant is either optional or required.
///
/// This behaves similarly to the standard-library [`Option`] type
/// except that there are two [`None`]-like variants:
/// [`ResourceOption::MissingOptional`] and [`ResourceOption::MissingRequired`].
#[derive(Clone, Debug)]
pub enum ResourceOption {
    /// An available resource.
    Some(RcResource),
    /// A missing optional resource.
    MissingOptional,
    /// A missing required resource.
    MissingRequired,
}

impl ResourceOption {
    /// Creates a resource option that is either [`ResourceOption::MissingRequired`]
    /// or [`ResourceOption::MissingOptional`] based on whether the given [`ResourceId`]
    /// is required or optional.
    pub fn missing_resource(resource_id: &ResourceId) -> Self {
        if resource_id.is_required() {
            Self::MissingRequired
        } else {
            Self::MissingOptional
        }
    }

    /// Returns [`true`] if this option contains a recource, otherwise [`false`].
    pub fn is_some(&self) -> bool {
        matches!(self, Self::Some(_))
    }

    /// Resource [`true`] if this option is missing a resource of any type, otherwise [`false`].
    pub fn is_none(&self) -> bool {
        matches!(self, Self::MissingOptional | Self::MissingRequired)
    }

    /// Returns [`true`] if this option is missing a required resource, otherwise [`false`].
    pub fn is_required_and_missing(&self) -> bool {
        matches!(self, Self::MissingRequired)
    }
}

impl From<ResourceOption> for Option<RcResource> {
    fn from(other: ResourceOption) -> Self {
        match other {
            ResourceOption::Some(id) => Some(id),
            _ => None,
        }
    }
}

pub type ResourceFuture = Shared<Pin<Box<dyn Future<Output = ResourceOption>>>>;

#[derive(Debug, Clone)]
pub enum ResourceStatus {
    /// The resource is missing.  Don't bother trying to fetch.
    MissingRequired,
    MissingOptional,
    /// The resource is loading and future will deliver the result.
    Loading(ResourceFuture),
    /// The resource is loaded and parsed.
    Loaded(RcResource),
}

impl From<ResourceOption> for ResourceStatus {
    fn from(input: ResourceOption) -> Self {
        match input {
            ResourceOption::Some(res) => Self::Loaded(res),
            ResourceOption::MissingOptional => Self::MissingOptional,
            ResourceOption::MissingRequired => Self::MissingRequired,
        }
    }
}

impl Future for ResourceStatus {
    type Output = ResourceOption;

    fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
        use ResourceStatus::*;

        let this = &mut *self;

        match this {
            MissingRequired => ResourceOption::MissingRequired.into(),
            MissingOptional => ResourceOption::MissingOptional.into(),
            Loaded(res) => ResourceOption::Some(res.clone()).into(),
            Loading(res) => Pin::new(res).poll(cx),
        }
    }
}

/// `FileSource` provides a generic fetching and caching of fluent resources.
/// The user of `FileSource` provides a [`FileFetcher`](trait.FileFetcher.html)
/// implementation and `FileSource` takes care of the rest.
#[derive(Clone)]
pub struct FileSource {
    /// Name of the FileSource, e.g. "browser"
    pub name: String,
    /// Pre-formatted path for the FileSource, e.g. "/browser/data/locale/{locale}/"
    pub pre_path: String,
    /// Metasource name for the FileSource, e.g. "app", "langpack"
    /// Only sources from the same metasource are passed into the solver.
    pub metasource: String,
    /// The locales for which data is present in the FileSource, e.g. ["en-US", "pl"]
    locales: Vec<LanguageIdentifier>,
    shared: Rc<Inner>,
    index: Option<Vec<String>>,
    pub options: FileSourceOptions,
}

struct Inner {
    fetcher: Box<dyn FileFetcher>,
    error_reporter: Option<RefCell<Box<dyn ErrorReporter>>>,
    entries: RefCell<FxHashMap<String, ResourceStatus>>,
}

impl fmt::Display for FileSource {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.name)
    }
}

impl PartialEq<FileSource> for FileSource {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name && self.metasource == other.metasource
    }
}

impl Eq for FileSource {}

impl Hash for FileSource {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.name.hash(state)
    }
}

#[derive(PartialEq, Clone, Debug, Default)]
pub struct FileSourceOptions {
    pub allow_override: bool,
}

impl FileSource {
    /// Create a `FileSource` using the provided [`FileFetcher`](../trait.FileFetcher.html).
    pub fn new(
        name: String,
        metasource: Option<String>,
        locales: Vec<LanguageIdentifier>,
        pre_path: String,
        options: FileSourceOptions,
        fetcher: impl FileFetcher + 'static,
    ) -> Self {
        FileSource {
            name,
            metasource: metasource.unwrap_or_default(),
            pre_path,
            locales,
            index: None,
            shared: Rc::new(Inner {
                entries: RefCell::new(FxHashMap::default()),
                fetcher: Box::new(fetcher),
                error_reporter: None,
            }),
            options,
        }
    }

    pub fn new_with_index(
        name: String,
        metasource: Option<String>,
        locales: Vec<LanguageIdentifier>,
        pre_path: String,
        options: FileSourceOptions,
        fetcher: impl FileFetcher + 'static,
        index: Vec<String>,
    ) -> Self {
        FileSource {
            name,
            metasource: metasource.unwrap_or_default(),
            pre_path,
            locales,
            index: Some(index),
            shared: Rc::new(Inner {
                entries: RefCell::new(FxHashMap::default()),
                fetcher: Box::new(fetcher),
                error_reporter: None,
            }),
            options,
        }
    }

    pub fn set_reporter(&mut self, reporter: impl ErrorReporter + 'static) {
        let shared = Rc::get_mut(&mut self.shared).unwrap();
        shared.error_reporter = Some(RefCell::new(Box::new(reporter)));
    }
}

fn calculate_pos_in_source(source: &str, idx: usize) -> (usize, usize) {
    let mut ptr = 0;
    let mut result = (1, 1);
    for line in source.lines() {
        let bytes = line.as_bytes().len();
        if ptr + bytes < idx {
            ptr += bytes + 1;
            result.0 += 1;
        } else {
            result.1 = idx - ptr + 1;
            break;
        }
    }
    result
}

impl FileSource {
    fn get_path(&self, locale: &LanguageIdentifier, resource_id: &ResourceId) -> String {
        format!(
            "{}{}",
            self.pre_path.replace("{locale}", &locale.to_string()),
            resource_id.value,
        )
    }

    fn fetch_sync(&self, resource_id: &ResourceId) -> ResourceOption {
        self.shared
            .fetcher
            .fetch_sync(resource_id)
            .ok()
            .map(|source| match FluentResource::try_new(source) {
                Ok(res) => ResourceOption::Some(Rc::new(res)),
                Err((res, errors)) => {
                    if let Some(reporter) = &self.shared.error_reporter {
                        reporter.borrow().report_errors(
                            errors
                                .into_iter()
                                .map(|e| L10nRegistryError::FluentError {
                                    resource_id: resource_id.clone(),
                                    loc: Some(calculate_pos_in_source(res.source(), e.pos.start)),
                                    error: e.into(),
                                })
                                .collect(),
                        );
                    }
                    ResourceOption::Some(Rc::new(res))
                }
            })
            .unwrap_or_else(|| ResourceOption::missing_resource(resource_id))
    }

    /// Attempt to synchronously fetch resource for the combination of `locale`
    /// and `path`. Returns `Some(ResourceResult)` if the resource is available,
    /// else `None`.
    pub fn fetch_file_sync(
        &self,
        locale: &LanguageIdentifier,
        resource_id: &ResourceId,
        overload: bool,
    ) -> ResourceOption {
        use ResourceStatus::*;

        if self.has_file(locale, resource_id) == Some(false) {
            return ResourceOption::missing_resource(resource_id);
        }

        let full_path_id = self
            .get_path(locale, resource_id)
            .to_resource_id(resource_id.resource_type);

        let res = self.shared.lookup_resource(full_path_id.clone(), || {
            self.fetch_sync(&full_path_id).into()
        });

        match res {
            MissingRequired => ResourceOption::MissingRequired,
            MissingOptional => ResourceOption::MissingOptional,
            Loaded(res) => ResourceOption::Some(res),
            Loading(..) if overload => {
                // A sync load has been requested for the same resource that has
                // a pending async load in progress. How do we handle this?
                //
                // Ideally, we would sync load and resolve all the pending
                // futures with the result. With the current Futures and
                // combinators, it's unclear how to proceed. One potential
                // solution is to store a oneshot::Sender and
                // Shared<oneshot::Receiver>. When the async loading future
                // resolves it would check that the state is still `Loading`,
                // and if so, send the result. The sync load would do the same
                // send on the oneshot::Sender.
                //
                // For now, we warn and return the resource, paying the cost of
                // duplication of the resource.
                self.fetch_sync(&full_path_id)
            }
            Loading(..) => {
                panic!("[l10nregistry] Attempting to synchronously load file {} while it's being loaded asynchronously.", &full_path_id.value);
            }
        }
    }

    /// Attempt to fetch resource for the combination of `locale` and `path`.
    /// Returns [`ResourceStatus`](enum.ResourceStatus.html) which is
    /// a `Future` that can be polled.
    pub fn fetch_file(
        &self,
        locale: &LanguageIdentifier,
        resource_id: &ResourceId,
    ) -> ResourceStatus {
        use ResourceStatus::*;

        if self.has_file(locale, resource_id) == Some(false) {
            return ResourceOption::missing_resource(resource_id).into();
        }

        let full_path_id = self
            .get_path(locale, resource_id)
            .to_resource_id(resource_id.resource_type);

        self.shared.lookup_resource(full_path_id.clone(), || {
            let shared = self.shared.clone();
            Loading(read_resource(full_path_id, shared).boxed_local().shared())
        })
    }

    /// Determine if the `FileSource` has a loaded resource for the combination
    /// of `locale` and `path`. Returns `Some(true)` if the file is loaded, else
    /// `Some(false)`. `None` is returned if there is an outstanding async fetch
    /// pending and the status is yet to be determined.
    pub fn has_file<L: Borrow<LanguageIdentifier>>(
        &self,
        locale: L,
        path: &ResourceId,
    ) -> Option<bool> {
        let locale = locale.borrow();
        if !self.locales.contains(locale) {
            Some(false)
        } else {
            let full_path = self.get_path(locale, path);
            if let Some(index) = &self.index {
                return Some(index.iter().any(|p| p == &full_path));
            }
            self.shared.has_file(&full_path)
        }
    }

    pub fn locales(&self) -> &[LanguageIdentifier] {
        &self.locales
    }

    pub fn get_index(&self) -> Option<&Vec<String>> {
        self.index.as_ref()
    }
}

impl std::fmt::Debug for FileSource {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
        if let Some(index) = &self.index {
            f.debug_struct("FileSource")
                .field("name", &self.name)
                .field("metasource", &self.metasource)
                .field("locales", &self.locales)
                .field("pre_path", &self.pre_path)
                .field("index", index)
                .finish()
        } else {
            f.debug_struct("FileSource")
                .field("name", &self.name)
                .field("metasource", &self.metasource)
                .field("locales", &self.locales)
                .field("pre_path", &self.pre_path)
                .finish()
        }
    }
}

impl Inner {
    fn lookup_resource<F>(&self, resource_id: ResourceId, f: F) -> ResourceStatus
    where
        F: FnOnce() -> ResourceStatus,
    {
        let mut lock = self.entries.borrow_mut();
        lock.entry(resource_id.value).or_insert_with(f).clone()
    }

    fn update_resource(&self, resource_id: ResourceId, resource: ResourceOption) -> ResourceOption {
        let mut lock = self.entries.borrow_mut();
        let entry = lock.get_mut(&resource_id.value);
        match entry {
            Some(entry) => *entry = resource.clone().into(),
            _ => panic!("Expected "),
        }
        resource
    }

    pub fn has_file(&self, full_path: &str) -> Option<bool> {
        match self.entries.borrow().get(full_path) {
            Some(ResourceStatus::MissingRequired) => Some(false),
            Some(ResourceStatus::MissingOptional) => Some(false),
            Some(ResourceStatus::Loaded(_)) => Some(true),
            Some(ResourceStatus::Loading(_)) | None => None,
        }
    }
}

async fn read_resource(resource_id: ResourceId, shared: Rc<Inner>) -> ResourceOption {
    let resource = shared
        .fetcher
        .fetch(&resource_id)
        .await
        .ok()
        .map(|source| match FluentResource::try_new(source) {
            Ok(res) => ResourceOption::Some(Rc::new(res)),
            Err((res, errors)) => {
                if let Some(reporter) = &shared.error_reporter {
                    reporter.borrow().report_errors(
                        errors
                            .into_iter()
                            .map(|e| L10nRegistryError::FluentError {
                                resource_id: resource_id.clone(),
                                loc: Some(calculate_pos_in_source(res.source(), e.pos.start)),
                                error: e.into(),
                            })
                            .collect(),
                    );
                }
                ResourceOption::Some(Rc::new(res))
            }
        })
        .unwrap_or_else(|| ResourceOption::missing_resource(&resource_id));
    // insert the resource into the cache
    shared.update_resource(resource_id, resource)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn calculate_source_pos() {
        let source = r#"
key = Value

key2 = Value 2
"#
        .trim();
        let result = calculate_pos_in_source(source, 0);
        assert_eq!(result, (1, 1));

        let result = calculate_pos_in_source(source, 1);
        assert_eq!(result, (1, 2));

        let result = calculate_pos_in_source(source, 12);
        assert_eq!(result, (2, 1));

        let result = calculate_pos_in_source(source, 13);
        assert_eq!(result, (3, 1));
    }
}

[ Dauer der Verarbeitung: 0.3 Sekunden  (vorverarbeitet)  ]