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 WebCore { 35 36 const int secondsPerHour = 3600; 37 const int secondsPerMinute = 60; 38 const unsigned nptIdentiferLength = 4; // "npt:" 39 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 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 66 double MediaFragmentURIParser::invalidTimeValue() 67 { 68 return MediaPlayer::invalidTime(); 69 } 70 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 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 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 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(String::StrictConversion).data(); 138 validUTF8 = !name.isEmpty(); 139 } 140 if (validUTF8 && !value.isEmpty()) { 141 value = value.utf8(String::StrictConversion).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 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 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 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