1/*
2 * Copyright (C) 2013 Apple Inc. All rights reserved.
3 * Copyright (C) 2013 Google Inc. All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 *     * Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *     * Redistributions in binary form must reproduce the above
12 * copyright notice, this list of conditions and the following disclaimer
13 * in the documentation and/or other materials provided with the
14 * distribution.
15 *     * Neither the name of Google Inc. nor the names of its
16 * contributors may be used to endorse or promote products derived from
17 * this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 */
31
32#include "config.h"
33#include "HTMLSrcsetParser.h"
34
35#include "HTMLParserIdioms.h"
36#include "ParsingUtilities.h"
37
38namespace WebCore {
39
40static inline bool compareByDensity(const ImageCandidate& first, const ImageCandidate& second)
41{
42    return first.density < second.density;
43}
44
45enum DescriptorTokenizerState {
46    Start,
47    InParenthesis,
48    AfterToken,
49};
50
51template<typename CharType>
52static void appendDescriptorAndReset(const CharType*& descriptorStart, const CharType* position, Vector<StringView>& descriptors)
53{
54    if (position > descriptorStart)
55        descriptors.append(StringView(descriptorStart, position - descriptorStart));
56    descriptorStart = nullptr;
57}
58
59// The following is called appendCharacter to match the spec's terminology.
60template<typename CharType>
61static void appendCharacter(const CharType* descriptorStart, const CharType* position)
62{
63    // Since we don't copy the tokens, this just set the point where the descriptor tokens start.
64    if (!descriptorStart)
65        descriptorStart = position;
66}
67
68template<typename CharType>
69static bool isEOF(const CharType* position, const CharType* end)
70{
71    return position >= end;
72}
73
74template<typename CharType>
75static void tokenizeDescriptors(const CharType*& position, const CharType* attributeEnd, Vector<StringView>& descriptors)
76{
77    DescriptorTokenizerState state = Start;
78    const CharType* descriptorsStart = position;
79    const CharType* currentDescriptorStart = descriptorsStart;
80    for (; ; ++position) {
81        switch (state) {
82        case Start:
83            if (isEOF(position, attributeEnd)) {
84                appendDescriptorAndReset(currentDescriptorStart, attributeEnd, descriptors);
85                return;
86            }
87            if (isComma(*position)) {
88                appendDescriptorAndReset(currentDescriptorStart, position, descriptors);
89                ++position;
90                return;
91            }
92            if (isHTMLSpace(*position)) {
93                appendDescriptorAndReset(currentDescriptorStart, position, descriptors);
94                currentDescriptorStart = position + 1;
95                state = AfterToken;
96            } else if (*position == '(') {
97                appendCharacter(currentDescriptorStart, position);
98                state = InParenthesis;
99            } else
100                appendCharacter(currentDescriptorStart, position);
101            break;
102        case InParenthesis:
103            if (isEOF(position, attributeEnd)) {
104                appendDescriptorAndReset(currentDescriptorStart, attributeEnd, descriptors);
105                return;
106            }
107            if (*position == ')') {
108                appendCharacter(currentDescriptorStart, position);
109                state = Start;
110            } else
111                appendCharacter(currentDescriptorStart, position);
112            break;
113        case AfterToken:
114            if (isEOF(position, attributeEnd))
115                return;
116            if (!isHTMLSpace(*position)) {
117                state = Start;
118                currentDescriptorStart = position;
119                --position;
120            }
121            break;
122        }
123    }
124}
125
126static bool parseDescriptors(Vector<StringView>& descriptors, DescriptorParsingResult& result)
127{
128    for (auto& descriptor : descriptors) {
129        if (descriptor.isEmpty())
130            continue;
131        unsigned descriptorCharPosition = descriptor.length() - 1;
132        UChar descriptorChar = descriptor[descriptorCharPosition];
133        descriptor = descriptor.substring(0, descriptorCharPosition);
134        bool isValid = false;
135        if (descriptorChar == 'x') {
136            if (result.hasDensity() || result.hasHeight() || result.hasWidth())
137                return false;
138            float density = descriptor.toFloat(isValid);
139            if (!isValid || density < 0)
140                return false;
141            result.setDensity(density);
142        } else if (descriptorChar == 'w') {
143#if ENABLE(PICTURE_SIZES)
144            if (result.hasDensity() || result.hasWidth())
145                return false;
146            int resourceWidth = descriptor.toInt(isValid);
147            if (!isValid || resourceWidth <= 0)
148                return false;
149            result.setResourceWidth(resourceWidth);
150#else
151            return false;
152#endif
153        } else if (descriptorChar == 'h') {
154#if ENABLE(PICTURE_SIZES)
155            // This is here only for future compat purposes.
156            // The value of the 'h' descriptor is not used.
157            if (result.hasDensity() || result.hasHeight())
158                return false;
159            int resourceHeight = descriptor.toInt(isValid);
160            if (!isValid || resourceHeight <= 0)
161                return false;
162            result.setResourceHeight(resourceHeight);
163#else
164            return false;
165#endif
166        }
167    }
168    return true;
169}
170
171// http://picture.responsiveimages.org/#parse-srcset-attr
172template<typename CharType>
173static void parseImageCandidatesFromSrcsetAttribute(const CharType* attributeStart, unsigned length, Vector<ImageCandidate>& imageCandidates)
174{
175    const CharType* attributeEnd = attributeStart + length;
176
177    for (const CharType* position = attributeStart; position < attributeEnd;) {
178        // 4. Splitting loop: Collect a sequence of characters that are space characters or U+002C COMMA characters.
179        skipWhile<CharType, isHTMLSpaceOrComma<CharType> >(position, attributeEnd);
180        if (position == attributeEnd) {
181            // Contrary to spec language - descriptor parsing happens on each candidate, so when we reach the attributeEnd, we can exit.
182            break;
183        }
184        const CharType* imageURLStart = position;
185        // 6. Collect a sequence of characters that are not space characters, and let that be url.
186
187        skipUntil<CharType, isHTMLSpace<CharType> >(position, attributeEnd);
188        const CharType* imageURLEnd = position;
189
190        DescriptorParsingResult result;
191
192        // 8. If url ends with a U+002C COMMA character (,)
193        if (isComma(*(position - 1))) {
194            // Remove all trailing U+002C COMMA characters from url.
195            imageURLEnd = position - 1;
196            reverseSkipWhile<CharType, isComma>(imageURLEnd, imageURLStart);
197            ++imageURLEnd;
198            // If url is empty, then jump to the step labeled splitting loop.
199            if (imageURLStart == imageURLEnd)
200                continue;
201        } else {
202            // Advancing position here (contrary to spec) to avoid an useless extra state machine step.
203            // Filed a spec bug: https://github.com/ResponsiveImagesCG/picture-element/issues/189
204            ++position;
205            Vector<StringView> descriptorTokens;
206            tokenizeDescriptors(position, attributeEnd, descriptorTokens);
207            // Contrary to spec language - descriptor parsing happens on each candidate.
208            // This is a black-box equivalent, to avoid storing descriptor lists for each candidate.
209            if (!parseDescriptors(descriptorTokens, result))
210                continue;
211        }
212
213        ASSERT(imageURLEnd > imageURLStart);
214        unsigned imageURLLength = imageURLEnd - imageURLStart;
215        imageCandidates.append(ImageCandidate(StringView(imageURLStart, imageURLLength), result, ImageCandidate::SrcsetOrigin));
216        // 11. Return to the step labeled splitting loop.
217    }
218}
219
220static void parseImageCandidatesFromSrcsetAttribute(StringView attribute, Vector<ImageCandidate>& imageCandidates)
221{
222    // FIXME: We should consider replacing the direct pointers in the parsing process with StringView and positions.
223    if (attribute.is8Bit())
224        parseImageCandidatesFromSrcsetAttribute<LChar>(attribute.characters8(), attribute.length(), imageCandidates);
225    else
226        parseImageCandidatesFromSrcsetAttribute<UChar>(attribute.characters16(), attribute.length(), imageCandidates);
227}
228
229static ImageCandidate pickBestImageCandidate(float deviceScaleFactor, Vector<ImageCandidate>& imageCandidates
230#if ENABLE(PICTURE_SIZES)
231    , unsigned sourceSize
232#endif
233    )
234{
235    bool ignoreSrc = false;
236    if (imageCandidates.isEmpty())
237        return ImageCandidate();
238
239    // http://picture.responsiveimages.org/#normalize-source-densities
240    for (auto& candidate : imageCandidates) {
241#if ENABLE(PICTURE_SIZES)
242        if (candidate.resourceWidth > 0) {
243            candidate.density = static_cast<float>(candidate.resourceWidth) / static_cast<float>(sourceSize);
244            ignoreSrc = true;
245        } else
246#endif
247        if (candidate.density < 0)
248            candidate.density = DefaultDensityValue;
249    }
250
251    std::stable_sort(imageCandidates.begin(), imageCandidates.end(), compareByDensity);
252
253    unsigned i;
254    for (i = 0; i < imageCandidates.size() - 1; ++i) {
255        if ((imageCandidates[i].density >= deviceScaleFactor) && (!ignoreSrc || !imageCandidates[i].srcOrigin()))
256            break;
257    }
258
259    if (imageCandidates[i].srcOrigin() && ignoreSrc) {
260        ASSERT(i > 0);
261        --i;
262    }
263    float winningDensity = imageCandidates[i].density;
264
265    unsigned winner = i;
266    // 16. If an entry b in candidates has the same associated ... pixel density as an earlier entry a in candidates,
267    // then remove entry b
268    while ((i > 0) && (imageCandidates[--i].density == winningDensity))
269        winner = i;
270
271    return imageCandidates[winner];
272}
273
274ImageCandidate bestFitSourceForImageAttributes(float deviceScaleFactor, const AtomicString& srcAttribute, const AtomicString& srcsetAttribute
275#if ENABLE(PICTURE_SIZES)
276    , unsigned sourceSize
277#endif
278    )
279{
280    if (srcsetAttribute.isNull()) {
281        if (srcAttribute.isNull())
282            return ImageCandidate();
283        return ImageCandidate(StringView(srcAttribute), DescriptorParsingResult(), ImageCandidate::SrcOrigin);
284    }
285
286    Vector<ImageCandidate> imageCandidates;
287
288    parseImageCandidatesFromSrcsetAttribute(StringView(srcsetAttribute), imageCandidates);
289
290    if (!srcAttribute.isEmpty())
291        imageCandidates.append(ImageCandidate(StringView(srcAttribute), DescriptorParsingResult(), ImageCandidate::SrcOrigin));
292
293    return pickBestImageCandidate(deviceScaleFactor, imageCandidates
294#if ENABLE(PICTURE_SIZES)
295        , sourceSize
296#endif
297        );
298}
299
300} // namespace WebCore
301