// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

/**
 * @fileoverview The SSH wire protocol defined by RFC4251 (Section 5).
 *               https://tools.ietf.org/html/rfc4251#section-5
 */

// Do everything with Typed arrays.
//   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays
// The DataView helper has native 8/16/32 int methods!  Need to add 64 int.
//   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView
//   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView#64-bit_Integer_Values
// TextDecoder can be used to convert typed arrays to JS strings.
//   https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
// TextEncoder can be used to convert JS strings to typed arrays.
//   https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder

// Realloc
//   https://github.com/domenic/proposal-arraybuffer-transfer/#arraybufferprototypetransfer

nassh.wire = {};

/**
 * Given a typed array, get a DataView of it.
 *
 * This handles typed arrays that don't point to the start of their internal
 * buffers.  This is a common enough idiom in our code base to warrant a simple
 * helper.
 */
nassh.wire.toDataView = function(array) {
  return new DataView(array.buffer, array.byteOffset, array.byteLength);
};

nassh.wire.utf16CodeUnits = function(string) {
  return string.split('').map((ch) => ch.charCodeAt(0));
};

nassh.wire.Packet = function({data=undefined, length=0}={}) {
  this.data_ = data;
  this.offset_ = 0;
};

nassh.wire.textDecoder_ = new TextDecoder();
nassh.wire.textEncoder_ = new TextEncoder();

nassh.wire.String = function(string) {
  if (ArrayBuffer.isView(string)) {
    this.buffer = string;
    this.utf8_ = undefined;
  } else {
    this.setUtf8(string);
  }
};

nassh.wire.String.prototype.getUtf8 = function() {
  if (!this.utf8_) {
    this.utf8_ = nassh.wire.textDecoder_.decode(this.buffer);
  }
  return this.utf8_;
};

nassh.wire.String.prototype.setUtf8 = function(utf8) {
  this.utf8_ = utf8;
  this.buffer = nassh.wire.textEncoder_.encode(utf8);
};

nassh.wire.String.prototype.toString = function() {
  return this.getUtf8();
};

nassh.wire.PacketView = class extends DataView {
  getUint8Array(begin=0, end=undefined) {
    const u8 = new Uint8Array(this.buffer);
    return u8.subarray(begin, end);
  }
  setUint8Array(array, offset=undefined) {
    const u8 = new Uint8Array(this.buffer);
    u8.set(array, offset);
  }

  getByte(byteOffset=0) {
    return this.getUint8(byteOffset);
  }
  setByte(byteOffset=0, value=0) {
    this.setUint8(byteOffset, value);
  }

  getBoolean(byteOffset=0) {
    return this.getUint8(byteOffset) != 0;
  }
  setBoolean(byteOffset=0, value=0) {
    this.setUint8(byteOffset, value != 0);
  }

  getUint64(byteOffset=0) {
    const hi32 = this.getUint32(byteOffset);
    const lo32 = this.getUint32(byteOffset + 4);
    let ret = lo32;

    if (hi32) {
      ret = lo32 + (hi32 * 0x100000000);
      if (ret > Number.MAX_SAFE_INTEGER) {
        throw new RangeError(`integer is too large: ${ret}`);
      }
    }

    return ret;
  }
  setUint64(byteOffset=0, value=0) {
    if (value < 0x100000000) {
      this.setUint32(offset, 0);
    } else {
      if (value > Number.MAX_SAFE_INTEGER) {
        throw new RangeError(`integer is too large: ${value}`);
      }
      this.setUint32(byteOffset, value / 0x100000000);
    }
    this.setUint32(byteOffset + 4, value);
  }

  getString(byteOffset=0) {
    const size = this.getUint32(byteOffset);
    byteOffset += 4;

    const data = this.getUint8Array(byteOffset, byteOffset + size);
    return new nassh.wire.String(data);
  }
  setString(byteOffset=0, value) {
    const size = value.length;
    this.setUint32(byteOffset, size);
    byteOffset += 4;

    this.setUint8Array(value, byteOffset);
  }

  // TODO: Doesn't work as getString returns a nassh.wire.String().
  getNameList(byteOffset=0) {
    const names = this.getString(byteOffset);
    return names.split(',');
  }
  setNameList(byteOffset=0, value) {
  }

  // TODO: Finish this.
  getMpInt(byteOffset=0) {
    const mpint = this.getString();
  }
  setMpInt(byteOffset=0, value) {
    const data = this.getString();
  }
};

nassh.wire.TYPES = {
  'byte': {
    'size': 1,
    'get': (view, offset=0) => [view.getUint8(offset), 1],
    'set': (view, value, offset=0) => {
      view.setUint8(offset, value);
      return 1;
    },
  },

  'boolean': {
    'size': 1,
    'get': (view, offset=0) => [view.getUint8(offset) != 0, 1],
    'set': (view, value, offset=0) => {
      view.setUint8(offset, value != 0);
      return 1;
    },
  },

  'uint32': {
    'size': 4,
    'get': (view, offset=0) => [view.getUint32(offset), 4],
    'set': (view, value, offset=0) => {
      view.setUint32(offset, value);
      return 4;
    },
  },

  'uint64': {
    'size': 8,
    'get': (view, offset=0) => {
      const hi32 = v.getUint32(offset);
      const lo32 = v.getUint32(offset + 4);
      let ret = lo32;

      if (hi32) {
        ret = lo32 + (hi32 * 0x100000000);
        if (ret > Number.MAX_SAFE_INTEGER) {
          throw new RangeError(`integer is too large: ${ret}`);
        }
      }

      return [ret, 8];
    },
    'set': (view, value, offset=0) => {
      if (value < 0x100000000) {
        view.setUint32(offset, 0);
      } else {
        if (value > Number.MAX_SAFE_INTEGER) {
          throw new RangeError(`integer is too large: ${value}`);
        }
        view.setUint32(offset, value / 0x100000000);
      }
      view.setUint32(offset + 4, value);
      return 8;
    },
  },

  'string': {
    'size': 4,
    'get': (view, offset=0) => {
      const size = view.getUint32(offset);
      offset += 4;

      const u8 = new Uint8Array(view.buffer);
      const subarray = u8.subarray(offset, offset + size);
      return [new nassh.wire.String(subarray), size + 4];
    },
    'set': (view, string, offset=0) => {
      let typedarray;
      if (ArrayBuffer.isView(string)) {
        typedarray = string;
      } else {
        // Assume nassh.wire.String.
        typedarray = string.buffer;
      }

      const size = typedarray.length;
      view.setUint32(offset, size);
      offset += 4;

      const u8 = new Uint8Array(view.buffer);
      u8.set(typedarray, offset);

      return size + 4;
    },
  },
};

nassh.wire.Struct = function(...fields) {
  this.fields_ = [];
  this.staticSize_ = 0;
  fields.forEach(([fieldName, typeName]) => {
    const type = nassh.wire.TYPES[typeName];
    this.fields_.push([fieldName, type]);
    this.staticSize_ += type.size;
  });
};

nassh.wire.Struct.prototype.get = function(buffer, offset=0) {
  const view = nassh.wire.toDataView(buffer);
  const ret = {};
  let size;

  this.fields_.forEach(([name, type]) => {
    [ret[name], size] = type.get(view, offset);
    offset += size;
  });

  return [ret, offset];
};

nassh.wire.Struct.prototype.set = function(obj, buffer, offset=0) {
  const view = nassh.wire.toDataView(buffer);
  let size;

  this.fields_.forEach(([name, type]) => {
    size = type.set(view, obj[name], offset);
    offset += size;
  });

  return offset;
};



(function(exports) {
const dec = new TextDecoder();
const enc = new TextEncoder();
const rechk = /^([<>])?(([1-9]\d*)?([xcbB?hHiIqQfdsSp]))*$/;
const refmt = /([1-9]\d*)?([xcbB?hHiIqQfdsSp])/g;
const str = (v,o,c) => dec.decode(new Uint8Array(v.buffer, v.byteOffset + o, c));
const rts = (v,o,c,s) => { new Uint8Array(v.buffer, v.byteOffset + o, c).set(enc.encode(s)); };
const pst = (v,o,c) => str(v, o + 1, Math.min(v.getUint8(o), c - 1));
const tsp = (v,o,c,s) => { v.setUint8(o, s.length); rts(v, o + 1, c - 1, s); };
const ustring = (v,o,c) => str(v, o + 4, v.getUint32(o));
const pstring = (v,o,c,s) => { v.setUint32(o, s.length); rts(v, o + 4, s.length, s); };
const getUint64 = (v,o,le) => {
  const hi32 = v.getUint32(0, le);
  const lo32 = v.getUint32(4, le);
  if (hi32) {
    if (hi32 > 0xfffff)
      throw errval;
    return lo32 + (hi32 * 0x100000000);
  } else {
    return lo32;
  }
};
const setUint64 = (v,o,q,le) => {
  if (q < 0x100000000)
    v.setUint32(0, 0, le);
  else
    v.setUint32(0, q / 0x100000000, le);
  v.setUint32(4, q, le);
};
const getInt64 = getUint64;
const setInt64 = setUint64;
const lut = le => ({
    x: c=>[1,c,0],
    c: c=>[c,1,o=>({u:v=>str(v, o, 1)      , p:(v,c)=>rts(v, o, 1, c)     })],
    '?': c=>[c,1,o=>({u:v=>Boolean(v.getUint8(o)),p:(v,B)=>v.setUint8(o,B)})],
    b: c=>[c,1,o=>({u:v=>v.getInt8(   o   ), p:(v,b)=>v.setInt8(   o,b   )})],
    B: c=>[c,1,o=>({u:v=>v.getUint8(  o   ), p:(v,B)=>v.setUint8(  o,B   )})],
    h: c=>[c,2,o=>({u:v=>v.getInt16(  o,le), p:(v,h)=>v.setInt16(  o,h,le)})],
    H: c=>[c,2,o=>({u:v=>v.getUint16( o,le), p:(v,H)=>v.setUint16( o,H,le)})],
    i: c=>[c,4,o=>({u:v=>v.getInt32(  o,le), p:(v,i)=>v.setInt32(  o,i,le)})],
    I: c=>[c,4,o=>({u:v=>v.getUint32( o,le), p:(v,I)=>v.setUint32( o,I,le)})],
    q: c=>[c,8,o=>({u:v=>getInt64(v,o,le),   p:(v,q)=>setInt64(  v,o,q,le)})],
    Q: c=>[c,8,o=>({u:v=>getUint64(v,o,le),  p:(v,Q)=>setUint64( v,o,Q,le)})],
    f: c=>[c,4,o=>({u:v=>v.getFloat32(o,le), p:(v,f)=>v.setFloat32(o,f,le)})],
    d: c=>[c,8,o=>({u:v=>v.getFloat64(o,le), p:(v,d)=>v.setFloat64(o,d,le)})],
    s: c=>[1,c,o=>({u:v=>str(v,o,c), p:(v,s)=>rts(v,o,c,s.slice(0,c    ) )})],
    p: c=>[1,c,o=>({u:v=>pst(v,o,c), p:(v,s)=>tsp(v,o,c,s.slice(0,c - 1) )})],
    S: c=>[1,4,o=>({u:v=>ustring(v,o,c),     p:(v,s)=>pstring(v,o,c,s    ),s:(s)=>s.length})],
});
const errbuf = new RangeError("Structure larger than remaining buffer");
const errval = new RangeError("Not enough values for structure");
const struct = format => {
    let fns = [], size = 0, m = rechk.exec(format);
    if (!m) { throw new RangeError("Invalid format string"); }
    const t = lut('<' === m[1]);
    const lu = (n, c) => t[c](n ? parseInt(n, 10) : 1);
    while ((m = refmt.exec(format))) {
        ((r, s, f) => {
            for (let i = 0; i < r; ++i, size += s) { if (f) {fns.push(f(size));} }
        })(...lu(...m.slice(1)));
    }
    const unpack_from = (arrb, offs) => {
        // We don't check the size as reading past the array buffer will throw
        // an error for us already.  This way we can support dynamic strings too.
        //if (arrb.byteLength < (offs|0) + size) { throw errbuf; };
        let v = new DataView(arrb, offs|0);
        return fns.map(f => f.u(v));
    };
    const pack_into = (arrb, offs, ...values) => {
        if (values.length < fns.length) { throw errval; }
        if (arrb.byteLength < offs + size) { throw errbuf; }
        const v = new DataView(arrb, offs);
        new Uint8Array(arrb, offs, size);
        fns.forEach((f, i) => f.p(v, values[i]));
    };
    const pack = (...values) => {
        const oldSize = size;
        fns.forEach((f, i) => {
          if (f.s) { size += f.s(values[i]); }
        });
        let b = new ArrayBuffer(size);
        pack_into(b, 0, ...values);
        size = oldSize;
        return b;
    };
    const unpack = arrb => unpack_from(arrb, 0);
    function* iter_unpack(arrb) {
        for (let offs = 0; offs + size <= arrb.byteLength; offs += size) {
            yield unpack_from(arrb, offs);
        }
    }
    return Object.freeze({
        unpack, pack, unpack_from, pack_into, iter_unpack, format, size});
};

//exports.struct = struct;
})(nassh.wire);

/*
const pack = (format, ...values) => struct(format).pack(...values)
const unpack = (format, buffer) => struct(format).unpack(buffer)
const pack_into = (format, arrb, offs, ...values) =>
    struct(format).pack_into(arrb, offs, ...values)
const unpack_from = (format, arrb, offset) =>
    struct(format).unpack_from(arrb, offset)
const iter_unpack = (format, arrb) => struct(format).iter_unpack(arrb)
const calcsize = format => struct(format).size
module.exports = {
    struct, pack, unpack, pack_into, unpack_from, iter_unpack, calcsize }
*/
