Source: encrypt/schedule.js

/**
 * Copyright (C) 2015-2016 Regents of the University of California.
 * @author: Jeff Thompson <[email protected]>
 * @author: From ndn-group-encrypt src/schedule 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 Interval = require('./interval.js').Interval; /** @ignore */
var RepetitiveInterval = require('./repetitive-interval.js').RepetitiveInterval; /** @ignore */
var Tlv = require('../encoding/tlv/tlv.js').Tlv; /** @ignore */
var TlvEncoder = require('../encoding/tlv/tlv-encoder.js').TlvEncoder; /** @ignore */
var TlvDecoder = require('../encoding/tlv/tlv-decoder.js').TlvDecoder; /** @ignore */
var Blob = require('../util/blob.js').Blob;

/**
 * Schedule is used to manage the times when a member can access data using two
 * sets of RepetitiveInterval as follows. whiteIntervalList is an ordered
 * set for the times a member is allowed to access to data, and
 * blackIntervalList is for the times a member is not allowed.
 * Create a Schedule with one of these forms:
 * Schedule() A Schedule with empty whiteIntervalList and blackIntervalList.
 * Schedule(schedule). A copy of the given schedule.
 * @note This class is an experimental feature. The API may change.
 * @constructor
 */
var Schedule = function Schedule(value)
{
  if (typeof value === 'object' && value instanceof Schedule) {
    // Make a copy.
    var schedule = value;

    // RepetitiveInterval is immutable, so we don't need to make a deep copy.
    this.whiteIntervalList_ = schedule.whiteIntervalList_.slice(0);
    this.blackIntervalList_ = schedule.blackIntervalList_.slice(0);
  }
  else {
    // The default constructor.
    this.whiteIntervalList_ = [];
    this.blackIntervalList_ = [];
  }
};

exports.Schedule = Schedule;

/**
 * Add the repetitiveInterval to the whiteIntervalList.
 * @param {RepetitiveInterval} repetitiveInterval The RepetitiveInterval to add.
 * If the list already contains the same RepetitiveInterval, this does nothing.
 * @return {Schedule} This Schedule so you can chain calls to add.
 */
Schedule.prototype.addWhiteInterval = function(repetitiveInterval)
{
  // RepetitiveInterval is immutable, so we don't need to make a copy.
  Schedule.sortedSetAdd_(this.whiteIntervalList_, repetitiveInterval);
  return this;
};

/**
 * Add the repetitiveInterval to the blackIntervalList.
 * @param {RepetitiveInterval} repetitiveInterval The RepetitiveInterval to add.
 * If the list already contains the same RepetitiveInterval, this does nothing.
 * @return {Schedule} This Schedule so you can chain calls to add.
 */
Schedule.prototype.addBlackInterval = function(repetitiveInterval)
{
  // RepetitiveInterval is immutable, so we don't need to make a copy.
  Schedule.sortedSetAdd_(this.blackIntervalList_, repetitiveInterval);
  return this;
};

/**
 * Get the interval that covers the time stamp. This iterates over the two
 * repetitive interval sets and find the shortest interval that allows a group
 * member to access the data. If there is no interval covering the time stamp,
 * this returns false for isPositive and a negative interval.
 * @param {number} timeStamp The time stamp as milliseconds since Jan 1, 1970 UTC.
 * @returns {object} An associative array with fields
 * (isPositive, interval) where
 * isPositive is true if the returned interval is positive or false if negative,
 * and interval is the Interval covering the time stamp, or a negative interval
 * if not found.
 */
Schedule.prototype.getCoveringInterval = function(timeStamp)
{
  var blackPositiveResult = new Interval(true);
  var whitePositiveResult = new Interval(true);

  var blackNegativeResult = new Interval();
  var whiteNegativeResult = new Interval();

  // Get the black result.
  Schedule.calculateIntervalResult_
    (this.blackIntervalList_, timeStamp, blackPositiveResult, blackNegativeResult);

  // If the black positive result is not empty, then isPositive must be false.
  if (!blackPositiveResult.isEmpty())
    return { isPositive: false, interval: blackPositiveResult };

  // Get the whiteResult.
  Schedule.calculateIntervalResult_
    (this.whiteIntervalList_, timeStamp, whitePositiveResult, whiteNegativeResult);

  if (whitePositiveResult.isEmpty() && !whiteNegativeResult.isValid()) {
    // There is no white interval covering the time stamp.
    // Return false and a 24-hour interval.
    var timeStampDateOnly =
      RepetitiveInterval.toDateOnlyMilliseconds_(timeStamp);
    return { isPositive: false,
             interval:  new Interval
               (timeStampDateOnly,
                timeStampDateOnly + RepetitiveInterval.MILLISECONDS_IN_DAY) };
  }

  if (!whitePositiveResult.isEmpty()) {
    // There is white interval covering the time stamp.
    // Return true and calculate the intersection.
    if (blackNegativeResult.isValid())
      return { isPositive: true,
               interval: whitePositiveResult.intersectWith(blackNegativeResult) };
    else
      return  { isPositive: true, interval: whitePositiveResult };
  }
  else
    // There is no white interval covering the time stamp.
    // Return false.
    return { isPositive: false, interval: whiteNegativeResult };
};

/**
 * Encode this Schedule.
 * @return {Blob} The encoded buffer.
 */
Schedule.prototype.wireEncode = function()
{
  // For now, don't use WireFormat and hardcode to use TLV since the encoding
  // doesn't go out over the wire, only into the local SQL database.
  var encoder = new TlvEncoder(256);
  var saveLength = encoder.getLength();

  // Encode backwards.
  // Encode the blackIntervalList.
  var saveLengthForList = encoder.getLength();
  for (var i = this.blackIntervalList_.length - 1; i >= 0; i--)
    Schedule.encodeRepetitiveInterval_(this.blackIntervalList_[i], encoder);
  encoder.writeTypeAndLength
    (Tlv.Encrypt_BlackIntervalList, encoder.getLength() - saveLengthForList);

  // Encode the whiteIntervalList.
  saveLengthForList = encoder.getLength();
  for (var i = this.whiteIntervalList_.length - 1; i >= 0; i--)
    Schedule.encodeRepetitiveInterval_(this.whiteIntervalList_[i], encoder);
  encoder.writeTypeAndLength
    (Tlv.Encrypt_WhiteIntervalList, encoder.getLength() - saveLengthForList);

  encoder.writeTypeAndLength
    (Tlv.Encrypt_Schedule, encoder.getLength() - saveLength);

  return new Blob(encoder.getOutput(), false);
};

/**
 * Decode the input and update this Schedule object.
 * @param {Blob|Buffer} input The input buffer to decode. For Buffer, this reads
 * from position() to limit(), but does not change the position.
 * @throws EncodingException For invalid encoding.
 */
Schedule.prototype.wireDecode = function(input)
{
  // If input is a blob, get its buf().
  var decodeBuffer = typeof input === 'object' && input instanceof Blob ?
    input.buf() : input;

  // For now, don't use WireFormat and hardcode to use TLV since the encoding
  // doesn't go out over the wire, only into the local SQL database.
  var decoder = new TlvDecoder(decodeBuffer);

  var endOffset = decoder.readNestedTlvsStart(Tlv.Encrypt_Schedule);

  // Decode the whiteIntervalList.
  this.whiteIntervalList_ = [];
  var listEndOffset = decoder.readNestedTlvsStart(Tlv.Encrypt_WhiteIntervalList);
  while (decoder.getOffset() < listEndOffset)
    Schedule.sortedSetAdd_
      (this.whiteIntervalList_, Schedule.decodeRepetitiveInterval_(decoder));
  decoder.finishNestedTlvs(listEndOffset);

  // Decode the blackIntervalList.
  this.blackIntervalList_ = [];
  listEndOffset = decoder.readNestedTlvsStart(Tlv.Encrypt_BlackIntervalList);
  while (decoder.getOffset() < listEndOffset)
    Schedule.sortedSetAdd_
      (this.blackIntervalList_, Schedule.decodeRepetitiveInterval_(decoder));
  decoder.finishNestedTlvs(listEndOffset);

  decoder.finishNestedTlvs(endOffset);
};

/**
 * Insert element into the list, sorted using element.compare(). If it is a
 * duplicate of an existing list element, don't add it.
 */
Schedule.sortedSetAdd_ = function(list, element)
{
  // Find the index of the first element where it is not less than element.
  var i = 0;
  while (i < list.length) {
    var comparison = list[i].compare(element);
    if (comparison == 0)
      // Don't add a duplicate.
      return;
    if (!(comparison < 0))
      break;

    ++i;
  }

  list.splice(i, 0, element);
};

/**
 * Encode the RepetitiveInterval as NDN-TLV to the encoder.
 * @param {RepetitiveInterval} repetitiveInterval The RepetitiveInterval to encode.
 * @param {TlvEncoder} encoder The TlvEncoder to receive the encoding.
 */
Schedule.encodeRepetitiveInterval_ = function(repetitiveInterval, encoder)
{
  var saveLength = encoder.getLength();

  // Encode backwards.
  // The RepeatUnit enum has the same values as the encoding.
  encoder.writeNonNegativeIntegerTlv
    (Tlv.Encrypt_RepeatUnit, repetitiveInterval.getRepeatUnit());
  encoder.writeNonNegativeIntegerTlv
    (Tlv.Encrypt_NRepeats, repetitiveInterval.getNRepeats());
  encoder.writeNonNegativeIntegerTlv
    (Tlv.Encrypt_IntervalEndHour, repetitiveInterval.getIntervalEndHour());
  encoder.writeNonNegativeIntegerTlv
    (Tlv.Encrypt_IntervalStartHour, repetitiveInterval.getIntervalStartHour());
  // Use Blob to convert the string to UTF8 encoding.
  encoder.writeBlobTlv(Tlv.Encrypt_EndDate,
    new Blob(Schedule.toIsoString(repetitiveInterval.getEndDate())).buf());
  encoder.writeBlobTlv(Tlv.Encrypt_StartDate,
    new Blob(Schedule.toIsoString(repetitiveInterval.getStartDate())).buf());

  encoder.writeTypeAndLength
    (Tlv.Encrypt_RepetitiveInterval, encoder.getLength() - saveLength);
};

/**
 * Decode the input as an NDN-TLV RepetitiveInterval.
 * @param {TlvDecoder} decoder The decoder with the input to decode.
 * @return {RepetitiveInterval} A new RepetitiveInterval with the decoded result.
 */
Schedule.decodeRepetitiveInterval_ = function(decoder)
{
  var endOffset = decoder.readNestedTlvsStart(Tlv.Encrypt_RepetitiveInterval);

  // Use Blob to convert UTF8 to a string.
  var startDate = Schedule.fromIsoString
    (new Blob(decoder.readBlobTlv(Tlv.Encrypt_StartDate), true).toString());
  var endDate = Schedule.fromIsoString
    (new Blob(decoder.readBlobTlv(Tlv.Encrypt_EndDate), true).toString());
  var startHour = decoder.readNonNegativeIntegerTlv(Tlv.Encrypt_IntervalStartHour);
  var endHour = decoder.readNonNegativeIntegerTlv(Tlv.Encrypt_IntervalEndHour);
  var nRepeats = decoder.readNonNegativeIntegerTlv(Tlv.Encrypt_NRepeats);

  // The RepeatUnit enum has the same values as the encoding.
  var repeatUnit = decoder.readNonNegativeIntegerTlv(Tlv.Encrypt_RepeatUnit);

  decoder.finishNestedTlvs(endOffset);
  return new RepetitiveInterval
    (startDate, endDate, startHour, endHour, nRepeats, repeatUnit);
};

/**
 * A helper function to calculate black interval results or white interval
 * results.
 * @param {Array} list The set of RepetitiveInterval, which can be the white
 * list or the black list.
 * @param {number} timeStamp The time stamp as milliseconds since Jan 1, 1970 UTC.
 * @param {Interval} positiveResult The positive result which is updated.
 * @param {Interval} negativeResult The negative result which is updated.
 */
Schedule.calculateIntervalResult_ = function
  (list, timeStamp, positiveResult, negativeResult)
{
  for (var i = 0; i < list.length; ++i) {
    var element = list[i];

    var result = element.getInterval(timeStamp);
    var tempInterval = result.interval;
    if (result.isPositive == true)
      positiveResult.unionWith(tempInterval);
    else {
      if (!negativeResult.isValid())
        negativeResult.set(tempInterval);
      else
        negativeResult.intersectWith(tempInterval);
    }
  }
};

/**
 * Convert a UNIX timestamp to ISO time representation with the "T" in the middle.
 * @param {number} msSince1970 Timestamp as milliseconds since Jan 1, 1970 UTC.
 * @returns {string} The string representation.
 */
Schedule.toIsoString = function(msSince1970)
{
  var utcTime = new Date(Math.round(msSince1970));
  return utcTime.getUTCFullYear() +
         Schedule.to2DigitString(utcTime.getUTCMonth() + 1) +
         Schedule.to2DigitString(utcTime.getUTCDate()) +
         "T" +
         Schedule.to2DigitString(utcTime.getUTCHours()) +
         Schedule.to2DigitString(utcTime.getUTCMinutes()) +
         Schedule.to2DigitString(utcTime.getUTCSeconds());
};

/**
 * A private method to zero pad an integer to 2 digits.
 * @param {number} x The number to pad.  Assume it is a non-negative integer.
 * @returns {string} The padded string.
 */
Schedule.to2DigitString = function(x)
{
  var result = x.toString();
  return result.length === 1 ? "0" + result : result;
};

/**
 * Convert an ISO time representation with the "T" in the middle to a UNIX
 * timestamp.
 * @param {string} timeString The ISO time representation.
 * @returns {number} The timestamp as milliseconds since Jan 1, 1970 UTC.
 */
Schedule.fromIsoString = function(timeString)
{
  if (timeString.length != 15 || timeString.substr(8, 1) != 'T')
    throw new Error("fromIsoString: Format is not the expected yyyymmddThhmmss");

  return Date.UTC
    (parseInt(timeString.substr(0, 4)),
     parseInt(timeString.substr(4, 2) - 1),
     parseInt(timeString.substr(6, 2)),
     parseInt(timeString.substr(9, 2)),
     parseInt(timeString.substr(11, 2)),
     parseInt(timeString.substr(13, 2)));
};