tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

collection_validator.sys.mjs (8415B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  Async: "resource://services-common/async.sys.mjs",
      9 });
     10 
     11 export class CollectionProblemData {
     12  constructor() {
     13    this.missingIDs = 0;
     14    this.clientDuplicates = [];
     15    this.duplicates = [];
     16    this.clientMissing = [];
     17    this.serverMissing = [];
     18    this.serverDeleted = [];
     19    this.serverUnexpected = [];
     20    this.differences = [];
     21  }
     22 
     23  /**
     24   * Produce a list summarizing problems found. Each entry contains {name, count},
     25   * where name is the field name for the problem, and count is the number of times
     26   * the problem was encountered.
     27   *
     28   * Validation has failed if all counts are not 0.
     29   */
     30  getSummary() {
     31    return [
     32      { name: "clientMissing", count: this.clientMissing.length },
     33      { name: "serverMissing", count: this.serverMissing.length },
     34      { name: "serverDeleted", count: this.serverDeleted.length },
     35      { name: "serverUnexpected", count: this.serverUnexpected.length },
     36      { name: "differences", count: this.differences.length },
     37      { name: "missingIDs", count: this.missingIDs },
     38      { name: "clientDuplicates", count: this.clientDuplicates.length },
     39      { name: "duplicates", count: this.duplicates.length },
     40    ];
     41  }
     42 }
     43 
     44 export class CollectionValidator {
     45  // Construct a generic collection validator. This is intended to be called by
     46  // subclasses.
     47  // - name: Name of the engine
     48  // - idProp: Property that identifies a record. That is, if a client and server
     49  //   record have the same value for the idProp property, they should be
     50  //   compared against eachother.
     51  // - props: Array of properties that should be compared
     52  constructor(name, idProp, props) {
     53    this.name = name;
     54    this.props = props;
     55    this.idProp = idProp;
     56 
     57    // This property deals with the fact that form history records are never
     58    // deleted from the server. The FormValidator subclass needs to ignore the
     59    // client missing records, and it uses this property to achieve it -
     60    // (Bug 1354016).
     61    this.ignoresMissingClients = false;
     62  }
     63 
     64  // Should a custom ProblemData type be needed, return it here.
     65  emptyProblemData() {
     66    return new CollectionProblemData();
     67  }
     68 
     69  async getServerItems(engine) {
     70    let collection = engine.itemSource();
     71    let collectionKey = engine.service.collectionKeys.keyForCollection(
     72      engine.name
     73    );
     74    collection.full = true;
     75    let result = await collection.getBatched();
     76    if (!result.response.success) {
     77      throw result.response;
     78    }
     79    let cleartexts = [];
     80 
     81    await lazy.Async.yieldingForEach(result.records, async record => {
     82      await record.decrypt(collectionKey);
     83      cleartexts.push(record.cleartext);
     84    });
     85 
     86    return cleartexts;
     87  }
     88 
     89  // Should return a promise that resolves to an array of client items.
     90  getClientItems() {
     91    return Promise.reject("Must implement");
     92  }
     93 
     94  /**
     95   * Can we guarantee validation will fail with a reason that isn't actually a
     96   * problem? For example, if we know there are pending changes left over from
     97   * the last sync, this should resolve to false. By default resolves to true.
     98   */
     99  async canValidate() {
    100    return true;
    101  }
    102 
    103  // Turn the client item into something that can be compared with the server item,
    104  // and is also safe to mutate.
    105  normalizeClientItem(item) {
    106    return Cu.cloneInto(item, {});
    107  }
    108 
    109  // Turn the server item into something that can be easily compared with the client
    110  // items.
    111  async normalizeServerItem(item) {
    112    return item;
    113  }
    114 
    115  // Return whether or not a server item should be present on the client. Expected
    116  // to be overridden.
    117  clientUnderstands() {
    118    return true;
    119  }
    120 
    121  // Return whether or not a client item should be present on the server. Expected
    122  // to be overridden
    123  async syncedByClient() {
    124    return true;
    125  }
    126 
    127  // Compare the server item and the client item, and return a list of property
    128  // names that are different. Can be overridden if needed.
    129  getDifferences(client, server) {
    130    let differences = [];
    131    for (let prop of this.props) {
    132      let clientProp = client[prop];
    133      let serverProp = server[prop];
    134      if ((clientProp || "") !== (serverProp || "")) {
    135        differences.push(prop);
    136      }
    137    }
    138    return differences;
    139  }
    140 
    141  // Returns an object containing
    142  //   problemData: an instance of the class returned by emptyProblemData(),
    143  //   clientRecords: Normalized client records
    144  //   records: Normalized server records,
    145  //   deletedRecords: Array of ids that were marked as deleted by the server.
    146  async compareClientWithServer(clientItems, serverItems) {
    147    const yieldState = lazy.Async.yieldState();
    148 
    149    const clientRecords = [];
    150 
    151    await lazy.Async.yieldingForEach(
    152      clientItems,
    153      item => {
    154        clientRecords.push(this.normalizeClientItem(item));
    155      },
    156      yieldState
    157    );
    158 
    159    const serverRecords = [];
    160    await lazy.Async.yieldingForEach(
    161      serverItems,
    162      async item => {
    163        serverRecords.push(await this.normalizeServerItem(item));
    164      },
    165      yieldState
    166    );
    167 
    168    let problems = this.emptyProblemData();
    169    let seenServer = new Map();
    170    let serverDeleted = new Set();
    171    let allRecords = new Map();
    172 
    173    for (let record of serverRecords) {
    174      let id = record[this.idProp];
    175      if (!id) {
    176        ++problems.missingIDs;
    177        continue;
    178      }
    179      if (record.deleted) {
    180        serverDeleted.add(record);
    181      } else {
    182        let serverHasPossibleDupe = seenServer.has(id);
    183        if (serverHasPossibleDupe) {
    184          problems.duplicates.push(id);
    185        } else {
    186          seenServer.set(id, record);
    187          allRecords.set(id, { server: record, client: null });
    188        }
    189        record.understood = this.clientUnderstands(record);
    190      }
    191    }
    192 
    193    let seenClient = new Map();
    194    for (let record of clientRecords) {
    195      let id = record[this.idProp];
    196      record.shouldSync = await this.syncedByClient(record);
    197      let clientHasPossibleDupe = seenClient.has(id);
    198      if (clientHasPossibleDupe && record.shouldSync) {
    199        // Only report duplicate client IDs for syncable records.
    200        problems.clientDuplicates.push(id);
    201        continue;
    202      }
    203      seenClient.set(id, record);
    204      let combined = allRecords.get(id);
    205      if (combined) {
    206        combined.client = record;
    207      } else {
    208        allRecords.set(id, { client: record, server: null });
    209      }
    210    }
    211 
    212    for (let [id, { server, client }] of allRecords) {
    213      if (!client && !server) {
    214        throw new Error("Impossible: no client or server record for " + id);
    215      } else if (server && !client) {
    216        if (!this.ignoresMissingClients && server.understood) {
    217          problems.clientMissing.push(id);
    218        }
    219      } else if (client && !server) {
    220        if (client.shouldSync) {
    221          problems.serverMissing.push(id);
    222        }
    223      } else {
    224        if (!client.shouldSync) {
    225          if (!problems.serverUnexpected.includes(id)) {
    226            problems.serverUnexpected.push(id);
    227          }
    228          continue;
    229        }
    230        let differences = this.getDifferences(client, server);
    231        if (differences && differences.length) {
    232          problems.differences.push({ id, differences });
    233        }
    234      }
    235    }
    236    return {
    237      problemData: problems,
    238      clientRecords,
    239      records: serverRecords,
    240      deletedRecords: [...serverDeleted],
    241    };
    242  }
    243 
    244  async validate(engine) {
    245    let start = ChromeUtils.now();
    246    let clientItems = await this.getClientItems();
    247    let serverItems = await this.getServerItems(engine);
    248    let serverRecordCount = serverItems.length;
    249    let result = await this.compareClientWithServer(clientItems, serverItems);
    250    let end = ChromeUtils.now();
    251    let duration = end - start;
    252    engine._log.debug(`Validated ${this.name} in ${duration}ms`);
    253    engine._log.debug(`Problem summary`);
    254    for (let { name, count } of result.problemData.getSummary()) {
    255      engine._log.debug(`  ${name}: ${count}`);
    256    }
    257    return {
    258      duration,
    259      version: this.version,
    260      problems: result.problemData,
    261      recordCount: serverRecordCount,
    262    };
    263  }
    264 }
    265 
    266 // Default to 0, some engines may override.
    267 CollectionValidator.prototype.version = 0;