dom/smil/nsSMILParserUtils.cpp
author Mike Hommey <mh+mozilla@glandium.org>
Fri, 02 Jun 2017 15:07:58 +0900
changeset 410640 f360dd9d411e7ba61531bed2c32a0981cc6c418a
parent 358722 8c6e2e1c4f2a897c153fd6be9a2a6cb52ba2d8d6
child 416428 6a629adbb62a299d7208373d1c6f375149d2afdb
permissions -rw-r--r--
Bug 1369622 - Restore static asserts for lack of arguments on some macros. r=froydnj Bug 1368932 removed MOZ_STATIC_ASSERT_VALID_ARG_COUNT because it didn't actually work for large numbers of arguments. But it was kind of useful for macros that expand to something broken when they are given no variadic argument at all. Now that we have a macro that doesn't require tricks to count empty arguments lists, however, we can use straightforward static_asserts instead of a generic macro, which has the side effect of providing more context in the error message.

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsSMILParserUtils.h"
#include "nsSMILKeySpline.h"
#include "nsISMILAttr.h"
#include "nsSMILValue.h"
#include "nsSMILTimeValue.h"
#include "nsSMILTimeValueSpecParams.h"
#include "nsSMILTypes.h"
#include "nsSMILRepeatCount.h"
#include "nsContentUtils.h"
#include "nsCharSeparatedTokenizer.h"
#include "SVGContentUtils.h"

using namespace mozilla;
using namespace mozilla::dom;
//------------------------------------------------------------------------------
// Helper functions and Constants

namespace {

const uint32_t MSEC_PER_SEC  = 1000;
const uint32_t MSEC_PER_MIN  = 1000 * 60;
const uint32_t MSEC_PER_HOUR = 1000 * 60 * 60;

#define ACCESSKEY_PREFIX_LC NS_LITERAL_STRING("accesskey(") // SMIL2+
#define ACCESSKEY_PREFIX_CC NS_LITERAL_STRING("accessKey(") // SVG/SMIL ANIM
#define REPEAT_PREFIX    NS_LITERAL_STRING("repeat(")
#define WALLCLOCK_PREFIX NS_LITERAL_STRING("wallclock(")

inline bool
SkipWhitespace(RangedPtr<const char16_t>& aIter,
               const RangedPtr<const char16_t>& aEnd)
{
  while (aIter != aEnd) {
    if (!IsSVGWhitespace(*aIter)) {
      return true;
    }
    ++aIter;
  }
  return false;
}

inline bool
ParseColon(RangedPtr<const char16_t>& aIter,
           const RangedPtr<const char16_t>& aEnd)
{
  if (aIter == aEnd || *aIter != ':') {
    return false;
  }
  ++aIter;
  return true;
}

/*
 * Exactly two digits in the range 00 - 59 are expected.
 */
bool
ParseSecondsOrMinutes(RangedPtr<const char16_t>& aIter,
                      const RangedPtr<const char16_t>& aEnd,
                      uint32_t& aValue)
{
  if (aIter == aEnd || !SVGContentUtils::IsDigit(*aIter)) {
    return false;
  }

  RangedPtr<const char16_t> iter(aIter);

  if (++iter == aEnd || !SVGContentUtils::IsDigit(*iter)) {
     return false;
  }

  uint32_t value = 10 * SVGContentUtils::DecimalDigitValue(*aIter) +
                   SVGContentUtils::DecimalDigitValue(*iter);
  if (value > 59) {
    return false;
  }
  if (++iter != aEnd && SVGContentUtils::IsDigit(*iter)) {
    return false;
  }

  aValue = value;
  aIter = iter;
  return true;
}

inline bool
ParseClockMetric(RangedPtr<const char16_t>& aIter,
                 const RangedPtr<const char16_t>& aEnd,
                 uint32_t& aMultiplier)
{
  if (aIter == aEnd) {
    aMultiplier = MSEC_PER_SEC;
    return true;
  }

  switch (*aIter) {
  case 'h':
    if (++aIter == aEnd) {
      aMultiplier = MSEC_PER_HOUR;
      return true;
    }
    return false;
  case 'm':
    {
      const nsAString& metric = Substring(aIter.get(), aEnd.get());
      if (metric.EqualsLiteral("min")) {
        aMultiplier = MSEC_PER_MIN;
        aIter = aEnd;
        return true;
      }
      if (metric.EqualsLiteral("ms")) {
        aMultiplier = 1;
        aIter = aEnd;
        return true;
      }
    }
    return false;
  case 's':
    if (++aIter == aEnd) {
      aMultiplier = MSEC_PER_SEC;
      return true;
    }
  }
  return false;
}

/**
 * See http://www.w3.org/TR/SVG/animate.html#ClockValueSyntax
 */
bool
ParseClockValue(RangedPtr<const char16_t>& aIter,
                const RangedPtr<const char16_t>& aEnd,
                nsSMILTimeValue* aResult)
{
  if (aIter == aEnd) {
    return false;
  }

  // TIMECOUNT_VALUE     ::= Timecount ("." Fraction)? (Metric)?
  // PARTIAL_CLOCK_VALUE ::= Minutes ":" Seconds ("." Fraction)?
  // FULL_CLOCK_VALUE    ::= Hours ":" Minutes ":" Seconds ("." Fraction)?
  enum ClockType {
    TIMECOUNT_VALUE,
    PARTIAL_CLOCK_VALUE,
    FULL_CLOCK_VALUE
  };

  int32_t clockType = TIMECOUNT_VALUE;

  RangedPtr<const char16_t> iter(aIter);

  // Determine which type of clock value we have by counting the number
  // of colons in the string.
  do {
    switch (*iter) {
    case ':':
       if (clockType == FULL_CLOCK_VALUE) {
         return false;
       }
       ++clockType;
       break;
    case 'e':
    case 'E':
    case '-':
    case '+':
      // Exclude anything invalid (for clock values)
      // that number parsing might otherwise allow.
      return false;
    }
    ++iter;
  } while (iter != aEnd);

  iter = aIter;

  int32_t hours = 0, timecount;
  double fraction = 0.0;
  uint32_t minutes, seconds, multiplier;

  switch (clockType) {
    case FULL_CLOCK_VALUE:
      if (!SVGContentUtils::ParseInteger(iter, aEnd, hours) ||
          !ParseColon(iter, aEnd)) {
        return false;
      }
      MOZ_FALLTHROUGH;
    case PARTIAL_CLOCK_VALUE:
      if (!ParseSecondsOrMinutes(iter, aEnd, minutes) ||
          !ParseColon(iter, aEnd) ||
          !ParseSecondsOrMinutes(iter, aEnd, seconds)) {
        return false;
      }
      if (iter != aEnd &&
          (*iter != '.' ||
           !SVGContentUtils::ParseNumber(iter, aEnd, fraction))) {
        return false;
      }
      aResult->SetMillis(nsSMILTime(hours) * MSEC_PER_HOUR +
                         minutes * MSEC_PER_MIN +
                         seconds * MSEC_PER_SEC +
                         NS_round(fraction * MSEC_PER_SEC));
      aIter = iter;
      return true;
    case TIMECOUNT_VALUE:
      if (!SVGContentUtils::ParseInteger(iter, aEnd, timecount)) {
        return false;
      }
      if (iter != aEnd && *iter == '.' &&
          !SVGContentUtils::ParseNumber(iter, aEnd, fraction)) {
        return false;
      }
      if (!ParseClockMetric(iter, aEnd, multiplier)) {
        return false;
      }
      aResult->SetMillis(nsSMILTime(timecount) * multiplier +
                         NS_round(fraction * multiplier));
      aIter = iter;
      return true;
  }

  return false;
}

bool
ParseOffsetValue(RangedPtr<const char16_t>& aIter,
                 const RangedPtr<const char16_t>& aEnd,
                 nsSMILTimeValue* aResult)
{
  RangedPtr<const char16_t> iter(aIter);

  int32_t sign;
  if (!SVGContentUtils::ParseOptionalSign(iter, aEnd, sign) ||
      !SkipWhitespace(iter, aEnd) ||
      !ParseClockValue(iter, aEnd, aResult)) {
    return false;
  }
  if (sign == -1) {
    aResult->SetMillis(-aResult->GetMillis());
  }
  aIter = iter;
  return true;
}

bool
ParseOffsetValue(const nsAString& aSpec,
                 nsSMILTimeValue* aResult)
{
  RangedPtr<const char16_t> iter(SVGContentUtils::GetStartRangedPtr(aSpec));
  const RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec));

  return ParseOffsetValue(iter, end, aResult) && iter == end;
}

bool
ParseOptionalOffset(RangedPtr<const char16_t>& aIter,
                    const RangedPtr<const char16_t>& aEnd,
                    nsSMILTimeValue* aResult)
{
  if (aIter == aEnd) {
    aResult->SetMillis(0L);
    return true;
  }

  return SkipWhitespace(aIter, aEnd) &&
         ParseOffsetValue(aIter, aEnd, aResult);
}

bool
ParseAccessKey(const nsAString& aSpec, nsSMILTimeValueSpecParams& aResult)
{
  MOZ_ASSERT(StringBeginsWith(aSpec, ACCESSKEY_PREFIX_CC) ||
             StringBeginsWith(aSpec, ACCESSKEY_PREFIX_LC),
             "Calling ParseAccessKey on non-accesskey-type spec");

  nsSMILTimeValueSpecParams result;
  result.mType = nsSMILTimeValueSpecParams::ACCESSKEY;

  MOZ_ASSERT(ACCESSKEY_PREFIX_LC.Length() == ACCESSKEY_PREFIX_CC.Length(),
             "Case variations for accesskey prefix differ in length");

  RangedPtr<const char16_t> iter(SVGContentUtils::GetStartRangedPtr(aSpec));
  RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec));

  iter += ACCESSKEY_PREFIX_LC.Length();

  // Expecting at least <accesskey> + ')'
  if (end - iter < 2)
    return false;

  uint32_t c = *iter++;

  // Process 32-bit codepoints
  if (NS_IS_HIGH_SURROGATE(c)) {
    if (end - iter < 2) // Expecting at least low-surrogate + ')'
      return false;
    uint32_t lo = *iter++;
    if (!NS_IS_LOW_SURROGATE(lo))
      return false;
    c = SURROGATE_TO_UCS4(c, lo);
  // XML 1.1 says that 0xFFFE and 0xFFFF are not valid characters
  } else if (NS_IS_LOW_SURROGATE(c) || c == 0xFFFE || c == 0xFFFF) {
    return false;
  }

  result.mRepeatIterationOrAccessKey = c;

  if (*iter++ != ')')
    return false;

  if (!ParseOptionalOffset(iter, end, &result.mOffset) || iter != end) {
    return false;
  }
  aResult = result;
  return true;
}

void
MoveToNextToken(RangedPtr<const char16_t>& aIter,
                const RangedPtr<const char16_t>& aEnd,
                bool aBreakOnDot,
                bool& aIsAnyCharEscaped)
{
  aIsAnyCharEscaped = false;

  bool isCurrentCharEscaped = false;

  while (aIter != aEnd && !IsSVGWhitespace(*aIter)) {
    if (isCurrentCharEscaped) {
      isCurrentCharEscaped = false;
    } else {
      if (*aIter == '+' || *aIter == '-' ||
          (aBreakOnDot && *aIter == '.')) {
        break;
      }
      if (*aIter == '\\') {
        isCurrentCharEscaped = true;
        aIsAnyCharEscaped = true;
      }
    }
    ++aIter;
  }
}

already_AddRefed<nsIAtom>
ConvertUnescapedTokenToAtom(const nsAString& aToken)
{
  // Whether the token is an id-ref or event-symbol it should be a valid NCName
  if (aToken.IsEmpty() || NS_FAILED(nsContentUtils::CheckQName(aToken, false)))
    return nullptr;
  return NS_Atomize(aToken);
}
    
already_AddRefed<nsIAtom>
ConvertTokenToAtom(const nsAString& aToken,
                   bool aUnescapeToken)
{
  // Unescaping involves making a copy of the string which we'd like to avoid if possible
  if (!aUnescapeToken) {
    return ConvertUnescapedTokenToAtom(aToken);
  }

  nsAutoString token(aToken);

  const char16_t* read = token.BeginReading();
  const char16_t* const end = token.EndReading();
  char16_t* write = token.BeginWriting();
  bool escape = false;

  while (read != end) {
    MOZ_ASSERT(write <= read, "Writing past where we've read");
    if (!escape && *read == '\\') {
      escape = true;
      ++read;
    } else {
      *write++ = *read++;
      escape = false;
    }
  }
  token.Truncate(write - token.BeginReading());

  return ConvertUnescapedTokenToAtom(token);
}

bool
ParseElementBaseTimeValueSpec(const nsAString& aSpec,
                              nsSMILTimeValueSpecParams& aResult)
{
  nsSMILTimeValueSpecParams result;

  //
  // The spec will probably look something like one of these
  //
  // element-name.begin
  // element-name.event-name
  // event-name
  // element-name.repeat(3)
  // event\.name
  //
  // Technically `repeat(3)' is permitted but the behaviour in this case is not
  // defined (for SMIL Animation) so we don't support it here.
  //

  RangedPtr<const char16_t> start(SVGContentUtils::GetStartRangedPtr(aSpec));
  RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec));

  if (start == end) {
    return false;
  }

  RangedPtr<const char16_t> tokenEnd(start);

  bool requiresUnescaping;
  MoveToNextToken(tokenEnd, end, true, requiresUnescaping);

  RefPtr<nsIAtom> atom =
    ConvertTokenToAtom(Substring(start.get(), tokenEnd.get()),
                       requiresUnescaping);
  if (atom == nullptr) {
    return false;
  }

  // Parse the second token if there is one
  if (tokenEnd != end && *tokenEnd == '.') {
    result.mDependentElemID = atom;

    ++tokenEnd;
    start = tokenEnd;
    MoveToNextToken(tokenEnd, end, false, requiresUnescaping);

    const nsAString& token2 = Substring(start.get(), tokenEnd.get());

    // element-name.begin
    if (token2.EqualsLiteral("begin")) {
      result.mType = nsSMILTimeValueSpecParams::SYNCBASE;
      result.mSyncBegin = true;
    // element-name.end
    } else if (token2.EqualsLiteral("end")) {
      result.mType = nsSMILTimeValueSpecParams::SYNCBASE;
      result.mSyncBegin = false;
    // element-name.repeat(digit+)
    } else if (StringBeginsWith(token2, REPEAT_PREFIX)) {
      start += REPEAT_PREFIX.Length();
      int32_t repeatValue;
      if (start == tokenEnd || *start == '+' || *start == '-' ||
          !SVGContentUtils::ParseInteger(start, tokenEnd, repeatValue)) {
        return false;
      }
      if (start == tokenEnd || *start != ')') {
        return false;
      }
      result.mType = nsSMILTimeValueSpecParams::REPEAT;
      result.mRepeatIterationOrAccessKey = repeatValue;
    // element-name.event-symbol
    } else {
      atom = ConvertTokenToAtom(token2, requiresUnescaping);
      if (atom == nullptr) {
        return false;
      }
      result.mType = nsSMILTimeValueSpecParams::EVENT;
      result.mEventSymbol = atom;
    }
  } else {
    // event-symbol
    result.mType = nsSMILTimeValueSpecParams::EVENT;
    result.mEventSymbol = atom;
  }

  // We've reached the end of the token, so we should now be either looking at
  // a '+', '-' (possibly with whitespace before it), or the end.
  if (!ParseOptionalOffset(tokenEnd, end, &result.mOffset) || tokenEnd != end) {
    return false;
  }
  aResult = result;
  return true;
}

} // namespace

//------------------------------------------------------------------------------
// Implementation

const nsDependentSubstring
nsSMILParserUtils::TrimWhitespace(const nsAString& aString)
{
  nsAString::const_iterator start, end;

  aString.BeginReading(start);
  aString.EndReading(end);

  // Skip whitespace characters at the beginning
  while (start != end && IsSVGWhitespace(*start)) {
    ++start;
  }

  // Skip whitespace characters at the end.
  while (end != start) {
    --end;

    if (!IsSVGWhitespace(*end)) {
      // Step back to the last non-whitespace character.
      ++end;

      break;
    }
  }

  return Substring(start, end);
}

bool
nsSMILParserUtils::ParseKeySplines(const nsAString& aSpec,
                                   FallibleTArray<nsSMILKeySpline>& aKeySplines)
{
  nsCharSeparatedTokenizerTemplate<IsSVGWhitespace> controlPointTokenizer(aSpec, ';');
  while (controlPointTokenizer.hasMoreTokens()) {

    nsCharSeparatedTokenizerTemplate<IsSVGWhitespace> 
      tokenizer(controlPointTokenizer.nextToken(), ',',
                nsCharSeparatedTokenizer::SEPARATOR_OPTIONAL);

    double values[4];
    for (int i = 0 ; i < 4; i++) {
      if (!tokenizer.hasMoreTokens() ||
          !SVGContentUtils::ParseNumber(tokenizer.nextToken(), values[i]) ||
          values[i] > 1.0 || values[i] < 0.0) {
        return false;
      }
    }
    if (tokenizer.hasMoreTokens() ||
        tokenizer.separatorAfterCurrentToken() ||
        !aKeySplines.AppendElement(nsSMILKeySpline(values[0],
                                                   values[1],
                                                   values[2],
                                                   values[3]),
                                   fallible)) {
      return false;
    }
  }

  return !aKeySplines.IsEmpty();
}

bool
nsSMILParserUtils::ParseSemicolonDelimitedProgressList(const nsAString& aSpec,
                                                       bool aNonDecreasing,
                                                       FallibleTArray<double>& aArray)
{
  nsCharSeparatedTokenizerTemplate<IsSVGWhitespace> tokenizer(aSpec, ';');

  double previousValue = -1.0;

  while (tokenizer.hasMoreTokens()) {
    double value;
    if (!SVGContentUtils::ParseNumber(tokenizer.nextToken(), value)) {
      return false;
    }

    if (value > 1.0 || value < 0.0 ||
        (aNonDecreasing && value < previousValue)) {
      return false;
    }

    if (!aArray.AppendElement(value, fallible)) {
      return false;
    }
    previousValue = value;
  }

  return !aArray.IsEmpty();
}

// Helper class for ParseValues
class MOZ_STACK_CLASS SMILValueParser :
  public nsSMILParserUtils::GenericValueParser
{
public:
  SMILValueParser(const SVGAnimationElement* aSrcElement,
                  const nsISMILAttr* aSMILAttr,
                  FallibleTArray<nsSMILValue>* aValuesArray,
                  bool* aPreventCachingOfSandwich) :
    mSrcElement(aSrcElement),
    mSMILAttr(aSMILAttr),
    mValuesArray(aValuesArray),
    mPreventCachingOfSandwich(aPreventCachingOfSandwich)
  {}

  virtual bool Parse(const nsAString& aValueStr) override {
    nsSMILValue newValue;
    bool tmpPreventCachingOfSandwich = false;
    if (NS_FAILED(mSMILAttr->ValueFromString(aValueStr, mSrcElement, newValue,
                                             tmpPreventCachingOfSandwich)))
      return false;

    if (!mValuesArray->AppendElement(newValue, fallible)) {
      return false;
    }
    if (tmpPreventCachingOfSandwich) {
      *mPreventCachingOfSandwich = true;
    }
    return true;
  }
protected:
  const SVGAnimationElement* mSrcElement;
  const nsISMILAttr* mSMILAttr;
  FallibleTArray<nsSMILValue>* mValuesArray;
  bool* mPreventCachingOfSandwich;
};

bool
nsSMILParserUtils::ParseValues(const nsAString& aSpec,
                               const SVGAnimationElement* aSrcElement,
                               const nsISMILAttr& aAttribute,
                               FallibleTArray<nsSMILValue>& aValuesArray,
                               bool& aPreventCachingOfSandwich)
{
  // Assume all results can be cached, until we find one that can't.
  aPreventCachingOfSandwich = false;
  SMILValueParser valueParser(aSrcElement, &aAttribute,
                              &aValuesArray, &aPreventCachingOfSandwich);
  return ParseValuesGeneric(aSpec, valueParser);
}

bool
nsSMILParserUtils::ParseValuesGeneric(const nsAString& aSpec,
                                      GenericValueParser& aParser)
{
  nsCharSeparatedTokenizerTemplate<IsSVGWhitespace> tokenizer(aSpec, ';');
  if (!tokenizer.hasMoreTokens()) { // Empty list
    return false;
  }

  while (tokenizer.hasMoreTokens()) {
    if (!aParser.Parse(tokenizer.nextToken())) {
      return false;
    }
  }

  return true;
}

bool
nsSMILParserUtils::ParseRepeatCount(const nsAString& aSpec,
                                    nsSMILRepeatCount& aResult)
{
  const nsAString& spec =
    nsSMILParserUtils::TrimWhitespace(aSpec);

  if (spec.EqualsLiteral("indefinite")) {
    aResult.SetIndefinite();
    return true;
  }

  double value;
  if (!SVGContentUtils::ParseNumber(spec, value) || value <= 0.0) {
    return false;
  }
  aResult = value;
  return true;
}

bool
nsSMILParserUtils::ParseTimeValueSpecParams(const nsAString& aSpec,
                                            nsSMILTimeValueSpecParams& aResult)
{
  const nsAString& spec = TrimWhitespace(aSpec);

  if (spec.EqualsLiteral("indefinite")) {
     aResult.mType = nsSMILTimeValueSpecParams::INDEFINITE;
     return true;
  }
  
  // offset type
  if (ParseOffsetValue(spec, &aResult.mOffset)) {
    aResult.mType = nsSMILTimeValueSpecParams::OFFSET;
    return true;
  }

  // wallclock type
  if (StringBeginsWith(spec, WALLCLOCK_PREFIX)) {
    return false; // Wallclock times not implemented
  }

  // accesskey type
  if (StringBeginsWith(spec, ACCESSKEY_PREFIX_LC) ||
      StringBeginsWith(spec, ACCESSKEY_PREFIX_CC)) {
    return ParseAccessKey(spec, aResult);
  }

  // event, syncbase, or repeat
  return ParseElementBaseTimeValueSpec(spec, aResult);
}

bool
nsSMILParserUtils::ParseClockValue(const nsAString& aSpec,
                                   nsSMILTimeValue* aResult)
{
  RangedPtr<const char16_t> iter(SVGContentUtils::GetStartRangedPtr(aSpec));
  RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec));

  return ::ParseClockValue(iter, end, aResult) && iter == end;
}

int32_t
nsSMILParserUtils::CheckForNegativeNumber(const nsAString& aStr)
{
  int32_t absValLocation = -1;

  RangedPtr<const char16_t> start(SVGContentUtils::GetStartRangedPtr(aStr));
  RangedPtr<const char16_t> iter = start;
  RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aStr));

  // Skip initial whitespace
  while (iter != end && IsSVGWhitespace(*iter)) {
    ++iter;
  }

  // Check for dash
  if (iter != end && *iter == '-') {
    ++iter;
    // Check for numeric character
    if (iter != end && SVGContentUtils::IsDigit(*iter)) {
      absValLocation = iter - start;
    }
  }
  return absValLocation;
}