Skip to main content

style/servo/
url.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Common handling for the specified value CSS url() values.
6
7use crate::derives::*;
8use crate::parser::{Parse, ParserContext};
9use crate::stylesheets::CorsMode;
10use crate::values::computed::{Context, ToComputedValue};
11use cssparser::{Parser, SourceLocation};
12use servo_arc::Arc;
13use std::fmt::{self, Write};
14use std::ops::Deref;
15use style_traits::{CssWriter, ParseError, ToCss};
16use to_shmem::{SharedMemoryBuilder, ToShmem};
17use url::Url;
18
19/// A CSS url() value for servo.
20///
21/// Servo eagerly resolves SpecifiedUrls, which it can then take advantage of
22/// when computing values. In contrast, Gecko uses a different URL backend, so
23/// eagerly resolving with rust-url would be duplicated work.
24///
25/// However, this approach is still not necessarily optimal: See
26/// <https://bugzilla.mozilla.org/show_bug.cgi?id=1347435#c6>
27#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize, SpecifiedValueInfo)]
28#[css(function = "url")]
29#[repr(C)]
30pub struct CssUrl(#[ignore_malloc_size_of = "Arc"] pub Arc<CssUrlData>);
31
32/// Data shared between CssUrls.
33///
34#[derive(Debug, Deserialize, MallocSizeOf, Serialize, SpecifiedValueInfo)]
35#[repr(C)]
36pub struct CssUrlData {
37    /// The original URI. This might be optional since we may insert computed
38    /// values of images into the cascade directly, and we don't bother to
39    /// convert their serialization.
40    ///
41    /// Refcounted since cloning this should be cheap and data: uris can be
42    /// really large.
43    #[ignore_malloc_size_of = "Arc"]
44    original: Option<Arc<String>>,
45
46    /// The resolved value for the url, if valid.
47    #[ignore_malloc_size_of = "Arc"]
48    resolved: Option<Arc<Url>>,
49}
50
51impl ToShmem for CssUrl {
52    fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> {
53        unimplemented!("If servo wants to share stylesheets across processes, ToShmem for Url must be implemented");
54    }
55}
56
57impl Deref for CssUrl {
58    type Target = CssUrlData;
59    fn deref(&self) -> &Self::Target {
60        &self.0
61    }
62}
63
64impl CssUrl {
65    /// Parse a URL from a string value that is a valid CSS token for a URL,
66    /// enforcing attr()-tainting constraints if applicable.
67    /// https://drafts.csswg.org/css-values-5/#attr-security
68    pub fn parse_from_string<'i>(
69        url: String,
70        url_start: usize,
71        url_end: usize,
72        context: &ParserContext,
73        cors_mode: CorsMode,
74        location: SourceLocation,
75    ) -> Result<Self, ParseError<'i>> {
76        use crate::custom_properties::AttrTaintedRange;
77        use style_traits::StyleParseErrorKind;
78        let range = AttrTaintedRange::new(url_start, url_end);
79        if context.disallow_urls_in_range(&range) {
80            return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
81        }
82        Ok(Self::new_from_string(url, context, cors_mode))
83    }
84
85    /// Create a new CSS URL that is attr()-untainted given a valid CSS token for a URL.
86    /// Be cautious when calling `expect_url()` to not bypass attr()-tainting checks. If
87    /// it's possible attr()'s were substituted into the `url`, DO NOT use this method.
88    /// https://drafts.csswg.org/css-values-5/#attr-security
89    pub fn new_from_untainted_string(
90        url: String,
91        context: &ParserContext,
92        cors_mode: CorsMode,
93    ) -> Self {
94        debug_assert!(context.attr_tainted_regions.is_empty());
95        Self::new_from_string(url, context, cors_mode)
96    }
97
98    /// Try to parse a URL from a string value that is a valid CSS token for a
99    /// URL.
100    ///
101    /// FIXME(emilio): Should honor CorsMode.
102    fn new_from_string(url: String, context: &ParserContext, _cors_mode: CorsMode) -> Self {
103        let serialization = Arc::new(url);
104        // Per https://drafts.csswg.org/css-values-4/#url-empty
105        // If the original url is empty, then the resolved url is considered invalid.
106        let resolved = (!serialization.is_empty())
107            .then(|| context.url_data.0.join(&serialization))
108            .and_then(Result::ok)
109            .map(Arc::new);
110        CssUrl(Arc::new(CssUrlData {
111            original: Some(serialization),
112            resolved: resolved,
113        }))
114    }
115
116    /// Returns true if the URL is definitely invalid. For Servo URLs, we can
117    /// use its |resolved| status.
118    pub fn is_invalid(&self) -> bool {
119        self.resolved.is_none()
120    }
121
122    /// Returns true if this URL looks like a fragment.
123    /// See https://drafts.csswg.org/css-values/#local-urls
124    ///
125    /// Since Servo currently stores resolved URLs, this is hard to implement. We
126    /// either need to change servo to lazily resolve (like Gecko), or note this
127    /// information in the tokenizer.
128    pub fn is_fragment(&self) -> bool {
129        error!("Can't determine whether the url is a fragment.");
130        false
131    }
132
133    /// Returns the resolved url if it was valid.
134    pub fn url(&self) -> Option<&Arc<Url>> {
135        self.resolved.as_ref()
136    }
137
138    /// Return the resolved url as string, or the empty string if it's invalid.
139    ///
140    /// TODO(emilio): Should we return the original one if needed?
141    pub fn as_str(&self) -> &str {
142        match self.resolved {
143            Some(ref url) => url.as_str(),
144            None => "",
145        }
146    }
147
148    /// Creates an already specified url value from an already resolved URL
149    /// for insertion in the cascade.
150    pub fn for_cascade(url: Arc<::url::Url>) -> Self {
151        CssUrl(Arc::new(CssUrlData {
152            original: None,
153            resolved: Some(url),
154        }))
155    }
156
157    /// Gets a new url from a string for unit tests.
158    pub fn new_for_testing(url: &str) -> Self {
159        CssUrl(Arc::new(CssUrlData {
160            original: Some(Arc::new(url.into())),
161            resolved: (!url.is_empty())
162                .then(|| ::url::Url::parse(url))
163                .and_then(Result::ok)
164                .map(Arc::new),
165        }))
166    }
167
168    /// Parses a URL request and records that the corresponding request needs to
169    /// be CORS-enabled.
170    ///
171    /// This is only for shape images and masks in Gecko, thus unimplemented for
172    /// now so somebody notices when trying to do so.
173    pub fn parse_with_cors_mode<'i, 't>(
174        context: &ParserContext,
175        input: &mut Parser<'i, 't>,
176        cors_mode: CorsMode,
177    ) -> Result<Self, ParseError<'i>> {
178        let start = input.position().byte_index();
179        let location = input.current_source_location();
180        let url = input.expect_url()?;
181        let end = input.position().byte_index();
182        Self::parse_from_string(
183            url.as_ref().to_owned(),
184            start,
185            end,
186            context,
187            cors_mode,
188            location,
189        )
190    }
191}
192
193impl Parse for CssUrl {
194    fn parse<'i, 't>(
195        context: &ParserContext,
196        input: &mut Parser<'i, 't>,
197    ) -> Result<Self, ParseError<'i>> {
198        Self::parse_with_cors_mode(context, input, CorsMode::None)
199    }
200}
201
202impl PartialEq for CssUrl {
203    fn eq(&self, other: &Self) -> bool {
204        // TODO(emilio): maybe we care about equality of the specified values if
205        // present? Seems not.
206        self.resolved == other.resolved
207    }
208}
209
210impl Eq for CssUrl {}
211
212impl ToCss for CssUrl {
213    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
214    where
215        W: Write,
216    {
217        let string = match self.0.original {
218            Some(ref original) => &**original,
219            None => match self.resolved {
220                Some(ref url) => url.as_str(),
221                // This can only happen if the url wasn't specified by the
222                // user *and* it's an invalid url that has been transformed
223                // back to specified value via the "uncompute" functionality.
224                None => "about:invalid",
225            },
226        };
227
228        dest.write_str("url(")?;
229        string.to_css(dest)?;
230        dest.write_char(')')
231    }
232}
233
234/// A specified url() value for servo.
235pub type SpecifiedUrl = CssUrl;
236
237impl ToComputedValue for SpecifiedUrl {
238    type ComputedValue = ComputedUrl;
239
240    // If we can't resolve the URL from the specified one, we fall back to the original
241    // but still return it as a ComputedUrl::Invalid
242    fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
243        match self.resolved {
244            Some(ref url) => ComputedUrl::Valid(url.clone()),
245            None => match self.original {
246                Some(ref url) => ComputedUrl::Invalid(url.clone()),
247                None => {
248                    unreachable!("Found specified url with neither resolved or original URI!");
249                },
250            },
251        }
252    }
253
254    fn from_computed_value(computed: &ComputedUrl) -> Self {
255        let data = match *computed {
256            ComputedUrl::Valid(ref url) => CssUrlData {
257                original: None,
258                resolved: Some(url.clone()),
259            },
260            ComputedUrl::Invalid(ref url) => CssUrlData {
261                original: Some(url.clone()),
262                resolved: None,
263            },
264        };
265        CssUrl(Arc::new(data))
266    }
267}
268
269/// The computed value of a CSS `url()`, resolved relative to the stylesheet URL.
270#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
271pub enum ComputedUrl {
272    /// The `url()` was invalid or it wasn't specified by the user.
273    Invalid(#[ignore_malloc_size_of = "Arc"] Arc<String>),
274    /// The resolved `url()` relative to the stylesheet URL.
275    Valid(#[ignore_malloc_size_of = "Arc"] Arc<Url>),
276}
277
278impl ComputedUrl {
279    /// Returns the resolved url if it was valid.
280    pub fn url(&self) -> Option<&Arc<Url>> {
281        match *self {
282            ComputedUrl::Valid(ref url) => Some(url),
283            _ => None,
284        }
285    }
286}
287
288impl ToCss for ComputedUrl {
289    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
290    where
291        W: Write,
292    {
293        let string = match *self {
294            ComputedUrl::Valid(ref url) => url.as_str(),
295            ComputedUrl::Invalid(ref invalid_string) => invalid_string,
296        };
297
298        dest.write_str("url(")?;
299        string.to_css(dest)?;
300        dest.write_char(')')
301    }
302}