Source: encrypt/producer.js

  1. /**
  2. * Copyright (C) 2015-2016 Regents of the University of California.
  3. * @author: Jeff Thompson <jefft0@remap.ucla.edu>
  4. * @author: From ndn-group-encrypt src/producer https://github.com/named-data/ndn-group-encrypt
  5. *
  6. * This program is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Lesser General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. * A copy of the GNU Lesser General Public License is in the file COPYING.
  19. */
  20. /** @ignore */
  21. var Name = require('../name.js').Name; /** @ignore */
  22. var Interest = require('../interest.js').Interest; /** @ignore */
  23. var Data = require('../data.js').Data; /** @ignore */
  24. var Exclude = require('../exclude.js').Exclude; /** @ignore */
  25. var Encryptor = require('./algo/encryptor.js').Encryptor; /** @ignore */
  26. var EncryptParams = require('./algo/encrypt-params.js').EncryptParams; /** @ignore */
  27. var EncryptAlgorithmType = require('./algo/encrypt-params.js').EncryptAlgorithmType; /** @ignore */
  28. var AesKeyParams = require('../security/key-params.js').AesKeyParams; /** @ignore */
  29. var AesAlgorithm = require('./algo/aes-algorithm.js').AesAlgorithm; /** @ignore */
  30. var Schedule = require('./schedule.js').Schedule; /** @ignore */
  31. var EncryptError = require('./encrypt-error.js').EncryptError; /** @ignore */
  32. var NdnCommon = require('../util/ndn-common.js').NdnCommon; /** @ignore */
  33. var SyncPromise = require('../util/sync-promise.js').SyncPromise;
  34. /**
  35. * A Producer manages content keys used to encrypt a data packet in the
  36. * group-based encryption protocol.
  37. * Create a Producer to use the given ProducerDb, Face and other values.
  38. *
  39. * A producer can produce data with a naming convention:
  40. * /<prefix>/SAMPLE/<dataType>/[timestamp]
  41. *
  42. * The produced data packet is encrypted with a content key,
  43. * which is stored in the ProducerDb database.
  44. *
  45. * A producer also needs to produce data containing a content key
  46. * encrypted with E-KEYs. A producer can retrieve E-KEYs through the face,
  47. * and will re-try for at most repeatAttemps times when E-KEY retrieval fails.
  48. *
  49. * @param {Name} prefix The producer name prefix. This makes a copy of the Name.
  50. * @param {Name} dataType The dataType portion of the producer name. This makes
  51. * a copy of the Name.
  52. * @param {Face} face The face used to retrieve keys.
  53. * @param {KeyChain} keyChain The keyChain used to sign data packets.
  54. * @param {ProducerDb} database The ProducerDb database for storing keys.
  55. * @param {number} repeatAttempts (optional) The maximum retry for retrieving
  56. * keys. If omitted, use a default value of 3.
  57. * @note This class is an experimental feature. The API may change.
  58. * @constructor
  59. */
  60. var Producer = function Producer
  61. (prefix, dataType, face, keyChain, database, repeatAttempts)
  62. {
  63. this.face_ = face;
  64. this.keyChain_ = keyChain;
  65. this.database_ = database;
  66. this.maxRepeatAttempts_ = (repeatAttempts == undefined ? 3 : repeatAttempts);
  67. // The map key is the key name URI string. The value is an object with fields
  68. // "keyName" and "keyInfo" where "keyName" is the same Name used for the key
  69. // name URI string, and "keyInfo" is the Producer.KeyInfo_.
  70. // (Use a string because we can't use the Name object as the key in JavaScript.)
  71. // (Also put the original Name in the value because we need to iterate over
  72. // eKeyInfo_ and we don't want to rebuild the Name from the name URI string.)
  73. this.eKeyInfo_ = {};
  74. // The map key is the time stamp. The value is a Producer.KeyRequest_.
  75. this.keyRequests_ = {};
  76. var fixedPrefix = new Name(prefix);
  77. var fixedDataType = new Name(dataType);
  78. // Fill ekeyInfo_ with all permutations of dataType, including the 'E-KEY'
  79. // component of the name. This will be used in createContentKey to send
  80. // interests without reconstructing names every time.
  81. fixedPrefix.append(Encryptor.NAME_COMPONENT_READ);
  82. while (fixedDataType.size() > 0) {
  83. var nodeName = new Name(fixedPrefix);
  84. nodeName.append(fixedDataType);
  85. nodeName.append(Encryptor.NAME_COMPONENT_E_KEY);
  86. this.eKeyInfo_[nodeName.toUri()] =
  87. { keyName: nodeName, keyInfo: new Producer.KeyInfo_() };
  88. fixedDataType = fixedDataType.getPrefix(-1);
  89. }
  90. fixedPrefix.append(dataType);
  91. this.namespace_ = new Name(prefix);
  92. this.namespace_.append(Encryptor.NAME_COMPONENT_SAMPLE);
  93. this.namespace_.append(dataType);
  94. };
  95. exports.Producer = Producer;
  96. /**
  97. * Create the content key corresponding to the timeSlot. This first checks if
  98. * the content key exists. For an existing content key, this returns the
  99. * content key name directly. If the key does not exist, this creates one and
  100. * encrypts it using the corresponding E-KEYs. The encrypted content keys are
  101. * passed to the onEncryptedKeys callback.
  102. * @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
  103. * @param {function} onEncryptedKeys If this creates a content key, then this
  104. * calls onEncryptedKeys(keys) where keys is a list of encrypted content key
  105. * Data packets. If onEncryptedKeys is null, this does not use it.
  106. * NOTE: The library will log any exceptions thrown by this callback, but for
  107. * better error handling the callback should catch and properly handle any
  108. * exceptions.
  109. * @param {function} onContentKeyName This calls onContentKeyName(contentKeyName)
  110. * with the content key name for the time slot. If onContentKeyName is null,
  111. * this does not use it. (A callback is needed because of async database
  112. * operations.)
  113. * NOTE: The library will log any exceptions thrown by this callback, but for
  114. * better error handling the callback should catch and properly handle any
  115. * exceptions.
  116. * @param {function} onError (optional) This calls onError(errorCode, message)
  117. * for an error, where errorCode is from EncryptError.ErrorCode and message is a
  118. * string. If omitted, use a default callback which does nothing.
  119. * NOTE: The library will log any exceptions thrown by this callback, but for
  120. * better error handling the callback should catch and properly handle any
  121. * exceptions.
  122. */
  123. Producer.prototype.createContentKey = function
  124. (timeSlot, onEncryptedKeys, onContentKeyName, onError)
  125. {
  126. if (!onError)
  127. onError = Producer.defaultOnError;
  128. var hourSlot = Producer.getRoundedTimeSlot_(timeSlot);
  129. // Create the content key name.
  130. var contentKeyName = new Name(this.namespace_);
  131. contentKeyName.append(Encryptor.NAME_COMPONENT_C_KEY);
  132. contentKeyName.append(Schedule.toIsoString(hourSlot));
  133. var contentKeyBits;
  134. var thisProducer = this;
  135. // Check if we have created the content key before.
  136. this.database_.hasContentKeyPromise(timeSlot)
  137. .then(function(exists) {
  138. if (exists) {
  139. if (onContentKeyName != null)
  140. onContentKeyName(contentKeyName);
  141. return;
  142. }
  143. // We haven't created the content key. Create one and add it into the database.
  144. var aesParams = new AesKeyParams(128);
  145. contentKeyBits = AesAlgorithm.generateKey(aesParams).getKeyBits();
  146. thisProducer.database_.addContentKeyPromise(timeSlot, contentKeyBits)
  147. .then(function() {
  148. // Now we need to retrieve the E-KEYs for content key encryption.
  149. var timeCount = Math.round(timeSlot);
  150. thisProducer.keyRequests_[timeCount] =
  151. new Producer.KeyRequest_(thisProducer.getEKeyInfoSize_());
  152. var keyRequest = thisProducer.keyRequests_[timeCount];
  153. // Check if the current E-KEYs can cover the content key.
  154. var timeRange = new Exclude();
  155. Producer.excludeAfter
  156. (timeRange, new Name.Component(Schedule.toIsoString(timeSlot)));
  157. for (var keyNameUri in thisProducer.eKeyInfo_) {
  158. // For each current E-KEY.
  159. var entry = thisProducer.eKeyInfo_[keyNameUri];
  160. var keyInfo = entry.keyInfo;
  161. if (timeSlot < keyInfo.beginTimeSlot || timeSlot >= keyInfo.endTimeSlot) {
  162. // The current E-KEY cannot cover the content key, so retrieve one.
  163. keyRequest.repeatAttempts[keyNameUri] = 0;
  164. thisProducer.sendKeyInterest_
  165. (new Interest(entry.keyName).setExclude(timeRange).setChildSelector(1),
  166. timeSlot, onEncryptedKeys, onError);
  167. }
  168. else {
  169. // The current E-KEY can cover the content key.
  170. // Encrypt the content key directly.
  171. var eKeyName = new Name(entry.keyName);
  172. eKeyName.append(Schedule.toIsoString(keyInfo.beginTimeSlot));
  173. eKeyName.append(Schedule.toIsoString(keyInfo.endTimeSlot));
  174. thisProducer.encryptContentKeyPromise_
  175. (keyInfo.keyBits, eKeyName, timeSlot, onEncryptedKeys, onError);
  176. }
  177. }
  178. if (onContentKeyName != null)
  179. onContentKeyName(contentKeyName);
  180. });
  181. });
  182. };
  183. /**
  184. * Encrypt the given content with the content key that covers timeSlot, and
  185. * update the data packet with the encrypted content and an appropriate data
  186. * name.
  187. * @param {Data} data An empty Data object which is updated.
  188. * @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
  189. * @param {Blob} content The content to encrypt.
  190. * @param {function} onComplete This calls onComplete() when the data packet has
  191. * been updated.
  192. * NOTE: The library will log any exceptions thrown by this callback, but for
  193. * better error handling the callback should catch and properly handle any
  194. * exceptions.
  195. * @param {function} onError (optional) This calls onError(errorCode, message)
  196. * for an error, where errorCode is from EncryptError.ErrorCode and message is a
  197. * string. If omitted, use a default callback which does nothing.
  198. * NOTE: The library will log any exceptions thrown by this callback, but for
  199. * better error handling the callback should catch and properly handle any
  200. * exceptions.
  201. */
  202. Producer.prototype.produce = function
  203. (data, timeSlot, content, onComplete, onError)
  204. {
  205. if (!onError)
  206. onError = Producer.defaultOnError;
  207. var thisProducer = this;
  208. // Get a content key.
  209. this.createContentKey(timeSlot, null, function(contentKeyName) {
  210. thisProducer.database_.getContentKeyPromise(timeSlot)
  211. .then(function(contentKey) {
  212. // Produce data.
  213. var dataName = new Name(thisProducer.namespace_);
  214. dataName.append(Schedule.toIsoString(timeSlot));
  215. data.setName(dataName);
  216. var params = new EncryptParams(EncryptAlgorithmType.AesCbc, 16);
  217. return Encryptor.encryptData
  218. (data, content, contentKeyName, contentKey, params);
  219. })
  220. .then(function() {
  221. return thisProducer.keyChain_.signPromise(data);
  222. })
  223. .then(function() {
  224. try {
  225. onComplete();
  226. } catch (ex) {
  227. console.log("Error in onComplete: " + NdnCommon.getErrorWithStackTrace(ex));
  228. }
  229. }, function(error) {
  230. try {
  231. onError(EncryptError.ErrorCode.General, "" + error);
  232. } catch (ex) {
  233. console.log("Error in onError: " + NdnCommon.getErrorWithStackTrace(ex));
  234. }
  235. });
  236. }, onError);
  237. };
  238. /**
  239. * The default onError callback which does nothing.
  240. */
  241. Producer.defaultOnError = function(errorCode, message)
  242. {
  243. // Do nothing.
  244. };
  245. Producer.KeyInfo_ = function ProducerKeyInfo()
  246. {
  247. this.beginTimeSlot = 0.0;
  248. this.endTimeSlot = 0.0;
  249. this.keyBits = null; // Blob
  250. };
  251. Producer.KeyRequest_ = function ProducerKeyRequest(interests)
  252. {
  253. this.interestCount = interests; // number
  254. // The map key is the name URI string. The value is an int count.
  255. // (Use a string because we can't use the Name object as the key in JavaScript.)
  256. this.repeatAttempts = {};
  257. this.encryptedKeys = []; // of Data
  258. };
  259. /**
  260. * Round timeSlot to the nearest whole hour, so that we can store content keys
  261. * uniformly (by start of the hour).
  262. * @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
  263. * @return {number} The start of the hour as milliseconds since Jan 1, 1970 UTC.
  264. */
  265. Producer.getRoundedTimeSlot_ = function(timeSlot)
  266. {
  267. return Math.round(Math.floor(Math.round(timeSlot) / 3600000.0) * 3600000.0);
  268. }
  269. /**
  270. * Send an interest with the given name through the face with callbacks to
  271. * handleCoveringKey_ and handleTimeout_.
  272. * @param {Interest} interest The interest to send.
  273. * @param {number} timeSlot The time slot, passed to handleCoveringKey_ and
  274. * handleTimeout_.
  275. * @param {function} onEncryptedKeys The OnEncryptedKeys callback, passed to
  276. * handleCoveringKey_ and handleTimeout_.
  277. * @param {function} onError This calls onError(errorCode, message) for an error.
  278. */
  279. Producer.prototype.sendKeyInterest_ = function
  280. (interest, timeSlot, onEncryptedKeys, onError)
  281. {
  282. var thisProducer = this;
  283. function onKey(interest, data) {
  284. thisProducer.handleCoveringKey_
  285. (interest, data, timeSlot, onEncryptedKeys, onError);
  286. }
  287. function onTimeout(interest) {
  288. thisProducer.handleTimeout_(interest, timeSlot, onEncryptedKeys, onError);
  289. }
  290. this.face_.expressInterest(interest, onKey, onTimeout);
  291. };
  292. /**
  293. * This is called from an expressInterest timeout to update the state of
  294. * keyRequest. Re-express the interest if the number of retrials is less than
  295. * the max limit.
  296. * @param {Interest} interest The timed-out interest.
  297. * @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
  298. * @param {function} onEncryptedKeys When there are no more interests to process,
  299. * this calls onEncryptedKeys(keys) where keys is a list of encrypted content
  300. * key Data packets. If onEncryptedKeys is null, this does not use it.
  301. * @param {function} onError This calls onError(errorCode, message) for an error.
  302. */
  303. Producer.prototype.handleTimeout_ = function
  304. (interest, timeSlot, onEncryptedKeys, onError)
  305. {
  306. var timeCount = Math.round(timeSlot);
  307. var keyRequest = this.keyRequests_[timeCount];
  308. var interestName = interest.getName();
  309. var interestNameUri = interestName.toUri();
  310. if (keyRequest.repeatAttempts[interestNameUri] < this.maxRepeatAttempts_) {
  311. // Increase the retrial count.
  312. ++keyRequest.repeatAttempts[interestNameUri];
  313. this.sendKeyInterest_(interest, timeSlot, onEncryptedKeys, onError);
  314. }
  315. else
  316. // No more retrials.
  317. this.updateKeyRequest_(keyRequest, timeCount, onEncryptedKeys);
  318. };
  319. /**
  320. * Decrease the count of outstanding E-KEY interests for the C-KEY for
  321. * timeCount. If the count decreases to 0, invoke onEncryptedKeys.
  322. * @param {Producer.KeyRequest_} keyRequest The KeyRequest with the
  323. * interestCount to update.
  324. * @param {number} timeCount The time count for indexing keyRequests_.
  325. * @param {function} onEncryptedKeys When there are no more interests to
  326. * process, this calls onEncryptedKeys(keys) where keys is a list of encrypted
  327. * content key Data packets. If onEncryptedKeys is null, this does not use it.
  328. */
  329. Producer.prototype.updateKeyRequest_ = function
  330. (keyRequest, timeCount, onEncryptedKeys)
  331. {
  332. --keyRequest.interestCount;
  333. if (keyRequest.interestCount == 0 && onEncryptedKeys != null) {
  334. try {
  335. onEncryptedKeys(keyRequest.encryptedKeys);
  336. } catch (ex) {
  337. console.log("Error in onEncryptedKeys: " + NdnCommon.getErrorWithStackTrace(ex));
  338. }
  339. delete this.keyRequests_[timeCount];
  340. }
  341. };
  342. /**
  343. * This is called from an expressInterest OnData to check that the encryption
  344. * key contained in data fits the timeSlot. This sends a refined interest if
  345. * required.
  346. * @param {Interest} interest The interest given to expressInterest.
  347. * @param {Data} data The fetched Data packet.
  348. * @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
  349. * @param {function} onEncryptedKeys When there are no more interests to process,
  350. * this calls onEncryptedKeys(keys) where keys is a list of encrypted content
  351. * key Data packets. If onEncryptedKeys is null, this does not use it.
  352. * @param {function} onError This calls onError(errorCode, message) for an error.
  353. */
  354. Producer.prototype.handleCoveringKey_ = function
  355. (interest, data, timeSlot, onEncryptedKeys, onError)
  356. {
  357. var timeCount = Math.round(timeSlot);
  358. var keyRequest = this.keyRequests_[timeCount];
  359. var interestName = interest.getName();
  360. var interestNameUrl = interestName.toUri();
  361. var keyName = data.getName();
  362. var begin = Schedule.fromIsoString
  363. (keyName.get(Producer.START_TIME_STAMP_INDEX).getValue().toString());
  364. var end = Schedule.fromIsoString
  365. (keyName.get(Producer.END_TIME_STAMP_INDEX).getValue().toString());
  366. if (timeSlot >= end) {
  367. // If the received E-KEY covers some earlier period, try to retrieve an
  368. // E-KEY covering a later one.
  369. var timeRange = new Exclude(interest.getExclude());
  370. Producer.excludeBefore(timeRange, keyName.get(Producer.START_TIME_STAMP_INDEX));
  371. keyRequest.repeatAttempts[interestNameUrl] = 0;
  372. this.sendKeyInterest_
  373. (new Interest(interestName).setExclude(timeRange).setChildSelector(1),
  374. timeSlot, onEncryptedKeys, onError);
  375. }
  376. else {
  377. // If the received E-KEY covers the content key, encrypt the content.
  378. var encryptionKey = data.getContent();
  379. var thisProducer = this;
  380. this.encryptContentKeyPromise_
  381. (encryptionKey, keyName, timeSlot, onEncryptedKeys, onError)
  382. .then(function(success) {
  383. if (success) {
  384. var keyInfo = thisProducer.eKeyInfo_[interestNameUrl].keyInfo;
  385. keyInfo.beginTimeSlot = begin;
  386. keyInfo.endTimeSlot = end;
  387. keyInfo.keyBits = encryptionKey;
  388. }
  389. });
  390. }
  391. };
  392. /**
  393. * Get the content key from the database_ and encrypt it for the timeSlot
  394. * using encryptionKey.
  395. * @param {Blob} encryptionKey The encryption key value.
  396. * @param {Name} eKeyName The key name for the EncryptedContent.
  397. * @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
  398. * @param {function} onEncryptedKeys When there are no more interests to process,
  399. * this calls onEncryptedKeys(keys) where keys is a list of encrypted content
  400. * key Data packets. If onEncryptedKeys is null, this does not use it.
  401. * @param {function} onError This calls onError(errorCode, message) for an error.
  402. * @return {Promise} A promise that returns true if encryption succeeds,
  403. * otherwise false.
  404. */
  405. Producer.prototype.encryptContentKeyPromise_ = function
  406. (encryptionKey, eKeyName, timeSlot, onEncryptedKeys, onError)
  407. {
  408. var timeCount = Math.round(timeSlot);
  409. var keyRequest = this.keyRequests_[timeCount];
  410. var keyName = new Name(this.namespace_);
  411. keyName.append(Encryptor.NAME_COMPONENT_C_KEY);
  412. keyName.append(Schedule.toIsoString(Producer.getRoundedTimeSlot_(timeSlot)));
  413. var cKeyData;
  414. var thisProducer = this;
  415. return this.database_.getContentKeyPromise(timeSlot)
  416. .then(function(contentKey) {
  417. cKeyData = new Data();
  418. cKeyData.setName(keyName);
  419. var params = new EncryptParams(EncryptAlgorithmType.RsaOaep);
  420. return Encryptor.encryptDataPromise
  421. (cKeyData, contentKey, eKeyName, encryptionKey, params);
  422. })
  423. .then(function() {
  424. return SyncPromise.resolve(true);
  425. }, function(error) {
  426. try {
  427. onError(EncryptError.ErrorCode.EncryptionFailure,
  428. "encryptData failed: " + error);
  429. } catch (ex) {
  430. console.log("Error in onError: " + NdnCommon.getErrorWithStackTrace(ex));
  431. }
  432. return SyncPromise.resolve(false);
  433. })
  434. .then(function(success) {
  435. if (success) {
  436. return thisProducer.keyChain_.signPromise(cKeyData)
  437. .then(function() {
  438. keyRequest.encryptedKeys.push(cKeyData);
  439. thisProducer.updateKeyRequest_(keyRequest, timeCount, onEncryptedKeys);
  440. return SyncPromise.resolve(true);
  441. });
  442. }
  443. else
  444. return SyncPromise.resolve(false);
  445. });
  446. };
  447. Producer.prototype.getEKeyInfoSize_ = function()
  448. {
  449. // Note: This is really a method to find the key count in any object, but we
  450. // don't want to claim that it is a tested and general utility method.
  451. var size = 0;
  452. for (key in this.eKeyInfo_) {
  453. if (this.eKeyInfo_.hasOwnProperty(key))
  454. ++size;
  455. }
  456. return size;
  457. };
  458. // TODO: Move this to be the main representation inside the Exclude object.
  459. /**
  460. * Create a new ExcludeEntry.
  461. * @param {Name.Component} component
  462. * @param {boolean} anyFollowsComponent
  463. */
  464. Producer.ExcludeEntry = function ExcludeEntry(component, anyFollowsComponent)
  465. {
  466. this.component_ = component;
  467. this.anyFollowsComponent_ = anyFollowsComponent;
  468. };
  469. /**
  470. * Create a list of ExcludeEntry from the Exclude object.
  471. * @param {Exclude} exclude The Exclude object to read.
  472. * @return {Array<ExcludeEntry>} A new array of ExcludeEntry.
  473. */
  474. Producer.getExcludeEntries = function(exclude)
  475. {
  476. var entries = [];
  477. for (var i = 0; i < exclude.size(); ++i) {
  478. if (exclude.get(i) == Exclude.ANY) {
  479. if (entries.length == 0)
  480. // Add a "beginning ANY".
  481. entries.push(new Producer.ExcludeEntry(new Name.Component(), true));
  482. else
  483. // Set anyFollowsComponent of the final component.
  484. entries[entries.length - 1].anyFollowsComponent_ = true;
  485. }
  486. else
  487. entries.push(new Producer.ExcludeEntry(exclude.get(i), false));
  488. }
  489. return entries;
  490. };
  491. /**
  492. * Set the Exclude object from the array of ExcludeEntry.
  493. * @param {Exclude} exclude The Exclude object to update.
  494. * @param {Array<ExcludeEntry>} entries The array of ExcludeEntry.
  495. */
  496. Producer.setExcludeEntries = function(exclude, entries)
  497. {
  498. exclude.clear();
  499. for (var i = 0; i < entries.length; ++i) {
  500. var entry = entries[i];
  501. if (i == 0 && entry.component_.getValue().size() == 0 &&
  502. entry.anyFollowsComponent_)
  503. // This is a "beginning ANY".
  504. exclude.appendAny();
  505. else {
  506. exclude.appendComponent(entry.component_);
  507. if (entry.anyFollowsComponent_)
  508. exclude.appendAny();
  509. }
  510. }
  511. };
  512. /**
  513. * Get the latest entry in the array whose component_ is less than or equal to
  514. * component.
  515. * @param {Array<ExcludeEntry>} entries The array of ExcludeEntry.
  516. * @param {Name.Component} component The component to compare.
  517. * @return {number} The index of the found entry, or -1 if not found.
  518. */
  519. Producer.findEntryBeforeOrAt = function(entries, component)
  520. {
  521. var i = entries.length - 1;
  522. while (i >= 0) {
  523. if (entries[i].component_.compare(component) <= 0)
  524. break;
  525. --i;
  526. }
  527. return i;
  528. };
  529. /**
  530. * Exclude all components in the range beginning at "from".
  531. * @param {Exclude} exclude The Exclude object to update.
  532. * @param {Name.Component} from The first component in the exclude range.
  533. */
  534. Producer.excludeAfter = function(exclude, from)
  535. {
  536. var entries = Producer.getExcludeEntries(exclude);
  537. var iNewFrom;
  538. var iFoundFrom = Producer.findEntryBeforeOrAt(entries, from);
  539. if (iFoundFrom < 0) {
  540. // There is no entry before "from" so insert at the beginning.
  541. entries.splice(0, 0, new Producer.ExcludeEntry(from, true));
  542. iNewFrom = 0;
  543. }
  544. else {
  545. var foundFrom = entries[iFoundFrom];
  546. if (!foundFrom.anyFollowsComponent_) {
  547. if (foundFrom.component_.equals(from)) {
  548. // There is already an entry with "from", so just set the "ANY" flag.
  549. foundFrom.anyFollowsComponent_ = true;
  550. iNewFrom = iFoundFrom;
  551. }
  552. else {
  553. // Insert following the entry before "from".
  554. entries.splice(iFoundFrom + 1, 0, new Producer.ExcludeEntry(from, true));
  555. iNewFrom = iFoundFrom + 1;
  556. }
  557. }
  558. else
  559. // The entry before "from" already has an "ANY" flag, so do nothing.
  560. iNewFrom = iFoundFrom;
  561. }
  562. // Remove intermediate entries since they are inside the range.
  563. var iRemoveBegin = iNewFrom + 1;
  564. var nRemoveNeeded = entries.length - iRemoveBegin;
  565. entries.splice(iRemoveBegin, nRemoveNeeded);
  566. Producer.setExcludeEntries(exclude, entries);
  567. };
  568. /**
  569. * Exclude all components in the range ending at "to".
  570. * @param {Exclude} exclude The Exclude object to update.
  571. * @param {Name.Component} to The last component in the exclude range.
  572. */
  573. Producer.excludeBefore = function(exclude, to)
  574. {
  575. Producer.excludeRange(exclude, new Name.Component(), to);
  576. };
  577. /**
  578. * Exclude all components in the range beginning at "from" and ending at "to".
  579. * @param {Exclude} exclude The Exclude object to update.
  580. * @param {Name.Component} from The first component in the exclude range.
  581. * @param {Name.Component} to The last component in the exclude range.
  582. */
  583. Producer.excludeRange = function(exclude, from, to)
  584. {
  585. if (from.compare(to) >= 0) {
  586. if (from.compare(to) == 0)
  587. throw new Error
  588. ("excludeRange: from == to. To exclude a single component, sue excludeOne.");
  589. else
  590. throw new Error
  591. ("excludeRange: from must be less than to. Invalid range: [" +
  592. from.toEscapedString() + ", " + to.toEscapedString() + "]");
  593. }
  594. var entries = Producer.getExcludeEntries(exclude);
  595. var iNewFrom;
  596. var iFoundFrom = Producer.findEntryBeforeOrAt(entries, from);
  597. if (iFoundFrom < 0) {
  598. // There is no entry before "from" so insert at the beginning.
  599. entries.splice(0, 0, new Producer.ExcludeEntry(from, true));
  600. iNewFrom = 0;
  601. }
  602. else {
  603. var foundFrom = entries[iFoundFrom];
  604. if (!foundFrom.anyFollowsComponent_) {
  605. if (foundFrom.component_.equals(from)) {
  606. // There is already an entry with "from", so just set the "ANY" flag.
  607. foundFrom.anyFollowsComponent_ = true;
  608. iNewFrom = iFoundFrom;
  609. }
  610. else {
  611. // Insert following the entry before "from".
  612. entries.splice(iFoundFrom + 1, 0, new Producer.ExcludeEntry(from, true));
  613. iNewFrom = iFoundFrom + 1;
  614. }
  615. }
  616. else
  617. // The entry before "from" already has an "ANY" flag, so do nothing.
  618. iNewFrom = iFoundFrom;
  619. }
  620. // We have at least one "from" before "to", so we know this will find an entry.
  621. var iFoundTo = Producer.findEntryBeforeOrAt(entries, to);
  622. var foundTo = entries[iFoundTo];
  623. if (iFoundTo == iNewFrom)
  624. // Insert the "to" immediately after the "from".
  625. entries.splice(iNewFrom + 1, 0, new Producer.ExcludeEntry(to, false));
  626. else {
  627. var iRemoveEnd;
  628. if (!foundTo.anyFollowsComponent_) {
  629. if (foundTo.component_.equals(to))
  630. // The "to" entry already exists. Remove up to it.
  631. iRemoveEnd = iFoundTo;
  632. else {
  633. // Insert following the previous entry, which will be removed.
  634. entries.splice(iFoundTo + 1, 0, new Producer.ExcludeEntry(to, false));
  635. iRemoveEnd = iFoundTo + 1;
  636. }
  637. }
  638. else
  639. // "to" follows a component which is already followed by "ANY", meaning
  640. // the new range now encompasses it, so remove the component.
  641. iRemoveEnd = iFoundTo + 1;
  642. // Remove intermediate entries since they are inside the range.
  643. var iRemoveBegin = iNewFrom + 1;
  644. var nRemoveNeeded = iRemoveEnd - iRemoveBegin;
  645. entries.splice(iRemoveBegin, nRemoveNeeded);
  646. }
  647. Producer.setExcludeEntries(exclude, entries);
  648. };
  649. Producer.START_TIME_STAMP_INDEX = -2;
  650. Producer.END_TIME_STAMP_INDEX = -1;