All files / json-pack/src JsonPackMpint.ts

100% Statements 45/45
100% Branches 15/15
100% Functions 5/5
100% Lines 42/42

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116                    4x             49x             32x 4x     28x 28x   28x   14x 14x 14x     14x   14x 34x       14x 2x       14x 14x 41x 41x       14x 5x       28x             32x 4x     28x   28x   14x 14x 32x     14x 14x     14x 14x 46x   14x               4x 1x   3x             4x 4x 1x   3x      
/**
 * Represents an SSH multiprecision integer (mpint).
 *
 * An mpint is stored in two's complement format, 8 bits per byte, MSB first.
 * According to RFC 4251:
 * - Negative numbers have the value 1 as the most significant bit of the first byte
 * - If the most significant bit would be set for a positive number, the number MUST be preceded by a zero byte
 * - Unnecessary leading bytes with the value 0 or 255 MUST NOT be included
 * - The value zero MUST be stored as a string with zero bytes of data
 */
export class JsonPackMpint {
  /**
   * The raw bytes representing the mpint in two's complement format, MSB first.
   */
  public readonly data: Uint8Array;
 
  constructor(data: Uint8Array) {
    this.data = data;
  }
 
  /**
   * Create an mpint from a BigInt value.
   */
  public static fromBigInt(value: bigint): JsonPackMpint {
    if (value === BigInt(0)) {
      return new JsonPackMpint(new Uint8Array(0));
    }
 
    const negative = value < BigInt(0);
    const bytes: number[] = [];
 
    if (negative) {
      // For negative numbers, work with two's complement
      const absValue = -value;
      const bitLength = absValue.toString(2).length;
      const byteLength = Math.ceil((bitLength + 1) / 8); // +1 for sign bit
 
      // Calculate two's complement
      const twoComplement = (BigInt(1) << BigInt(byteLength * 8)) + value;
 
      for (let i = byteLength - 1; i >= 0; i--) {
        bytes.push(Number((twoComplement >> BigInt(i * 8)) & BigInt(0xff)));
      }
 
      // Ensure MSB is 1 for negative numbers
      while (bytes.length > 0 && bytes[0] === 0xff && bytes.length > 1 && (bytes[1] & 0x80) !== 0) {
        bytes.shift();
      }
    } else {
      // For positive numbers
      let tempValue = value;
      while (tempValue > BigInt(0)) {
        bytes.unshift(Number(tempValue & BigInt(0xff)));
        tempValue >>= BigInt(8);
      }
 
      // Add leading zero if MSB is set (to indicate positive number)
      if (bytes[0] & 0x80) {
        bytes.unshift(0);
      }
    }
 
    return new JsonPackMpint(new Uint8Array(bytes));
  }
 
  /**
   * Convert the mpint to a BigInt value.
   */
  public toBigInt(): bigint {
    if (this.data.length === 0) {
      return BigInt(0);
    }
 
    const negative = (this.data[0] & 0x80) !== 0;
 
    if (negative) {
      // Two's complement for negative numbers
      let value = BigInt(0);
      for (let i = 0; i < this.data.length; i++) {
        value = (value << BigInt(8)) | BigInt(this.data[i]);
      }
      // Convert from two's complement
      const bitLength = this.data.length * 8;
      return value - (BigInt(1) << BigInt(bitLength));
    } else {
      // Positive number
      let value = BigInt(0);
      for (let i = 0; i < this.data.length; i++) {
        value = (value << BigInt(8)) | BigInt(this.data[i]);
      }
      return value;
    }
  }
 
  /**
   * Create an mpint from a number (limited to safe integer range).
   */
  public static fromNumber(value: number): JsonPackMpint {
    if (!Number.isInteger(value)) {
      throw new Error('Value must be an integer');
    }
    return JsonPackMpint.fromBigInt(BigInt(value));
  }
 
  /**
   * Convert the mpint to a number (throws if out of safe integer range).
   */
  public toNumber(): number {
    const bigIntValue = this.toBigInt();
    if (bigIntValue > BigInt(Number.MAX_SAFE_INTEGER) || bigIntValue < BigInt(Number.MIN_SAFE_INTEGER)) {
      throw new Error('Value is outside safe integer range');
    }
    return Number(bigIntValue);
  }
}