1/*
2 * Copyright (C) 2011, 2012 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27
28#if ENABLE(VIDEO)
29
30#include "MediaFragmentURIParser.h"
31
32#include "HTMLElement.h"
33#include "MediaPlayer.h"
34#include "ProcessingInstruction.h"
35#include "SegmentedString.h"
36#include "Text.h"
37#include <wtf/text/CString.h>
38#include <wtf/text/StringBuilder.h>
39#include <wtf/text/WTFString.h>
40
41namespace WebCore {
42
43const int secondsPerHour = 3600;
44const int secondsPerMinute = 60;
45const unsigned nptIdentiferLength = 4; // "npt:"
46
47static String collectDigits(const LChar* input, unsigned length, unsigned& position)
48{
49    StringBuilder digits;
50
51    // http://www.ietf.org/rfc/rfc2326.txt
52    // DIGIT ; any positive number
53    while (position < length && isASCIIDigit(input[position]))
54        digits.append(input[position++]);
55    return digits.toString();
56}
57
58static String collectFraction(const LChar* input, unsigned length, unsigned& position)
59{
60    StringBuilder digits;
61
62    // http://www.ietf.org/rfc/rfc2326.txt
63    // [ "." *DIGIT ]
64    if (input[position] != '.')
65        return String();
66
67    digits.append(input[position++]);
68    while (position < length && isASCIIDigit(input[position]))
69        digits.append(input[position++]);
70    return digits.toString();
71}
72
73double MediaFragmentURIParser::invalidTimeValue()
74{
75    return MediaPlayer::invalidTime();
76}
77
78MediaFragmentURIParser::MediaFragmentURIParser(const KURL& url)
79    : m_url(url)
80    , m_timeFormat(None)
81    , m_startTime(MediaPlayer::invalidTime())
82    , m_endTime(MediaPlayer::invalidTime())
83{
84}
85
86double MediaFragmentURIParser::startTime()
87{
88    if (!m_url.isValid())
89        return MediaPlayer::invalidTime();
90    if (m_timeFormat == None)
91        parseTimeFragment();
92    return m_startTime;
93}
94
95double MediaFragmentURIParser::endTime()
96{
97    if (!m_url.isValid())
98        return MediaPlayer::invalidTime();
99    if (m_timeFormat == None)
100        parseTimeFragment();
101    return m_endTime;
102}
103
104void MediaFragmentURIParser::parseFragments()
105{
106    if (!m_url.hasFragmentIdentifier())
107        return;
108    String fragmentString = m_url.fragmentIdentifier();
109    if (fragmentString.isEmpty())
110        return;
111
112    unsigned offset = 0;
113    unsigned end = fragmentString.length();
114    while (offset < end) {
115        // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#processing-name-value-components
116        // 1. Parse the octet string according to the namevalues syntax, yielding a list of
117        //    name-value pairs, where name and value are both octet string. In accordance
118        //    with RFC 3986, the name and value components must be parsed and separated before
119        //    percent-encoded octets are decoded.
120        size_t parameterStart = offset;
121        size_t parameterEnd = fragmentString.find('&', offset);
122        if (parameterEnd == notFound)
123            parameterEnd = end;
124
125        size_t equalOffset = fragmentString.find('=', offset);
126        if (equalOffset == notFound || equalOffset > parameterEnd) {
127            offset = parameterEnd + 1;
128            continue;
129        }
130
131        // 2. For each name-value pair:
132        //  a. Decode percent-encoded octets in name and value as defined by RFC 3986. If either
133        //     name or value are not valid percent-encoded strings, then remove the name-value pair
134        //     from the list.
135        const UChar* fragmentStart = fragmentString.characters();
136        String name = decodeURLEscapeSequences(String(fragmentStart + parameterStart, equalOffset - parameterStart));
137        String value;
138        if (equalOffset != parameterEnd)
139            value = decodeURLEscapeSequences(String(fragmentStart + equalOffset + 1, parameterEnd - equalOffset - 1));
140
141        //  b. Convert name and value to Unicode strings by interpreting them as UTF-8. If either
142        //     name or value are not valid UTF-8 strings, then remove the name-value pair from the list.
143        bool validUTF8 = true;
144        if (!name.isEmpty()) {
145            name = name.utf8(String::StrictConversion).data();
146            validUTF8 = !name.isEmpty();
147        }
148        if (validUTF8 && !value.isEmpty()) {
149            value = value.utf8(String::StrictConversion).data();
150            validUTF8 = !value.isEmpty();
151        }
152
153        if (validUTF8)
154            m_fragments.append(std::make_pair(name, value));
155
156        offset = parameterEnd + 1;
157    }
158}
159
160void MediaFragmentURIParser::parseTimeFragment()
161{
162    ASSERT(m_timeFormat == None);
163
164    if (m_fragments.isEmpty())
165        parseFragments();
166
167    m_timeFormat = Invalid;
168
169    for (unsigned i = 0; i < m_fragments.size(); ++i) {
170        pair<String, String>& fragment = m_fragments[i];
171
172        ASSERT(fragment.first.is8Bit());
173        ASSERT(fragment.second.is8Bit());
174
175        // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#naming-time
176        // Temporal clipping is denoted by the name t, and specified as an interval with a begin
177        // time and an end time
178        if (fragment.first != "t")
179            continue;
180
181        // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#npt-time
182        // Temporal clipping can be specified either as Normal Play Time (npt) RFC 2326, as SMPTE timecodes,
183        // SMPTE, or as real-world clock time (clock) RFC 2326. Begin and end times are always specified
184        // in the same format. The format is specified by name, followed by a colon (:), with npt: being
185        // the default.
186
187        double start = MediaPlayer::invalidTime();
188        double end = MediaPlayer::invalidTime();
189        if (parseNPTFragment(fragment.second.characters8(), fragment.second.length(), start, end)) {
190            m_startTime = start;
191            m_endTime = end;
192            m_timeFormat = NormalPlayTime;
193
194            // Although we have a valid fragment, don't return yet because when a fragment dimensions
195            // occurs multiple times, only the last occurrence of that dimension is used:
196            // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#error-uri-general
197            // Multiple occurrences of the same dimension: only the last valid occurrence of a dimension
198            // (e.g., t=10 in #t=2&t=10) is interpreted, all previous occurrences (valid or invalid)
199            // SHOULD be ignored by the UA.
200        }
201    }
202    m_fragments.clear();
203}
204
205bool MediaFragmentURIParser::parseNPTFragment(const LChar* timeString, unsigned length, double& startTime, double& endTime)
206{
207    unsigned offset = 0;
208    if (length >= nptIdentiferLength && timeString[0] == 'n' && timeString[1] == 'p' && timeString[2] == 't' && timeString[3] == ':')
209            offset += nptIdentiferLength;
210
211    if (offset == length)
212        return false;
213
214    // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#naming-time
215    // If a single number only is given, this corresponds to the begin time except if it is preceded
216    // by a comma that would in this case indicate the end time.
217    if (timeString[offset] == ',')
218        startTime = 0;
219    else {
220        if (!parseNPTTime(timeString, length, offset, startTime))
221            return false;
222    }
223
224    if (offset == length)
225        return true;
226
227    if (timeString[offset] != ',')
228        return false;
229    if (++offset == length)
230        return false;
231
232    if (!parseNPTTime(timeString, length, offset, endTime))
233        return false;
234
235    if (offset != length)
236        return false;
237
238    if (startTime >= endTime)
239        return false;
240
241    return true;
242}
243
244bool MediaFragmentURIParser::parseNPTTime(const LChar* timeString, unsigned length, unsigned& offset, double& time)
245{
246    enum Mode { minutes, hours };
247    Mode mode = minutes;
248
249    if (offset >= length || !isASCIIDigit(timeString[offset]))
250        return false;
251
252    // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#npttimedef
253    // Normal Play Time can either be specified as seconds, with an optional
254    // fractional part to indicate miliseconds, or as colon-separated hours,
255    // minutes and seconds (again with an optional fraction). Minutes and
256    // seconds must be specified as exactly two digits, hours and fractional
257    // seconds can be any number of digits. The hours, minutes and seconds
258    // specification for NPT is a convenience only, it does not signal frame
259    // accuracy. The specification of the "npt:" identifier is optional since
260    // NPT is the default time scheme. This specification builds on the RTSP
261    // specification of NPT RFC 2326.
262    //
263    // ; defined in RFC 2326
264    // npt-sec       = 1*DIGIT [ "." *DIGIT ]                     ; definitions taken
265    // npt-hhmmss    = npt-hh ":" npt-mm ":" npt-ss [ "." *DIGIT] ; from RFC 2326
266    // npt-mmss      = npt-mm ":" npt-ss [ "." *DIGIT]
267    // npt-hh        =   1*DIGIT     ; any positive number
268    // npt-mm        =   2DIGIT      ; 0-59
269    // npt-ss        =   2DIGIT      ; 0-59
270
271    String digits1 = collectDigits(timeString, length, offset);
272    int value1 = digits1.toInt();
273    if (offset >= length || timeString[offset] == ',') {
274        time = value1;
275        return true;
276    }
277
278    double fraction = 0;
279    if (timeString[offset] == '.') {
280        if (offset == length)
281            return true;
282        String digits = collectFraction(timeString, length, offset);
283        fraction = digits.toDouble();
284        time = value1 + fraction;
285        return true;
286    }
287
288    if (digits1.length() < 2)
289        return false;
290    if (digits1.length() > 2)
291        mode = hours;
292
293    // Collect the next sequence of 0-9 after ':'
294    if (offset >= length || timeString[offset++] != ':')
295        return false;
296    if (offset >= length || !isASCIIDigit(timeString[(offset)]))
297        return false;
298    String digits2 = collectDigits(timeString, length, offset);
299    int value2 = digits2.toInt();
300    if (digits2.length() != 2)
301        return false;
302
303    // Detect whether this timestamp includes hours.
304    int value3;
305    if (mode == hours || (offset < length && timeString[offset] == ':')) {
306        if (offset >= length || timeString[offset++] != ':')
307            return false;
308        if (offset >= length || !isASCIIDigit(timeString[offset]))
309            return false;
310        String digits3 = collectDigits(timeString, length, offset);
311        if (digits3.length() != 2)
312            return false;
313        value3 = digits3.toInt();
314    } else {
315        value3 = value2;
316        value2 = value1;
317        value1 = 0;
318    }
319
320    if (offset < length && timeString[offset] == '.')
321        fraction = collectFraction(timeString, length, offset).toDouble();
322
323    time = (value1 * secondsPerHour) + (value2 * secondsPerMinute) + value3 + fraction;
324    return true;
325}
326
327}
328#endif
329