toolkit/components/url-classifier/ProtocolParser.cpp

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

     1 //* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 #include "ProtocolParser.h"
     7 #include "LookupCache.h"
     8 #include "nsNetCID.h"
     9 #include "prlog.h"
    10 #include "prnetdb.h"
    11 #include "prprf.h"
    13 #include "nsUrlClassifierUtils.h"
    15 // NSPR_LOG_MODULES=UrlClassifierDbService:5
    16 extern PRLogModuleInfo *gUrlClassifierDbServiceLog;
    17 #if defined(PR_LOGGING)
    18 #define LOG(args) PR_LOG(gUrlClassifierDbServiceLog, PR_LOG_DEBUG, args)
    19 #define LOG_ENABLED() PR_LOG_TEST(gUrlClassifierDbServiceLog, 4)
    20 #else
    21 #define LOG(args)
    22 #define LOG_ENABLED() (false)
    23 #endif
    25 namespace mozilla {
    26 namespace safebrowsing {
    28 // Updates will fail if fed chunks larger than this
    29 const uint32_t MAX_CHUNK_SIZE = (1024 * 1024);
    31 const uint32_t DOMAIN_SIZE = 4;
    33 // Parse one stringified range of chunks of the form "n" or "n-m" from a
    34 // comma-separated list of chunks.  Upon return, 'begin' will point to the
    35 // next range of chunks in the list of chunks.
    36 static bool
    37 ParseChunkRange(nsACString::const_iterator& aBegin,
    38                 const nsACString::const_iterator& aEnd,
    39                 uint32_t* aFirst, uint32_t* aLast)
    40 {
    41   nsACString::const_iterator iter = aBegin;
    42   FindCharInReadable(',', iter, aEnd);
    44   nsAutoCString element(Substring(aBegin, iter));
    45   aBegin = iter;
    46   if (aBegin != aEnd)
    47     aBegin++;
    49   uint32_t numRead = PR_sscanf(element.get(), "%u-%u", aFirst, aLast);
    50   if (numRead == 2) {
    51     if (*aFirst > *aLast) {
    52       uint32_t tmp = *aFirst;
    53       *aFirst = *aLast;
    54       *aLast = tmp;
    55     }
    56     return true;
    57   }
    59   if (numRead == 1) {
    60     *aLast = *aFirst;
    61     return true;
    62   }
    64   return false;
    65 }
    67 ProtocolParser::ProtocolParser()
    68     : mState(PROTOCOL_STATE_CONTROL)
    69   , mUpdateStatus(NS_OK)
    70   , mUpdateWait(0)
    71   , mResetRequested(false)
    72 {
    73 }
    75 ProtocolParser::~ProtocolParser()
    76 {
    77   CleanupUpdates();
    78 }
    80 nsresult
    81 ProtocolParser::Init(nsICryptoHash* aHasher)
    82 {
    83   mCryptoHash = aHasher;
    84   return NS_OK;
    85 }
    87 void
    88 ProtocolParser::SetCurrentTable(const nsACString& aTable)
    89 {
    90   mTableUpdate = GetTableUpdate(aTable);
    91 }
    93 nsresult
    94 ProtocolParser::AppendStream(const nsACString& aData)
    95 {
    96   if (NS_FAILED(mUpdateStatus))
    97     return mUpdateStatus;
    99   nsresult rv;
   100   mPending.Append(aData);
   102   bool done = false;
   103   while (!done) {
   104     if (mState == PROTOCOL_STATE_CONTROL) {
   105       rv = ProcessControl(&done);
   106     } else if (mState == PROTOCOL_STATE_CHUNK) {
   107       rv = ProcessChunk(&done);
   108     } else {
   109       NS_ERROR("Unexpected protocol state");
   110       rv = NS_ERROR_FAILURE;
   111     }
   112     if (NS_FAILED(rv)) {
   113       mUpdateStatus = rv;
   114       return rv;
   115     }
   116   }
   117   return NS_OK;
   118 }
   120 nsresult
   121 ProtocolParser::ProcessControl(bool* aDone)
   122 {
   123   nsresult rv;
   125   nsAutoCString line;
   126   *aDone = true;
   127   while (NextLine(line)) {
   128     //LOG(("Processing %s\n", line.get()));
   130     if (StringBeginsWith(line, NS_LITERAL_CSTRING("i:"))) {
   131       // Set the table name from the table header line.
   132       SetCurrentTable(Substring(line, 2));
   133     } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("n:"))) {
   134       if (PR_sscanf(line.get(), "n:%d", &mUpdateWait) != 1) {
   135         LOG(("Error parsing n: '%s' (%d)", line.get(), mUpdateWait));
   136         mUpdateWait = 0;
   137       }
   138     } else if (line.EqualsLiteral("r:pleasereset")) {
   139       mResetRequested = true;
   140     } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("u:"))) {
   141       rv = ProcessForward(line);
   142       NS_ENSURE_SUCCESS(rv, rv);
   143     } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("a:")) ||
   144                StringBeginsWith(line, NS_LITERAL_CSTRING("s:"))) {
   145       rv = ProcessChunkControl(line);
   146       NS_ENSURE_SUCCESS(rv, rv);
   147       *aDone = false;
   148       return NS_OK;
   149     } else if (StringBeginsWith(line, NS_LITERAL_CSTRING("ad:")) ||
   150                StringBeginsWith(line, NS_LITERAL_CSTRING("sd:"))) {
   151       rv = ProcessExpirations(line);
   152       NS_ENSURE_SUCCESS(rv, rv);
   153     }
   154   }
   156   *aDone = true;
   157   return NS_OK;
   158 }
   160 nsresult
   161 ProtocolParser::ProcessExpirations(const nsCString& aLine)
   162 {
   163   if (!mTableUpdate) {
   164     NS_WARNING("Got an expiration without a table.");
   165     return NS_ERROR_FAILURE;
   166   }
   167   const nsCSubstring &list = Substring(aLine, 3);
   168   nsACString::const_iterator begin, end;
   169   list.BeginReading(begin);
   170   list.EndReading(end);
   171   while (begin != end) {
   172     uint32_t first, last;
   173     if (ParseChunkRange(begin, end, &first, &last)) {
   174       for (uint32_t num = first; num <= last; num++) {
   175         if (aLine[0] == 'a')
   176           mTableUpdate->NewAddExpiration(num);
   177         else
   178           mTableUpdate->NewSubExpiration(num);
   179       }
   180     } else {
   181       return NS_ERROR_FAILURE;
   182     }
   183   }
   184   return NS_OK;
   185 }
   187 nsresult
   188 ProtocolParser::ProcessChunkControl(const nsCString& aLine)
   189 {
   190   if (!mTableUpdate) {
   191     NS_WARNING("Got a chunk before getting a table.");
   192     return NS_ERROR_FAILURE;
   193   }
   195   mState = PROTOCOL_STATE_CHUNK;
   196   char command;
   198   mChunkState.Clear();
   200   if (PR_sscanf(aLine.get(),
   201                 "%c:%d:%d:%d",
   202                 &command,
   203                 &mChunkState.num, &mChunkState.hashSize, &mChunkState.length)
   204       != 4)
   205   {
   206     return NS_ERROR_FAILURE;
   207   }
   209   if (mChunkState.length > MAX_CHUNK_SIZE) {
   210     return NS_ERROR_FAILURE;
   211   }
   213   if (!(mChunkState.hashSize == PREFIX_SIZE || mChunkState.hashSize == COMPLETE_SIZE)) {
   214     NS_WARNING("Invalid hash size specified in update.");
   215     return NS_ERROR_FAILURE;
   216   }
   218   if (StringEndsWith(mTableUpdate->TableName(),
   219                      NS_LITERAL_CSTRING("-shavar")) ||
   220       StringEndsWith(mTableUpdate->TableName(),
   221                      NS_LITERAL_CSTRING("-simple"))) {
   222     // Accommodate test tables ending in -simple for now.
   223     mChunkState.type = (command == 'a') ? CHUNK_ADD : CHUNK_SUB;
   224   } else if (StringEndsWith(mTableUpdate->TableName(),
   225     NS_LITERAL_CSTRING("-digest256"))) {
   226     LOG(("Processing digest256 data"));
   227     mChunkState.type = (command == 'a') ? CHUNK_ADD_DIGEST : CHUNK_SUB_DIGEST;
   228   }
   229   switch (mChunkState.type) {
   230     case CHUNK_ADD:
   231       mTableUpdate->NewAddChunk(mChunkState.num);
   232       break;
   233     case CHUNK_SUB:
   234       mTableUpdate->NewSubChunk(mChunkState.num);
   235       break;
   236     case CHUNK_ADD_DIGEST:
   237       mTableUpdate->NewAddChunk(mChunkState.num);
   238       break;
   239     case CHUNK_SUB_DIGEST:
   240       mTableUpdate->NewSubChunk(mChunkState.num);
   241       break;
   242   }
   244   return NS_OK;
   245 }
   247 nsresult
   248 ProtocolParser::ProcessForward(const nsCString& aLine)
   249 {
   250   const nsCSubstring &forward = Substring(aLine, 2);
   251   return AddForward(forward);
   252 }
   254 nsresult
   255 ProtocolParser::AddForward(const nsACString& aUrl)
   256 {
   257   if (!mTableUpdate) {
   258     NS_WARNING("Forward without a table name.");
   259     return NS_ERROR_FAILURE;
   260   }
   262   ForwardedUpdate *forward = mForwards.AppendElement();
   263   forward->table = mTableUpdate->TableName();
   264   forward->url.Assign(aUrl);
   266   return NS_OK;
   267 }
   269 nsresult
   270 ProtocolParser::ProcessChunk(bool* aDone)
   271 {
   272   if (!mTableUpdate) {
   273     NS_WARNING("Processing chunk without an active table.");
   274     return NS_ERROR_FAILURE;
   275   }
   277   NS_ASSERTION(mChunkState.num != 0, "Must have a chunk number.");
   279   if (mPending.Length() < mChunkState.length) {
   280     *aDone = true;
   281     return NS_OK;
   282   }
   284   // Pull the chunk out of the pending stream data.
   285   nsAutoCString chunk;
   286   chunk.Assign(Substring(mPending, 0, mChunkState.length));
   287   mPending = Substring(mPending, mChunkState.length);
   289   *aDone = false;
   290   mState = PROTOCOL_STATE_CONTROL;
   292   //LOG(("Handling a %d-byte chunk", chunk.Length()));
   293   if (StringEndsWith(mTableUpdate->TableName(),
   294                      NS_LITERAL_CSTRING("-shavar"))) {
   295     return ProcessShaChunk(chunk);
   296   }
   297   if (StringEndsWith(mTableUpdate->TableName(),
   298              NS_LITERAL_CSTRING("-digest256"))) {
   299     return ProcessDigestChunk(chunk);
   300   }
   301   return ProcessPlaintextChunk(chunk);
   302 }
   304 /**
   305  * Process a plaintext chunk (currently only used in unit tests).
   306  */
   307 nsresult
   308 ProtocolParser::ProcessPlaintextChunk(const nsACString& aChunk)
   309 {
   310   if (!mTableUpdate) {
   311     NS_WARNING("Chunk received with no table.");
   312     return NS_ERROR_FAILURE;
   313   }
   315   nsTArray<nsCString> lines;
   316   ParseString(PromiseFlatCString(aChunk), '\n', lines);
   318   // non-hashed tables need to be hashed
   319   for (uint32_t i = 0; i < lines.Length(); i++) {
   320     nsCString& line = lines[i];
   322     if (mChunkState.type == CHUNK_ADD) {
   323       if (mChunkState.hashSize == COMPLETE_SIZE) {
   324         Completion hash;
   325         hash.FromPlaintext(line, mCryptoHash);
   326         mTableUpdate->NewAddComplete(mChunkState.num, hash);
   327       } else {
   328         NS_ASSERTION(mChunkState.hashSize == 4, "Only 32- or 4-byte hashes can be used for add chunks.");
   329         Prefix hash;
   330         hash.FromPlaintext(line, mCryptoHash);
   331         mTableUpdate->NewAddPrefix(mChunkState.num, hash);
   332       }
   333     } else {
   334       nsCString::const_iterator begin, iter, end;
   335       line.BeginReading(begin);
   336       line.EndReading(end);
   337       iter = begin;
   338       uint32_t addChunk;
   339       if (!FindCharInReadable(':', iter, end) ||
   340           PR_sscanf(lines[i].get(), "%d:", &addChunk) != 1) {
   341         NS_WARNING("Received sub chunk without associated add chunk.");
   342         return NS_ERROR_FAILURE;
   343       }
   344       iter++;
   346       if (mChunkState.hashSize == COMPLETE_SIZE) {
   347         Completion hash;
   348         hash.FromPlaintext(Substring(iter, end), mCryptoHash);
   349         mTableUpdate->NewSubComplete(addChunk, hash, mChunkState.num);
   350       } else {
   351         NS_ASSERTION(mChunkState.hashSize == 4, "Only 32- or 4-byte hashes can be used for add chunks.");
   352         Prefix hash;
   353         hash.FromPlaintext(Substring(iter, end), mCryptoHash);
   354         mTableUpdate->NewSubPrefix(addChunk, hash, mChunkState.num);
   355       }
   356     }
   357   }
   359   return NS_OK;
   360 }
   362 nsresult
   363 ProtocolParser::ProcessShaChunk(const nsACString& aChunk)
   364 {
   365   uint32_t start = 0;
   366   while (start < aChunk.Length()) {
   367     // First four bytes are the domain key.
   368     Prefix domain;
   369     domain.Assign(Substring(aChunk, start, DOMAIN_SIZE));
   370     start += DOMAIN_SIZE;
   372     // Then a count of entries.
   373     uint8_t numEntries = static_cast<uint8_t>(aChunk[start]);
   374     start++;
   376     nsresult rv;
   377     if (mChunkState.type == CHUNK_ADD && mChunkState.hashSize == PREFIX_SIZE) {
   378       rv = ProcessHostAdd(domain, numEntries, aChunk, &start);
   379     } else if (mChunkState.type == CHUNK_ADD && mChunkState.hashSize == COMPLETE_SIZE) {
   380       rv = ProcessHostAddComplete(numEntries, aChunk, &start);
   381     } else if (mChunkState.type == CHUNK_SUB && mChunkState.hashSize == PREFIX_SIZE) {
   382       rv = ProcessHostSub(domain, numEntries, aChunk, &start);
   383     } else if (mChunkState.type == CHUNK_SUB && mChunkState.hashSize == COMPLETE_SIZE) {
   384       rv = ProcessHostSubComplete(numEntries, aChunk, &start);
   385     } else {
   386       NS_WARNING("Unexpected chunk type/hash size!");
   387       LOG(("Got an unexpected chunk type/hash size: %s:%d",
   388            mChunkState.type == CHUNK_ADD ? "add" : "sub",
   389            mChunkState.hashSize));
   390       return NS_ERROR_FAILURE;
   391     }
   392     NS_ENSURE_SUCCESS(rv, rv);
   393   }
   395   return NS_OK;
   396 }
   398 nsresult
   399 ProtocolParser::ProcessDigestChunk(const nsACString& aChunk)
   400 {
   401   if (mChunkState.type == CHUNK_ADD_DIGEST) {
   402     return ProcessDigestAdd(aChunk);
   403   }
   404   if (mChunkState.type == CHUNK_SUB_DIGEST) {
   405     return ProcessDigestSub(aChunk);
   406   }
   407   return NS_ERROR_UNEXPECTED;
   408 }
   410 nsresult
   411 ProtocolParser::ProcessDigestAdd(const nsACString& aChunk)
   412 {
   413   // The ABNF format for add chunks is (HASH)+, where HASH is 32 bytes.
   414   MOZ_ASSERT(aChunk.Length() % 32 == 0,
   415              "Chunk length in bytes must be divisible by 4");
   416   uint32_t start = 0;
   417   while (start < aChunk.Length()) {
   418     Completion hash;
   419     hash.Assign(Substring(aChunk, start, COMPLETE_SIZE));
   420     start += COMPLETE_SIZE;
   421     mTableUpdate->NewAddComplete(mChunkState.num, hash);
   422   }
   423   return NS_OK;
   424 }
   426 nsresult
   427 ProtocolParser::ProcessDigestSub(const nsACString& aChunk)
   428 {
   429   // The ABNF format for sub chunks is (ADDCHUNKNUM HASH)+, where ADDCHUNKNUM
   430   // is a 4 byte chunk number, and HASH is 32 bytes.
   431   MOZ_ASSERT(aChunk.Length() % 36 == 0,
   432              "Chunk length in bytes must be divisible by 36");
   433   uint32_t start = 0;
   434   while (start < aChunk.Length()) {
   435     // Read ADDCHUNKNUM
   436     const nsCSubstring& addChunkStr = Substring(aChunk, start, 4);
   437     start += 4;
   439     uint32_t addChunk;
   440     memcpy(&addChunk, addChunkStr.BeginReading(), 4);
   441     addChunk = PR_ntohl(addChunk);
   443     // Read the hash
   444     Completion hash;
   445     hash.Assign(Substring(aChunk, start, COMPLETE_SIZE));
   446     start += COMPLETE_SIZE;
   448     mTableUpdate->NewSubComplete(addChunk, hash, mChunkState.num);
   449   }
   450   return NS_OK;
   451 }
   453 nsresult
   454 ProtocolParser::ProcessHostAdd(const Prefix& aDomain, uint8_t aNumEntries,
   455                                const nsACString& aChunk, uint32_t* aStart)
   456 {
   457   NS_ASSERTION(mChunkState.hashSize == PREFIX_SIZE,
   458                "ProcessHostAdd should only be called for prefix hashes.");
   460   if (aNumEntries == 0) {
   461     mTableUpdate->NewAddPrefix(mChunkState.num, aDomain);
   462     return NS_OK;
   463   }
   465   if (*aStart + (PREFIX_SIZE * aNumEntries) > aChunk.Length()) {
   466     NS_WARNING("Chunk is not long enough to contain the expected entries.");
   467     return NS_ERROR_FAILURE;
   468   }
   470   for (uint8_t i = 0; i < aNumEntries; i++) {
   471     Prefix hash;
   472     hash.Assign(Substring(aChunk, *aStart, PREFIX_SIZE));
   473     mTableUpdate->NewAddPrefix(mChunkState.num, hash);
   474     *aStart += PREFIX_SIZE;
   475   }
   477   return NS_OK;
   478 }
   480 nsresult
   481 ProtocolParser::ProcessHostSub(const Prefix& aDomain, uint8_t aNumEntries,
   482                                const nsACString& aChunk, uint32_t *aStart)
   483 {
   484   NS_ASSERTION(mChunkState.hashSize == PREFIX_SIZE,
   485                "ProcessHostSub should only be called for prefix hashes.");
   487   if (aNumEntries == 0) {
   488     if ((*aStart) + 4 > aChunk.Length()) {
   489       NS_WARNING("Received a zero-entry sub chunk without an associated add.");
   490       return NS_ERROR_FAILURE;
   491     }
   493     const nsCSubstring& addChunkStr = Substring(aChunk, *aStart, 4);
   494     *aStart += 4;
   496     uint32_t addChunk;
   497     memcpy(&addChunk, addChunkStr.BeginReading(), 4);
   498     addChunk = PR_ntohl(addChunk);
   500     mTableUpdate->NewSubPrefix(addChunk, aDomain, mChunkState.num);
   501     return NS_OK;
   502   }
   504   if (*aStart + ((PREFIX_SIZE + 4) * aNumEntries) > aChunk.Length()) {
   505     NS_WARNING("Chunk is not long enough to contain the expected entries.");
   506     return NS_ERROR_FAILURE;
   507   }
   509   for (uint8_t i = 0; i < aNumEntries; i++) {
   510     const nsCSubstring& addChunkStr = Substring(aChunk, *aStart, 4);
   511     *aStart += 4;
   513     uint32_t addChunk;
   514     memcpy(&addChunk, addChunkStr.BeginReading(), 4);
   515     addChunk = PR_ntohl(addChunk);
   517     Prefix prefix;
   518     prefix.Assign(Substring(aChunk, *aStart, PREFIX_SIZE));
   519     *aStart += PREFIX_SIZE;
   521     mTableUpdate->NewSubPrefix(addChunk, prefix, mChunkState.num);
   522   }
   524   return NS_OK;
   525 }
   527 nsresult
   528 ProtocolParser::ProcessHostAddComplete(uint8_t aNumEntries,
   529                                        const nsACString& aChunk, uint32_t* aStart)
   530 {
   531   NS_ASSERTION(mChunkState.hashSize == COMPLETE_SIZE,
   532                "ProcessHostAddComplete should only be called for complete hashes.");
   534   if (aNumEntries == 0) {
   535     // this is totally comprehensible.
   536     // My sarcasm detector is going off!
   537     NS_WARNING("Expected > 0 entries for a 32-byte hash add.");
   538     return NS_OK;
   539   }
   541   if (*aStart + (COMPLETE_SIZE * aNumEntries) > aChunk.Length()) {
   542     NS_WARNING("Chunk is not long enough to contain the expected entries.");
   543     return NS_ERROR_FAILURE;
   544   }
   546   for (uint8_t i = 0; i < aNumEntries; i++) {
   547     Completion hash;
   548     hash.Assign(Substring(aChunk, *aStart, COMPLETE_SIZE));
   549     mTableUpdate->NewAddComplete(mChunkState.num, hash);
   550     *aStart += COMPLETE_SIZE;
   551   }
   553   return NS_OK;
   554 }
   556 nsresult
   557 ProtocolParser::ProcessHostSubComplete(uint8_t aNumEntries,
   558                                        const nsACString& aChunk, uint32_t* aStart)
   559 {
   560   NS_ASSERTION(mChunkState.hashSize == COMPLETE_SIZE,
   561                "ProcessHostSubComplete should only be called for complete hashes.");
   563   if (aNumEntries == 0) {
   564     // this is totally comprehensible.
   565     NS_WARNING("Expected > 0 entries for a 32-byte hash sub.");
   566     return NS_OK;
   567   }
   569   if (*aStart + ((COMPLETE_SIZE + 4) * aNumEntries) > aChunk.Length()) {
   570     NS_WARNING("Chunk is not long enough to contain the expected entries.");
   571     return NS_ERROR_FAILURE;
   572   }
   574   for (uint8_t i = 0; i < aNumEntries; i++) {
   575     Completion hash;
   576     hash.Assign(Substring(aChunk, *aStart, COMPLETE_SIZE));
   577     *aStart += COMPLETE_SIZE;
   579     const nsCSubstring& addChunkStr = Substring(aChunk, *aStart, 4);
   580     *aStart += 4;
   582     uint32_t addChunk;
   583     memcpy(&addChunk, addChunkStr.BeginReading(), 4);
   584     addChunk = PR_ntohl(addChunk);
   586     mTableUpdate->NewSubComplete(addChunk, hash, mChunkState.num);
   587   }
   589   return NS_OK;
   590 }
   592 bool
   593 ProtocolParser::NextLine(nsACString& line)
   594 {
   595   int32_t newline = mPending.FindChar('\n');
   596   if (newline == kNotFound) {
   597     return false;
   598   }
   599   line.Assign(Substring(mPending, 0, newline));
   600   mPending = Substring(mPending, newline + 1);
   601   return true;
   602 }
   604 void
   605 ProtocolParser::CleanupUpdates()
   606 {
   607   for (uint32_t i = 0; i < mTableUpdates.Length(); i++) {
   608     delete mTableUpdates[i];
   609   }
   610   mTableUpdates.Clear();
   611 }
   613 TableUpdate *
   614 ProtocolParser::GetTableUpdate(const nsACString& aTable)
   615 {
   616   for (uint32_t i = 0; i < mTableUpdates.Length(); i++) {
   617     if (aTable.Equals(mTableUpdates[i]->TableName())) {
   618       return mTableUpdates[i];
   619     }
   620   }
   622   // We free automatically on destruction, ownership of these
   623   // updates can be transferred to DBServiceWorker, which passes
   624   // them back to Classifier when doing the updates, and that
   625   // will free them.
   626   TableUpdate *update = new TableUpdate(aTable);
   627   mTableUpdates.AppendElement(update);
   628   return update;
   629 }
   631 } // namespace safebrowsing
   632 } // namespace mozilla

mercurial