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 #include "core/html/MediaFragmentURIParser.h"
28
29 #include "platform/graphics/media/MediaPlayer.h"
30 #include "wtf/text/CString.h"
31 #include "wtf/text/StringBuilder.h"
32 #include "wtf/text/WTFString.h"
33
34 namespace blink {
35
36 const int secondsPerHour = 3600;
37 const int secondsPerMinute = 60;
38 const unsigned nptIdentiferLength = 4; // "npt:"
39
collectDigits(const LChar * input,unsigned length,unsigned & position)40 static String collectDigits(const LChar* input, unsigned length, unsigned& position)
41 {
42 StringBuilder digits;
43
44 // http://www.ietf.org/rfc/rfc2326.txt
45 // DIGIT ; any positive number
46 while (position < length && isASCIIDigit(input[position]))
47 digits.append(input[position++]);
48 return digits.toString();
49 }
50
collectFraction(const LChar * input,unsigned length,unsigned & position)51 static String collectFraction(const LChar* input, unsigned length, unsigned& position)
52 {
53 StringBuilder digits;
54
55 // http://www.ietf.org/rfc/rfc2326.txt
56 // [ "." *DIGIT ]
57 if (input[position] != '.')
58 return String();
59
60 digits.append(input[position++]);
61 while (position < length && isASCIIDigit(input[position]))
62 digits.append(input[position++]);
63 return digits.toString();
64 }
65
invalidTimeValue()66 double MediaFragmentURIParser::invalidTimeValue()
67 {
68 return MediaPlayer::invalidTime();
69 }
70
MediaFragmentURIParser(const KURL & url)71 MediaFragmentURIParser::MediaFragmentURIParser(const KURL& url)
72 : m_url(url)
73 , m_timeFormat(None)
74 , m_startTime(MediaPlayer::invalidTime())
75 , m_endTime(MediaPlayer::invalidTime())
76 {
77 }
78
startTime()79 double MediaFragmentURIParser::startTime()
80 {
81 if (!m_url.isValid())
82 return MediaPlayer::invalidTime();
83 if (m_timeFormat == None)
84 parseTimeFragment();
85 return m_startTime;
86 }
87
endTime()88 double MediaFragmentURIParser::endTime()
89 {
90 if (!m_url.isValid())
91 return MediaPlayer::invalidTime();
92 if (m_timeFormat == None)
93 parseTimeFragment();
94 return m_endTime;
95 }
96
parseFragments()97 void MediaFragmentURIParser::parseFragments()
98 {
99 if (!m_url.hasFragmentIdentifier())
100 return;
101 String fragmentString = m_url.fragmentIdentifier();
102 if (fragmentString.isEmpty())
103 return;
104
105 unsigned offset = 0;
106 unsigned end = fragmentString.length();
107 while (offset < end) {
108 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#processing-name-value-components
109 // 1. Parse the octet string according to the namevalues syntax, yielding a list of
110 // name-value pairs, where name and value are both octet string. In accordance
111 // with RFC 3986, the name and value components must be parsed and separated before
112 // percent-encoded octets are decoded.
113 size_t parameterStart = offset;
114 size_t parameterEnd = fragmentString.find('&', offset);
115 if (parameterEnd == kNotFound)
116 parameterEnd = end;
117
118 size_t equalOffset = fragmentString.find('=', offset);
119 if (equalOffset == kNotFound || equalOffset > parameterEnd) {
120 offset = parameterEnd + 1;
121 continue;
122 }
123
124 // 2. For each name-value pair:
125 // a. Decode percent-encoded octets in name and value as defined by RFC 3986. If either
126 // name or value are not valid percent-encoded strings, then remove the name-value pair
127 // from the list.
128 String name = decodeURLEscapeSequences(fragmentString.substring(parameterStart, equalOffset - parameterStart));
129 String value;
130 if (equalOffset != parameterEnd)
131 value = decodeURLEscapeSequences(fragmentString.substring(equalOffset + 1, parameterEnd - equalOffset - 1));
132
133 // b. Convert name and value to Unicode strings by interpreting them as UTF-8. If either
134 // name or value are not valid UTF-8 strings, then remove the name-value pair from the list.
135 bool validUTF8 = true;
136 if (!name.isEmpty()) {
137 name = name.utf8(StrictUTF8Conversion).data();
138 validUTF8 = !name.isEmpty();
139 }
140 if (validUTF8 && !value.isEmpty()) {
141 value = value.utf8(StrictUTF8Conversion).data();
142 validUTF8 = !value.isEmpty();
143 }
144
145 if (validUTF8)
146 m_fragments.append(std::make_pair(name, value));
147
148 offset = parameterEnd + 1;
149 }
150 }
151
parseTimeFragment()152 void MediaFragmentURIParser::parseTimeFragment()
153 {
154 ASSERT(m_timeFormat == None);
155
156 if (m_fragments.isEmpty())
157 parseFragments();
158
159 m_timeFormat = Invalid;
160
161 for (unsigned i = 0; i < m_fragments.size(); ++i) {
162 pair<String, String>& fragment = m_fragments[i];
163
164 ASSERT(fragment.first.is8Bit());
165 ASSERT(fragment.second.is8Bit());
166
167 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#naming-time
168 // Temporal clipping is denoted by the name t, and specified as an interval with a begin
169 // time and an end time
170 if (fragment.first != "t")
171 continue;
172
173 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#npt-time
174 // Temporal clipping can be specified either as Normal Play Time (npt) RFC 2326, as SMPTE timecodes,
175 // SMPTE, or as real-world clock time (clock) RFC 2326. Begin and end times are always specified
176 // in the same format. The format is specified by name, followed by a colon (:), with npt: being
177 // the default.
178
179 double start = MediaPlayer::invalidTime();
180 double end = MediaPlayer::invalidTime();
181 if (parseNPTFragment(fragment.second.characters8(), fragment.second.length(), start, end)) {
182 m_startTime = start;
183 m_endTime = end;
184 m_timeFormat = NormalPlayTime;
185
186 // Although we have a valid fragment, don't return yet because when a fragment dimensions
187 // occurs multiple times, only the last occurrence of that dimension is used:
188 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#error-uri-general
189 // Multiple occurrences of the same dimension: only the last valid occurrence of a dimension
190 // (e.g., t=10 in #t=2&t=10) is interpreted, all previous occurrences (valid or invalid)
191 // SHOULD be ignored by the UA.
192 }
193 }
194 m_fragments.clear();
195 }
196
parseNPTFragment(const LChar * timeString,unsigned length,double & startTime,double & endTime)197 bool MediaFragmentURIParser::parseNPTFragment(const LChar* timeString, unsigned length, double& startTime, double& endTime)
198 {
199 unsigned offset = 0;
200 if (length >= nptIdentiferLength && timeString[0] == 'n' && timeString[1] == 'p' && timeString[2] == 't' && timeString[3] == ':')
201 offset += nptIdentiferLength;
202
203 if (offset == length)
204 return false;
205
206 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#naming-time
207 // If a single number only is given, this corresponds to the begin time except if it is preceded
208 // by a comma that would in this case indicate the end time.
209 if (timeString[offset] == ',') {
210 startTime = 0;
211 } else {
212 if (!parseNPTTime(timeString, length, offset, startTime))
213 return false;
214 }
215
216 if (offset == length)
217 return true;
218
219 if (timeString[offset] != ',')
220 return false;
221 if (++offset == length)
222 return false;
223
224 if (!parseNPTTime(timeString, length, offset, endTime))
225 return false;
226
227 if (offset != length)
228 return false;
229
230 if (startTime >= endTime)
231 return false;
232
233 return true;
234 }
235
parseNPTTime(const LChar * timeString,unsigned length,unsigned & offset,double & time)236 bool MediaFragmentURIParser::parseNPTTime(const LChar* timeString, unsigned length, unsigned& offset, double& time)
237 {
238 enum Mode { Minutes, Hours };
239 Mode mode = Minutes;
240
241 if (offset >= length || !isASCIIDigit(timeString[offset]))
242 return false;
243
244 // http://www.w3.org/2008/WebVideo/Fragments/WD-media-fragments-spec/#npttimedef
245 // Normal Play Time can either be specified as seconds, with an optional
246 // fractional part to indicate miliseconds, or as colon-separated hours,
247 // minutes and seconds (again with an optional fraction). Minutes and
248 // seconds must be specified as exactly two digits, hours and fractional
249 // seconds can be any number of digits. The hours, minutes and seconds
250 // specification for NPT is a convenience only, it does not signal frame
251 // accuracy. The specification of the "npt:" identifier is optional since
252 // NPT is the default time scheme. This specification builds on the RTSP
253 // specification of NPT RFC 2326.
254 //
255 // ; defined in RFC 2326
256 // npt-sec = 1*DIGIT [ "." *DIGIT ] ; definitions taken
257 // npt-hhmmss = npt-hh ":" npt-mm ":" npt-ss [ "." *DIGIT] ; from RFC 2326
258 // npt-mmss = npt-mm ":" npt-ss [ "." *DIGIT]
259 // npt-hh = 1*DIGIT ; any positive number
260 // npt-mm = 2DIGIT ; 0-59
261 // npt-ss = 2DIGIT ; 0-59
262
263 String digits1 = collectDigits(timeString, length, offset);
264 int value1 = digits1.toInt();
265 if (offset >= length || timeString[offset] == ',') {
266 time = value1;
267 return true;
268 }
269
270 double fraction = 0;
271 if (timeString[offset] == '.') {
272 if (offset == length)
273 return true;
274 String digits = collectFraction(timeString, length, offset);
275 fraction = digits.toDouble();
276 time = value1 + fraction;
277 return true;
278 }
279
280 if (digits1.length() < 2)
281 return false;
282 if (digits1.length() > 2)
283 mode = Hours;
284
285 // Collect the next sequence of 0-9 after ':'
286 if (offset >= length || timeString[offset++] != ':')
287 return false;
288 if (offset >= length || !isASCIIDigit(timeString[(offset)]))
289 return false;
290 String digits2 = collectDigits(timeString, length, offset);
291 int value2 = digits2.toInt();
292 if (digits2.length() != 2)
293 return false;
294
295 // Detect whether this timestamp includes hours.
296 int value3;
297 if (mode == Hours || (offset < length && timeString[offset] == ':')) {
298 if (offset >= length || timeString[offset++] != ':')
299 return false;
300 if (offset >= length || !isASCIIDigit(timeString[offset]))
301 return false;
302 String digits3 = collectDigits(timeString, length, offset);
303 if (digits3.length() != 2)
304 return false;
305 value3 = digits3.toInt();
306 } else {
307 value3 = value2;
308 value2 = value1;
309 value1 = 0;
310 }
311
312 if (offset < length && timeString[offset] == '.')
313 fraction = collectFraction(timeString, length, offset).toDouble();
314
315 time = (value1 * secondsPerHour) + (value2 * secondsPerMinute) + value3 + fraction;
316 return true;
317 }
318
319 }
320