/**
* Copyright (C) 2015-2016 Regents of the University of California.
* @author: Jeff Thompson <jefft0@remap.ucla.edu>
* @author: From ndn-group-encrypt src/producer https://github.com/named-data/ndn-group-encrypt
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* A copy of the GNU Lesser General Public License is in the file COPYING.
*/
/** @ignore */
var Name = require('../name.js').Name; /** @ignore */
var Interest = require('../interest.js').Interest; /** @ignore */
var Data = require('../data.js').Data; /** @ignore */
var Exclude = require('../exclude.js').Exclude; /** @ignore */
var Encryptor = require('./algo/encryptor.js').Encryptor; /** @ignore */
var EncryptParams = require('./algo/encrypt-params.js').EncryptParams; /** @ignore */
var EncryptAlgorithmType = require('./algo/encrypt-params.js').EncryptAlgorithmType; /** @ignore */
var AesKeyParams = require('../security/key-params.js').AesKeyParams; /** @ignore */
var AesAlgorithm = require('./algo/aes-algorithm.js').AesAlgorithm; /** @ignore */
var Schedule = require('./schedule.js').Schedule; /** @ignore */
var EncryptError = require('./encrypt-error.js').EncryptError; /** @ignore */
var NdnCommon = require('../util/ndn-common.js').NdnCommon; /** @ignore */
var SyncPromise = require('../util/sync-promise.js').SyncPromise;
/**
* A Producer manages content keys used to encrypt a data packet in the
* group-based encryption protocol.
* Create a Producer to use the given ProducerDb, Face and other values.
*
* A producer can produce data with a naming convention:
* /<prefix>/SAMPLE/<dataType>/[timestamp]
*
* The produced data packet is encrypted with a content key,
* which is stored in the ProducerDb database.
*
* A producer also needs to produce data containing a content key
* encrypted with E-KEYs. A producer can retrieve E-KEYs through the face,
* and will re-try for at most repeatAttemps times when E-KEY retrieval fails.
*
* @param {Name} prefix The producer name prefix. This makes a copy of the Name.
* @param {Name} dataType The dataType portion of the producer name. This makes
* a copy of the Name.
* @param {Face} face The face used to retrieve keys.
* @param {KeyChain} keyChain The keyChain used to sign data packets.
* @param {ProducerDb} database The ProducerDb database for storing keys.
* @param {number} repeatAttempts (optional) The maximum retry for retrieving
* keys. If omitted, use a default value of 3.
* @note This class is an experimental feature. The API may change.
* @constructor
*/
var Producer = function Producer
(prefix, dataType, face, keyChain, database, repeatAttempts)
{
this.face_ = face;
this.keyChain_ = keyChain;
this.database_ = database;
this.maxRepeatAttempts_ = (repeatAttempts == undefined ? 3 : repeatAttempts);
// The map key is the key name URI string. The value is an object with fields
// "keyName" and "keyInfo" where "keyName" is the same Name used for the key
// name URI string, and "keyInfo" is the Producer.KeyInfo_.
// (Use a string because we can't use the Name object as the key in JavaScript.)
// (Also put the original Name in the value because we need to iterate over
// eKeyInfo_ and we don't want to rebuild the Name from the name URI string.)
this.eKeyInfo_ = {};
// The map key is the time stamp. The value is a Producer.KeyRequest_.
this.keyRequests_ = {};
var fixedPrefix = new Name(prefix);
var fixedDataType = new Name(dataType);
// Fill ekeyInfo_ with all permutations of dataType, including the 'E-KEY'
// component of the name. This will be used in createContentKey to send
// interests without reconstructing names every time.
fixedPrefix.append(Encryptor.NAME_COMPONENT_READ);
while (fixedDataType.size() > 0) {
var nodeName = new Name(fixedPrefix);
nodeName.append(fixedDataType);
nodeName.append(Encryptor.NAME_COMPONENT_E_KEY);
this.eKeyInfo_[nodeName.toUri()] =
{ keyName: nodeName, keyInfo: new Producer.KeyInfo_() };
fixedDataType = fixedDataType.getPrefix(-1);
}
fixedPrefix.append(dataType);
this.namespace_ = new Name(prefix);
this.namespace_.append(Encryptor.NAME_COMPONENT_SAMPLE);
this.namespace_.append(dataType);
};
exports.Producer = Producer;
/**
* Create the content key corresponding to the timeSlot. This first checks if
* the content key exists. For an existing content key, this returns the
* content key name directly. If the key does not exist, this creates one and
* encrypts it using the corresponding E-KEYs. The encrypted content keys are
* passed to the onEncryptedKeys callback.
* @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
* @param {function} onEncryptedKeys If this creates a content key, then this
* calls onEncryptedKeys(keys) where keys is a list of encrypted content key
* Data packets. If onEncryptedKeys is null, this does not use it.
* NOTE: The library will log any exceptions thrown by this callback, but for
* better error handling the callback should catch and properly handle any
* exceptions.
* @param {function} onContentKeyName This calls onContentKeyName(contentKeyName)
* with the content key name for the time slot. If onContentKeyName is null,
* this does not use it. (A callback is needed because of async database
* operations.)
* NOTE: The library will log any exceptions thrown by this callback, but for
* better error handling the callback should catch and properly handle any
* exceptions.
* @param {function} onError (optional) This calls onError(errorCode, message)
* for an error, where errorCode is from EncryptError.ErrorCode and message is a
* string. If omitted, use a default callback which does nothing.
* NOTE: The library will log any exceptions thrown by this callback, but for
* better error handling the callback should catch and properly handle any
* exceptions.
*/
Producer.prototype.createContentKey = function
(timeSlot, onEncryptedKeys, onContentKeyName, onError)
{
if (!onError)
onError = Producer.defaultOnError;
var hourSlot = Producer.getRoundedTimeSlot_(timeSlot);
// Create the content key name.
var contentKeyName = new Name(this.namespace_);
contentKeyName.append(Encryptor.NAME_COMPONENT_C_KEY);
contentKeyName.append(Schedule.toIsoString(hourSlot));
var contentKeyBits;
var thisProducer = this;
// Check if we have created the content key before.
this.database_.hasContentKeyPromise(timeSlot)
.then(function(exists) {
if (exists) {
if (onContentKeyName != null)
onContentKeyName(contentKeyName);
return;
}
// We haven't created the content key. Create one and add it into the database.
var aesParams = new AesKeyParams(128);
contentKeyBits = AesAlgorithm.generateKey(aesParams).getKeyBits();
thisProducer.database_.addContentKeyPromise(timeSlot, contentKeyBits)
.then(function() {
// Now we need to retrieve the E-KEYs for content key encryption.
var timeCount = Math.round(timeSlot);
thisProducer.keyRequests_[timeCount] =
new Producer.KeyRequest_(thisProducer.getEKeyInfoSize_());
var keyRequest = thisProducer.keyRequests_[timeCount];
// Check if the current E-KEYs can cover the content key.
var timeRange = new Exclude();
Producer.excludeAfter
(timeRange, new Name.Component(Schedule.toIsoString(timeSlot)));
for (var keyNameUri in thisProducer.eKeyInfo_) {
// For each current E-KEY.
var entry = thisProducer.eKeyInfo_[keyNameUri];
var keyInfo = entry.keyInfo;
if (timeSlot < keyInfo.beginTimeSlot || timeSlot >= keyInfo.endTimeSlot) {
// The current E-KEY cannot cover the content key, so retrieve one.
keyRequest.repeatAttempts[keyNameUri] = 0;
thisProducer.sendKeyInterest_
(new Interest(entry.keyName).setExclude(timeRange).setChildSelector(1),
timeSlot, onEncryptedKeys, onError);
}
else {
// The current E-KEY can cover the content key.
// Encrypt the content key directly.
var eKeyName = new Name(entry.keyName);
eKeyName.append(Schedule.toIsoString(keyInfo.beginTimeSlot));
eKeyName.append(Schedule.toIsoString(keyInfo.endTimeSlot));
thisProducer.encryptContentKeyPromise_
(keyInfo.keyBits, eKeyName, timeSlot, onEncryptedKeys, onError);
}
}
if (onContentKeyName != null)
onContentKeyName(contentKeyName);
});
});
};
/**
* Encrypt the given content with the content key that covers timeSlot, and
* update the data packet with the encrypted content and an appropriate data
* name.
* @param {Data} data An empty Data object which is updated.
* @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
* @param {Blob} content The content to encrypt.
* @param {function} onComplete This calls onComplete() when the data packet has
* been updated.
* NOTE: The library will log any exceptions thrown by this callback, but for
* better error handling the callback should catch and properly handle any
* exceptions.
* @param {function} onError (optional) This calls onError(errorCode, message)
* for an error, where errorCode is from EncryptError.ErrorCode and message is a
* string. If omitted, use a default callback which does nothing.
* NOTE: The library will log any exceptions thrown by this callback, but for
* better error handling the callback should catch and properly handle any
* exceptions.
*/
Producer.prototype.produce = function
(data, timeSlot, content, onComplete, onError)
{
if (!onError)
onError = Producer.defaultOnError;
var thisProducer = this;
// Get a content key.
this.createContentKey(timeSlot, null, function(contentKeyName) {
thisProducer.database_.getContentKeyPromise(timeSlot)
.then(function(contentKey) {
// Produce data.
var dataName = new Name(thisProducer.namespace_);
dataName.append(Schedule.toIsoString(timeSlot));
data.setName(dataName);
var params = new EncryptParams(EncryptAlgorithmType.AesCbc, 16);
return Encryptor.encryptData
(data, content, contentKeyName, contentKey, params);
})
.then(function() {
return thisProducer.keyChain_.signPromise(data);
})
.then(function() {
try {
onComplete();
} catch (ex) {
console.log("Error in onComplete: " + NdnCommon.getErrorWithStackTrace(ex));
}
}, function(error) {
try {
onError(EncryptError.ErrorCode.General, "" + error);
} catch (ex) {
console.log("Error in onError: " + NdnCommon.getErrorWithStackTrace(ex));
}
});
}, onError);
};
/**
* The default onError callback which does nothing.
*/
Producer.defaultOnError = function(errorCode, message)
{
// Do nothing.
};
Producer.KeyInfo_ = function ProducerKeyInfo()
{
this.beginTimeSlot = 0.0;
this.endTimeSlot = 0.0;
this.keyBits = null; // Blob
};
Producer.KeyRequest_ = function ProducerKeyRequest(interests)
{
this.interestCount = interests; // number
// The map key is the name URI string. The value is an int count.
// (Use a string because we can't use the Name object as the key in JavaScript.)
this.repeatAttempts = {};
this.encryptedKeys = []; // of Data
};
/**
* Round timeSlot to the nearest whole hour, so that we can store content keys
* uniformly (by start of the hour).
* @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
* @return {number} The start of the hour as milliseconds since Jan 1, 1970 UTC.
*/
Producer.getRoundedTimeSlot_ = function(timeSlot)
{
return Math.round(Math.floor(Math.round(timeSlot) / 3600000.0) * 3600000.0);
}
/**
* Send an interest with the given name through the face with callbacks to
* handleCoveringKey_ and handleTimeout_.
* @param {Interest} interest The interest to send.
* @param {number} timeSlot The time slot, passed to handleCoveringKey_ and
* handleTimeout_.
* @param {function} onEncryptedKeys The OnEncryptedKeys callback, passed to
* handleCoveringKey_ and handleTimeout_.
* @param {function} onError This calls onError(errorCode, message) for an error.
*/
Producer.prototype.sendKeyInterest_ = function
(interest, timeSlot, onEncryptedKeys, onError)
{
var thisProducer = this;
function onKey(interest, data) {
thisProducer.handleCoveringKey_
(interest, data, timeSlot, onEncryptedKeys, onError);
}
function onTimeout(interest) {
thisProducer.handleTimeout_(interest, timeSlot, onEncryptedKeys, onError);
}
this.face_.expressInterest(interest, onKey, onTimeout);
};
/**
* This is called from an expressInterest timeout to update the state of
* keyRequest. Re-express the interest if the number of retrials is less than
* the max limit.
* @param {Interest} interest The timed-out interest.
* @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
* @param {function} onEncryptedKeys When there are no more interests to process,
* this calls onEncryptedKeys(keys) where keys is a list of encrypted content
* key Data packets. If onEncryptedKeys is null, this does not use it.
* @param {function} onError This calls onError(errorCode, message) for an error.
*/
Producer.prototype.handleTimeout_ = function
(interest, timeSlot, onEncryptedKeys, onError)
{
var timeCount = Math.round(timeSlot);
var keyRequest = this.keyRequests_[timeCount];
var interestName = interest.getName();
var interestNameUri = interestName.toUri();
if (keyRequest.repeatAttempts[interestNameUri] < this.maxRepeatAttempts_) {
// Increase the retrial count.
++keyRequest.repeatAttempts[interestNameUri];
this.sendKeyInterest_(interest, timeSlot, onEncryptedKeys, onError);
}
else
// No more retrials.
this.updateKeyRequest_(keyRequest, timeCount, onEncryptedKeys);
};
/**
* Decrease the count of outstanding E-KEY interests for the C-KEY for
* timeCount. If the count decreases to 0, invoke onEncryptedKeys.
* @param {Producer.KeyRequest_} keyRequest The KeyRequest with the
* interestCount to update.
* @param {number} timeCount The time count for indexing keyRequests_.
* @param {function} onEncryptedKeys When there are no more interests to
* process, this calls onEncryptedKeys(keys) where keys is a list of encrypted
* content key Data packets. If onEncryptedKeys is null, this does not use it.
*/
Producer.prototype.updateKeyRequest_ = function
(keyRequest, timeCount, onEncryptedKeys)
{
--keyRequest.interestCount;
if (keyRequest.interestCount == 0 && onEncryptedKeys != null) {
try {
onEncryptedKeys(keyRequest.encryptedKeys);
} catch (ex) {
console.log("Error in onEncryptedKeys: " + NdnCommon.getErrorWithStackTrace(ex));
}
delete this.keyRequests_[timeCount];
}
};
/**
* This is called from an expressInterest OnData to check that the encryption
* key contained in data fits the timeSlot. This sends a refined interest if
* required.
* @param {Interest} interest The interest given to expressInterest.
* @param {Data} data The fetched Data packet.
* @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
* @param {function} onEncryptedKeys When there are no more interests to process,
* this calls onEncryptedKeys(keys) where keys is a list of encrypted content
* key Data packets. If onEncryptedKeys is null, this does not use it.
* @param {function} onError This calls onError(errorCode, message) for an error.
*/
Producer.prototype.handleCoveringKey_ = function
(interest, data, timeSlot, onEncryptedKeys, onError)
{
var timeCount = Math.round(timeSlot);
var keyRequest = this.keyRequests_[timeCount];
var interestName = interest.getName();
var interestNameUrl = interestName.toUri();
var keyName = data.getName();
var begin = Schedule.fromIsoString
(keyName.get(Producer.START_TIME_STAMP_INDEX).getValue().toString());
var end = Schedule.fromIsoString
(keyName.get(Producer.END_TIME_STAMP_INDEX).getValue().toString());
if (timeSlot >= end) {
// If the received E-KEY covers some earlier period, try to retrieve an
// E-KEY covering a later one.
var timeRange = new Exclude(interest.getExclude());
Producer.excludeBefore(timeRange, keyName.get(Producer.START_TIME_STAMP_INDEX));
keyRequest.repeatAttempts[interestNameUrl] = 0;
this.sendKeyInterest_
(new Interest(interestName).setExclude(timeRange).setChildSelector(1),
timeSlot, onEncryptedKeys, onError);
}
else {
// If the received E-KEY covers the content key, encrypt the content.
var encryptionKey = data.getContent();
var thisProducer = this;
this.encryptContentKeyPromise_
(encryptionKey, keyName, timeSlot, onEncryptedKeys, onError)
.then(function(success) {
if (success) {
var keyInfo = thisProducer.eKeyInfo_[interestNameUrl].keyInfo;
keyInfo.beginTimeSlot = begin;
keyInfo.endTimeSlot = end;
keyInfo.keyBits = encryptionKey;
}
});
}
};
/**
* Get the content key from the database_ and encrypt it for the timeSlot
* using encryptionKey.
* @param {Blob} encryptionKey The encryption key value.
* @param {Name} eKeyName The key name for the EncryptedContent.
* @param {number} timeSlot The time slot as milliseconds since Jan 1, 1970 UTC.
* @param {function} onEncryptedKeys When there are no more interests to process,
* this calls onEncryptedKeys(keys) where keys is a list of encrypted content
* key Data packets. If onEncryptedKeys is null, this does not use it.
* @param {function} onError This calls onError(errorCode, message) for an error.
* @return {Promise} A promise that returns true if encryption succeeds,
* otherwise false.
*/
Producer.prototype.encryptContentKeyPromise_ = function
(encryptionKey, eKeyName, timeSlot, onEncryptedKeys, onError)
{
var timeCount = Math.round(timeSlot);
var keyRequest = this.keyRequests_[timeCount];
var keyName = new Name(this.namespace_);
keyName.append(Encryptor.NAME_COMPONENT_C_KEY);
keyName.append(Schedule.toIsoString(Producer.getRoundedTimeSlot_(timeSlot)));
var cKeyData;
var thisProducer = this;
return this.database_.getContentKeyPromise(timeSlot)
.then(function(contentKey) {
cKeyData = new Data();
cKeyData.setName(keyName);
var params = new EncryptParams(EncryptAlgorithmType.RsaOaep);
return Encryptor.encryptDataPromise
(cKeyData, contentKey, eKeyName, encryptionKey, params);
})
.then(function() {
return SyncPromise.resolve(true);
}, function(error) {
try {
onError(EncryptError.ErrorCode.EncryptionFailure,
"encryptData failed: " + error);
} catch (ex) {
console.log("Error in onError: " + NdnCommon.getErrorWithStackTrace(ex));
}
return SyncPromise.resolve(false);
})
.then(function(success) {
if (success) {
return thisProducer.keyChain_.signPromise(cKeyData)
.then(function() {
keyRequest.encryptedKeys.push(cKeyData);
thisProducer.updateKeyRequest_(keyRequest, timeCount, onEncryptedKeys);
return SyncPromise.resolve(true);
});
}
else
return SyncPromise.resolve(false);
});
};
Producer.prototype.getEKeyInfoSize_ = function()
{
// Note: This is really a method to find the key count in any object, but we
// don't want to claim that it is a tested and general utility method.
var size = 0;
for (key in this.eKeyInfo_) {
if (this.eKeyInfo_.hasOwnProperty(key))
++size;
}
return size;
};
// TODO: Move this to be the main representation inside the Exclude object.
/**
* Create a new ExcludeEntry.
* @param {Name.Component} component
* @param {boolean} anyFollowsComponent
*/
Producer.ExcludeEntry = function ExcludeEntry(component, anyFollowsComponent)
{
this.component_ = component;
this.anyFollowsComponent_ = anyFollowsComponent;
};
/**
* Create a list of ExcludeEntry from the Exclude object.
* @param {Exclude} exclude The Exclude object to read.
* @return {Array<ExcludeEntry>} A new array of ExcludeEntry.
*/
Producer.getExcludeEntries = function(exclude)
{
var entries = [];
for (var i = 0; i < exclude.size(); ++i) {
if (exclude.get(i) == Exclude.ANY) {
if (entries.length == 0)
// Add a "beginning ANY".
entries.push(new Producer.ExcludeEntry(new Name.Component(), true));
else
// Set anyFollowsComponent of the final component.
entries[entries.length - 1].anyFollowsComponent_ = true;
}
else
entries.push(new Producer.ExcludeEntry(exclude.get(i), false));
}
return entries;
};
/**
* Set the Exclude object from the array of ExcludeEntry.
* @param {Exclude} exclude The Exclude object to update.
* @param {Array<ExcludeEntry>} entries The array of ExcludeEntry.
*/
Producer.setExcludeEntries = function(exclude, entries)
{
exclude.clear();
for (var i = 0; i < entries.length; ++i) {
var entry = entries[i];
if (i == 0 && entry.component_.getValue().size() == 0 &&
entry.anyFollowsComponent_)
// This is a "beginning ANY".
exclude.appendAny();
else {
exclude.appendComponent(entry.component_);
if (entry.anyFollowsComponent_)
exclude.appendAny();
}
}
};
/**
* Get the latest entry in the array whose component_ is less than or equal to
* component.
* @param {Array<ExcludeEntry>} entries The array of ExcludeEntry.
* @param {Name.Component} component The component to compare.
* @return {number} The index of the found entry, or -1 if not found.
*/
Producer.findEntryBeforeOrAt = function(entries, component)
{
var i = entries.length - 1;
while (i >= 0) {
if (entries[i].component_.compare(component) <= 0)
break;
--i;
}
return i;
};
/**
* Exclude all components in the range beginning at "from".
* @param {Exclude} exclude The Exclude object to update.
* @param {Name.Component} from The first component in the exclude range.
*/
Producer.excludeAfter = function(exclude, from)
{
var entries = Producer.getExcludeEntries(exclude);
var iNewFrom;
var iFoundFrom = Producer.findEntryBeforeOrAt(entries, from);
if (iFoundFrom < 0) {
// There is no entry before "from" so insert at the beginning.
entries.splice(0, 0, new Producer.ExcludeEntry(from, true));
iNewFrom = 0;
}
else {
var foundFrom = entries[iFoundFrom];
if (!foundFrom.anyFollowsComponent_) {
if (foundFrom.component_.equals(from)) {
// There is already an entry with "from", so just set the "ANY" flag.
foundFrom.anyFollowsComponent_ = true;
iNewFrom = iFoundFrom;
}
else {
// Insert following the entry before "from".
entries.splice(iFoundFrom + 1, 0, new Producer.ExcludeEntry(from, true));
iNewFrom = iFoundFrom + 1;
}
}
else
// The entry before "from" already has an "ANY" flag, so do nothing.
iNewFrom = iFoundFrom;
}
// Remove intermediate entries since they are inside the range.
var iRemoveBegin = iNewFrom + 1;
var nRemoveNeeded = entries.length - iRemoveBegin;
entries.splice(iRemoveBegin, nRemoveNeeded);
Producer.setExcludeEntries(exclude, entries);
};
/**
* Exclude all components in the range ending at "to".
* @param {Exclude} exclude The Exclude object to update.
* @param {Name.Component} to The last component in the exclude range.
*/
Producer.excludeBefore = function(exclude, to)
{
Producer.excludeRange(exclude, new Name.Component(), to);
};
/**
* Exclude all components in the range beginning at "from" and ending at "to".
* @param {Exclude} exclude The Exclude object to update.
* @param {Name.Component} from The first component in the exclude range.
* @param {Name.Component} to The last component in the exclude range.
*/
Producer.excludeRange = function(exclude, from, to)
{
if (from.compare(to) >= 0) {
if (from.compare(to) == 0)
throw new Error
("excludeRange: from == to. To exclude a single component, sue excludeOne.");
else
throw new Error
("excludeRange: from must be less than to. Invalid range: [" +
from.toEscapedString() + ", " + to.toEscapedString() + "]");
}
var entries = Producer.getExcludeEntries(exclude);
var iNewFrom;
var iFoundFrom = Producer.findEntryBeforeOrAt(entries, from);
if (iFoundFrom < 0) {
// There is no entry before "from" so insert at the beginning.
entries.splice(0, 0, new Producer.ExcludeEntry(from, true));
iNewFrom = 0;
}
else {
var foundFrom = entries[iFoundFrom];
if (!foundFrom.anyFollowsComponent_) {
if (foundFrom.component_.equals(from)) {
// There is already an entry with "from", so just set the "ANY" flag.
foundFrom.anyFollowsComponent_ = true;
iNewFrom = iFoundFrom;
}
else {
// Insert following the entry before "from".
entries.splice(iFoundFrom + 1, 0, new Producer.ExcludeEntry(from, true));
iNewFrom = iFoundFrom + 1;
}
}
else
// The entry before "from" already has an "ANY" flag, so do nothing.
iNewFrom = iFoundFrom;
}
// We have at least one "from" before "to", so we know this will find an entry.
var iFoundTo = Producer.findEntryBeforeOrAt(entries, to);
var foundTo = entries[iFoundTo];
if (iFoundTo == iNewFrom)
// Insert the "to" immediately after the "from".
entries.splice(iNewFrom + 1, 0, new Producer.ExcludeEntry(to, false));
else {
var iRemoveEnd;
if (!foundTo.anyFollowsComponent_) {
if (foundTo.component_.equals(to))
// The "to" entry already exists. Remove up to it.
iRemoveEnd = iFoundTo;
else {
// Insert following the previous entry, which will be removed.
entries.splice(iFoundTo + 1, 0, new Producer.ExcludeEntry(to, false));
iRemoveEnd = iFoundTo + 1;
}
}
else
// "to" follows a component which is already followed by "ANY", meaning
// the new range now encompasses it, so remove the component.
iRemoveEnd = iFoundTo + 1;
// Remove intermediate entries since they are inside the range.
var iRemoveBegin = iNewFrom + 1;
var nRemoveNeeded = iRemoveEnd - iRemoveBegin;
entries.splice(iRemoveBegin, nRemoveNeeded);
}
Producer.setExcludeEntries(exclude, entries);
};
Producer.START_TIME_STAMP_INDEX = -2;
Producer.END_TIME_STAMP_INDEX = -1;