Add @cosmjs/encoding and @cosmjs/math

This commit is contained in:
Simon Warta
2020-06-09 17:46:03 +02:00
parent 4cca97651b
commit 3cfad7a541
53 changed files with 2176 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
import { fromAscii, toAscii } from "./ascii";
describe("ascii", () => {
it("encodes to ascii", () => {
expect(toAscii("")).toEqual(new Uint8Array([]));
expect(toAscii("abc")).toEqual(new Uint8Array([0x61, 0x62, 0x63]));
expect(toAscii(" ?=-n|~+-*/\\")).toEqual(
new Uint8Array([0x20, 0x3f, 0x3d, 0x2d, 0x6e, 0x7c, 0x7e, 0x2b, 0x2d, 0x2a, 0x2f, 0x5c]),
);
expect(() => toAscii("ö")).toThrow();
expect(() => toAscii("ß")).toThrow();
});
it("decodes from ascii", () => {
expect(fromAscii(new Uint8Array([]))).toEqual("");
expect(fromAscii(new Uint8Array([0x61, 0x62, 0x63]))).toEqual("abc");
expect(
fromAscii(new Uint8Array([0x20, 0x3f, 0x3d, 0x2d, 0x6e, 0x7c, 0x7e, 0x2b, 0x2d, 0x2a, 0x2f, 0x5c])),
).toEqual(" ?=-n|~+-*/\\");
expect(() => fromAscii(new Uint8Array([0x00]))).toThrow();
expect(() => fromAscii(new Uint8Array([0x7f]))).toThrow();
expect(() => fromAscii(new Uint8Array([0xff]))).toThrow();
});
});
+31
View File
@@ -0,0 +1,31 @@
export function toAscii(input: string): Uint8Array {
const toNums = (str: string): readonly number[] =>
str.split("").map((x: string) => {
const charCode = x.charCodeAt(0);
// 0x000x1F control characters
// 0x200x7E printable characters
// 0x7F delete character
// 0x800xFF out of 7 bit ascii range
if (charCode < 0x20 || charCode > 0x7e) {
throw new Error("Cannot encode character that is out of printable ASCII range: " + charCode);
}
return charCode;
});
return Uint8Array.from(toNums(input));
}
export function fromAscii(data: Uint8Array): string {
const fromNums = (listOfNumbers: readonly number[]): readonly string[] =>
listOfNumbers.map((x: number): string => {
// 0x000x1F control characters
// 0x200x7E printable characters
// 0x7F delete character
// 0x800xFF out of 7 bit ascii range
if (x < 0x20 || x > 0x7e) {
throw new Error("Cannot decode character that is out of printable ASCII range: " + x);
}
return String.fromCharCode(x);
});
return fromNums(Array.from(data)).join("");
}
+65
View File
@@ -0,0 +1,65 @@
import { fromBase64, toBase64 } from "./base64";
describe("base64", () => {
it("encodes to base64", () => {
expect(toBase64(new Uint8Array([]))).toEqual("");
expect(toBase64(new Uint8Array([0x00]))).toEqual("AA==");
expect(toBase64(new Uint8Array([0x00, 0x00]))).toEqual("AAA=");
expect(toBase64(new Uint8Array([0x00, 0x00, 0x00]))).toEqual("AAAA");
expect(toBase64(new Uint8Array([0x00, 0x00, 0x00, 0x00]))).toEqual("AAAAAA==");
expect(toBase64(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00]))).toEqual("AAAAAAA=");
expect(toBase64(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]))).toEqual("AAAAAAAA");
expect(toBase64(new Uint8Array([0x61]))).toEqual("YQ==");
expect(toBase64(new Uint8Array([0x62]))).toEqual("Yg==");
expect(toBase64(new Uint8Array([0x63]))).toEqual("Yw==");
expect(toBase64(new Uint8Array([0x61, 0x62, 0x63]))).toEqual("YWJj");
});
it("decodes from base64", () => {
expect(fromBase64("")).toEqual(new Uint8Array([]));
expect(fromBase64("AA==")).toEqual(new Uint8Array([0x00]));
expect(fromBase64("AAA=")).toEqual(new Uint8Array([0x00, 0x00]));
expect(fromBase64("AAAA")).toEqual(new Uint8Array([0x00, 0x00, 0x00]));
expect(fromBase64("AAAAAA==")).toEqual(new Uint8Array([0x00, 0x00, 0x00, 0x00]));
expect(fromBase64("AAAAAAA=")).toEqual(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00]));
expect(fromBase64("AAAAAAAA")).toEqual(new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
expect(fromBase64("YQ==")).toEqual(new Uint8Array([0x61]));
expect(fromBase64("Yg==")).toEqual(new Uint8Array([0x62]));
expect(fromBase64("Yw==")).toEqual(new Uint8Array([0x63]));
expect(fromBase64("YWJj")).toEqual(new Uint8Array([0x61, 0x62, 0x63]));
// invalid length
expect(() => fromBase64("a")).toThrow();
expect(() => fromBase64("aa")).toThrow();
expect(() => fromBase64("aaa")).toThrow();
// proper length including invalid character
expect(() => fromBase64("aaa!")).toThrow();
expect(() => fromBase64("aaa*")).toThrow();
expect(() => fromBase64("aaaä")).toThrow();
// proper length plus invalid character
expect(() => fromBase64("aaaa!")).toThrow();
expect(() => fromBase64("aaaa*")).toThrow();
expect(() => fromBase64("aaaaä")).toThrow();
// extra spaces
expect(() => fromBase64("aaaa ")).toThrow();
expect(() => fromBase64(" aaaa")).toThrow();
expect(() => fromBase64("aa aa")).toThrow();
expect(() => fromBase64("aaaa\n")).toThrow();
expect(() => fromBase64("\naaaa")).toThrow();
expect(() => fromBase64("aa\naa")).toThrow();
// position of =
expect(() => fromBase64("=aaa")).toThrow();
expect(() => fromBase64("==aa")).toThrow();
// concatenated base64 strings should not be supported
// see https://github.com/beatgammit/base64-js/issues/42
expect(() => fromBase64("AAA=AAA=")).toThrow();
// wrong number of =
expect(() => fromBase64("a===")).toThrow();
});
});
+12
View File
@@ -0,0 +1,12 @@
import * as base64js from "base64-js";
export function toBase64(data: Uint8Array): string {
return base64js.fromByteArray(data);
}
export function fromBase64(base64String: string): Uint8Array {
if (!base64String.match(/^[a-zA-Z0-9+/]*={0,2}$/)) {
throw new Error("Invalid base64 string format");
}
return base64js.toByteArray(base64String);
}
+19
View File
@@ -0,0 +1,19 @@
import { Bech32 } from "./bech32";
import { fromHex } from "./hex";
describe("Bech32", () => {
// test data generate using https://github.com/nym-zone/bech32
// bech32 -e -h eth 9d4e856e572e442f0a4b2763e72d08a0e99d8ded
const ethAddressRaw = fromHex("9d4e856e572e442f0a4b2763e72d08a0e99d8ded");
it("encodes", () => {
expect(Bech32.encode("eth", ethAddressRaw)).toEqual("eth1n48g2mjh9ezz7zjtya37wtgg5r5emr0drkwlgw");
});
it("decodes", () => {
expect(Bech32.decode("eth1n48g2mjh9ezz7zjtya37wtgg5r5emr0drkwlgw")).toEqual({
prefix: "eth",
data: ethAddressRaw,
});
});
});
+16
View File
@@ -0,0 +1,16 @@
import * as bech32 from "bech32";
export class Bech32 {
public static encode(prefix: string, data: Uint8Array): string {
const address = bech32.encode(prefix, bech32.toWords(data));
return address;
}
public static decode(address: string): { readonly prefix: string; readonly data: Uint8Array } {
const decodedAddress = bech32.decode(address);
return {
prefix: decodedAddress.prefix,
data: new Uint8Array(bech32.fromWords(decodedAddress.words)),
};
}
}
+44
View File
@@ -0,0 +1,44 @@
import { fromHex, toHex } from "./hex";
describe("fromHex", () => {
it("works", () => {
// simple
expect(fromHex("")).toEqual(new Uint8Array([]));
expect(fromHex("00")).toEqual(new Uint8Array([0x00]));
expect(fromHex("01")).toEqual(new Uint8Array([0x01]));
expect(fromHex("10")).toEqual(new Uint8Array([0x10]));
expect(fromHex("11")).toEqual(new Uint8Array([0x11]));
expect(fromHex("112233")).toEqual(new Uint8Array([0x11, 0x22, 0x33]));
expect(fromHex("0123456789abcdef")).toEqual(
new Uint8Array([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
);
// capital letters
expect(fromHex("AA")).toEqual(new Uint8Array([0xaa]));
expect(fromHex("aAbBcCdDeEfF")).toEqual(new Uint8Array([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]));
// error
expect(() => fromHex("a")).toThrow();
expect(() => fromHex("aaa")).toThrow();
expect(() => fromHex("a!")).toThrow();
expect(() => fromHex("a ")).toThrow();
expect(() => fromHex("aa ")).toThrow();
expect(() => fromHex(" aa")).toThrow();
expect(() => fromHex("a a")).toThrow();
expect(() => fromHex("gg")).toThrow();
});
});
describe("toHex", () => {
it("works", () => {
expect(toHex(new Uint8Array([]))).toEqual("");
expect(toHex(new Uint8Array([0x00]))).toEqual("00");
expect(toHex(new Uint8Array([0x01]))).toEqual("01");
expect(toHex(new Uint8Array([0x10]))).toEqual("10");
expect(toHex(new Uint8Array([0x11]))).toEqual("11");
expect(toHex(new Uint8Array([0x11, 0x22, 0x33]))).toEqual("112233");
expect(toHex(new Uint8Array([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]))).toEqual(
"0123456789abcdef",
);
});
});
+23
View File
@@ -0,0 +1,23 @@
export function toHex(data: Uint8Array): string {
let out = "";
for (const byte of data) {
out += ("0" + byte.toString(16)).slice(-2);
}
return out;
}
export function fromHex(hexstring: string): Uint8Array {
if (hexstring.length % 2 !== 0) {
throw new Error("hex string length must be a multiple of 2");
}
const listOfInts: number[] = [];
for (let i = 0; i < hexstring.length; i += 2) {
const hexByteAsString = hexstring.substr(i, 2);
if (!hexByteAsString.match(/[0-9a-f]{2}/i)) {
throw new Error("hex string contains invalid characters");
}
listOfInts.push(parseInt(hexByteAsString, 16));
}
return new Uint8Array(listOfInts);
}
+6
View File
@@ -0,0 +1,6 @@
export { fromAscii, toAscii } from "./ascii";
export { fromBase64, toBase64 } from "./base64";
export { Bech32 } from "./bech32";
export { fromHex, toHex } from "./hex";
export { fromRfc3339, toRfc3339 } from "./rfc3339";
export { fromUtf8, toUtf8 } from "./utf8";
+178
View File
@@ -0,0 +1,178 @@
import { fromRfc3339, toRfc3339 } from "./rfc3339";
describe("RFC3339", () => {
it("parses dates with different time zones", () => {
// time zone +/- 0
expect(fromRfc3339("2002-10-02T11:12:13+00:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13)));
expect(fromRfc3339("2002-10-02T11:12:13-00:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13)));
expect(fromRfc3339("2002-10-02T11:12:13Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13)));
// time zone positive (full hours)
expect(fromRfc3339("2002-10-02T11:12:13+01:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 - 1, 12, 13)));
expect(fromRfc3339("2002-10-02T11:12:13+02:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 - 2, 12, 13)));
expect(fromRfc3339("2002-10-02T11:12:13+03:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 - 3, 12, 13)));
expect(fromRfc3339("2002-10-02T11:12:13+11:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 - 11, 12, 13)));
// time zone negative (full hours)
expect(fromRfc3339("2002-10-02T11:12:13-01:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 + 1, 12, 13)));
expect(fromRfc3339("2002-10-02T11:12:13-02:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 + 2, 12, 13)));
expect(fromRfc3339("2002-10-02T11:12:13-03:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 + 3, 12, 13)));
expect(fromRfc3339("2002-10-02T11:12:13-11:00")).toEqual(new Date(Date.UTC(2002, 9, 2, 11 + 11, 12, 13)));
// time zone positive (minutes only)
expect(fromRfc3339("2002-10-02T11:12:13+00:01")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12 - 1, 13)));
expect(fromRfc3339("2002-10-02T11:12:13+00:30")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12 - 30, 13)));
expect(fromRfc3339("2002-10-02T11:12:13+00:45")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12 - 45, 13)));
// time zone negative (minutes only)
expect(fromRfc3339("2002-10-02T11:12:13-00:01")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12 + 1, 13)));
expect(fromRfc3339("2002-10-02T11:12:13-00:30")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12 + 30, 13)));
expect(fromRfc3339("2002-10-02T11:12:13-00:45")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12 + 45, 13)));
// time zone positive (hours and minutes)
expect(fromRfc3339("2002-10-02T11:12:13+01:01")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11 - 1, 12 - 1, 13)),
);
expect(fromRfc3339("2002-10-02T11:12:13+04:30")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11 - 4, 12 - 30, 13)),
);
expect(fromRfc3339("2002-10-02T11:12:13+10:20")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11 - 10, 12 - 20, 13)),
);
// time zone negative (hours and minutes)
expect(fromRfc3339("2002-10-02T11:12:13-01:01")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11 + 1, 12 + 1, 13)),
);
expect(fromRfc3339("2002-10-02T11:12:13-04:30")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11 + 4, 12 + 30, 13)),
);
expect(fromRfc3339("2002-10-02T11:12:13-10:20")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11 + 10, 12 + 20, 13)),
);
});
it("parses dates with milliseconds", () => {
expect(fromRfc3339("2002-10-02T11:12:13.000Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)));
expect(fromRfc3339("2002-10-02T11:12:13.123Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 123)));
expect(fromRfc3339("2002-10-02T11:12:13.999Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 999)));
});
it("parses dates with low precision fractional seconds", () => {
// 1 digit
expect(fromRfc3339("2002-10-02T11:12:13.0Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)));
expect(fromRfc3339("2002-10-02T11:12:13.1Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 100)));
expect(fromRfc3339("2002-10-02T11:12:13.9Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 900)));
// 2 digit
expect(fromRfc3339("2002-10-02T11:12:13.00Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)));
expect(fromRfc3339("2002-10-02T11:12:13.12Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 120)));
expect(fromRfc3339("2002-10-02T11:12:13.99Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 990)));
});
it("parses dates with high precision fractional seconds", () => {
// everything after the 3rd digit is truncated
// 4 digits
expect(fromRfc3339("2002-10-02T11:12:13.0000Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)));
expect(fromRfc3339("2002-10-02T11:12:13.1234Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 123)));
expect(fromRfc3339("2002-10-02T11:12:13.9999Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 999)));
// 5 digits
expect(fromRfc3339("2002-10-02T11:12:13.00000Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)));
expect(fromRfc3339("2002-10-02T11:12:13.12345Z")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 123)),
);
expect(fromRfc3339("2002-10-02T11:12:13.99999Z")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 999)),
);
// 6 digits
expect(fromRfc3339("2002-10-02T11:12:13.000000Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)));
expect(fromRfc3339("2002-10-02T11:12:13.123456Z")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 123)),
);
expect(fromRfc3339("2002-10-02T11:12:13.999999Z")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 999)),
);
// 7 digits
expect(fromRfc3339("2002-10-02T11:12:13.0000000Z")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)),
);
expect(fromRfc3339("2002-10-02T11:12:13.1234567Z")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 123)),
);
expect(fromRfc3339("2002-10-02T11:12:13.9999999Z")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 999)),
);
// 8 digits
expect(fromRfc3339("2002-10-02T11:12:13.00000000Z")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)),
);
expect(fromRfc3339("2002-10-02T11:12:13.12345678Z")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 123)),
);
expect(fromRfc3339("2002-10-02T11:12:13.99999999Z")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 999)),
);
// 9 digits
expect(fromRfc3339("2002-10-02T11:12:13.000000000Z")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 0)),
);
expect(fromRfc3339("2002-10-02T11:12:13.123456789Z")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 123)),
);
expect(fromRfc3339("2002-10-02T11:12:13.999999999Z")).toEqual(
new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 999)),
);
});
it("accepts space separators", () => {
// https://tools.ietf.org/html/rfc3339#section-5.6
// Applications using this syntax may choose, for the sake of readability,
// to specify a full-date and full-time separated by (say) a space character.
expect(fromRfc3339("2002-10-02 11:12:13Z")).toEqual(new Date(Date.UTC(2002, 9, 2, 11, 12, 13)));
});
it("throws for invalid format", () => {
// extra whitespace
expect(() => fromRfc3339(" 2002-10-02T11:12:13Z")).toThrow();
expect(() => fromRfc3339("2002-10-02T11:12:13Z ")).toThrow();
expect(() => fromRfc3339("2002-10-02T11:12:13 Z")).toThrow();
// wrong date separators
expect(() => fromRfc3339("2002:10-02T11:12:13Z")).toThrow();
expect(() => fromRfc3339("2002-10:02T11:12:13Z")).toThrow();
// wrong time separators
expect(() => fromRfc3339("2002-10-02T11-12:13Z")).toThrow();
expect(() => fromRfc3339("2002-10-02T11:12-13Z")).toThrow();
// wrong separator
expect(() => fromRfc3339("2002-10-02TT11:12:13Z")).toThrow();
expect(() => fromRfc3339("2002-10-02 T11:12:13Z")).toThrow();
expect(() => fromRfc3339("2002-10-02T 11:12:13Z")).toThrow();
expect(() => fromRfc3339("2002-10-02t11:12:13Z")).toThrow();
expect(() => fromRfc3339("2002-10-02x11:12:13Z")).toThrow();
expect(() => fromRfc3339("2002-10-02311:12:13Z")).toThrow();
expect(() => fromRfc3339("2002-10-02.11:12:13Z")).toThrow();
// wrong time zone
expect(() => fromRfc3339("2002-10-02T11:12:13")).toThrow();
expect(() => fromRfc3339("2002-10-02T11:12:13z")).toThrow();
expect(() => fromRfc3339("2002-10-02T11:12:13 00:00")).toThrow();
expect(() => fromRfc3339("2002-10-02T11:12:13+0000")).toThrow();
// wrong fractional seconds
expect(() => fromRfc3339("2018-07-30T19:21:12345Z")).toThrow();
expect(() => fromRfc3339("2018-07-30T19:21:12.Z")).toThrow();
});
it("encodes dates", () => {
expect(toRfc3339(new Date(Date.UTC(0, 0, 1, 0, 0, 0)))).toEqual("1900-01-01T00:00:00.000Z");
expect(toRfc3339(new Date(Date.UTC(2002, 9, 2, 11, 12, 13, 456)))).toEqual("2002-10-02T11:12:13.456Z");
});
});
+58
View File
@@ -0,0 +1,58 @@
import { ReadonlyDate } from "readonly-date";
const rfc3339Matcher = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(\.\d{1,9})?((?:[+-]\d{2}:\d{2})|Z)$/;
function padded(integer: number, length = 2): string {
const filled = "00000" + integer.toString();
return filled.substring(filled.length - length);
}
export function fromRfc3339(str: string): ReadonlyDate {
const matches = rfc3339Matcher.exec(str);
if (!matches) {
throw new Error("Date string is not in RFC3339 format");
}
const year = +matches[1];
const month = +matches[2];
const day = +matches[3];
const hour = +matches[4];
const minute = +matches[5];
const second = +matches[6];
// fractional seconds match either undefined or a string like ".1", ".123456789"
const milliSeconds = matches[7] ? Math.floor(+matches[7] * 1000) : 0;
let tzOffsetSign: number;
let tzOffsetHours: number;
let tzOffsetMinutes: number;
// if timezone is undefined, it must be Z or nothing (otherwise the group would have captured).
if (matches[8] === "Z") {
tzOffsetSign = 1;
tzOffsetHours = 0;
tzOffsetMinutes = 0;
} else {
tzOffsetSign = matches[8].substring(0, 1) === "-" ? -1 : 1;
tzOffsetHours = +matches[8].substring(1, 3);
tzOffsetMinutes = +matches[8].substring(4, 6);
}
const tzOffset = tzOffsetSign * (tzOffsetHours * 60 + tzOffsetMinutes) * 60; // seconds
return new ReadonlyDate(
ReadonlyDate.UTC(year, month - 1, day, hour, minute, second, milliSeconds) - tzOffset * 1000,
);
}
export function toRfc3339(date: Date | ReadonlyDate): string {
const year = date.getUTCFullYear();
const month = padded(date.getUTCMonth() + 1);
const day = padded(date.getUTCDate());
const hour = padded(date.getUTCHours());
const minute = padded(date.getUTCMinutes());
const second = padded(date.getUTCSeconds());
const ms = padded(date.getUTCMilliseconds(), 3);
return `${year}-${month}-${day}T${hour}:${minute}:${second}.${ms}Z`;
}
+62
View File
@@ -0,0 +1,62 @@
import { fromUtf8, toUtf8 } from "./utf8";
describe("utf8", () => {
it("encodes ascii strings", () => {
expect(toUtf8("")).toEqual(new Uint8Array([]));
expect(toUtf8("abc")).toEqual(new Uint8Array([0x61, 0x62, 0x63]));
expect(toUtf8(" ?=-n|~+-*/\\")).toEqual(
new Uint8Array([0x20, 0x3f, 0x3d, 0x2d, 0x6e, 0x7c, 0x7e, 0x2b, 0x2d, 0x2a, 0x2f, 0x5c]),
);
});
it("decodes ascii string", () => {
expect(fromUtf8(new Uint8Array([]))).toEqual("");
expect(fromUtf8(new Uint8Array([0x61, 0x62, 0x63]))).toEqual("abc");
expect(
fromUtf8(new Uint8Array([0x20, 0x3f, 0x3d, 0x2d, 0x6e, 0x7c, 0x7e, 0x2b, 0x2d, 0x2a, 0x2f, 0x5c])),
).toEqual(" ?=-n|~+-*/\\");
});
it("encodes null character", () => {
expect(toUtf8("\u0000")).toEqual(new Uint8Array([0x00]));
});
it("decodes null byte", () => {
expect(fromUtf8(new Uint8Array([0x00]))).toEqual("\u0000");
});
it("encodes Basic Multilingual Plane strings", () => {
expect(toUtf8("ö")).toEqual(new Uint8Array([0xc3, 0xb6]));
expect(toUtf8("¥")).toEqual(new Uint8Array([0xc2, 0xa5]));
expect(toUtf8("Ф")).toEqual(new Uint8Array([0xd0, 0xa4]));
expect(toUtf8("ⱴ")).toEqual(new Uint8Array([0xe2, 0xb1, 0xb4]));
expect(toUtf8("ⵘ")).toEqual(new Uint8Array([0xe2, 0xb5, 0x98]));
});
it("decodes Basic Multilingual Plane strings", () => {
expect(fromUtf8(new Uint8Array([0xc3, 0xb6]))).toEqual("ö");
expect(fromUtf8(new Uint8Array([0xc2, 0xa5]))).toEqual("¥");
expect(fromUtf8(new Uint8Array([0xd0, 0xa4]))).toEqual("Ф");
expect(fromUtf8(new Uint8Array([0xe2, 0xb1, 0xb4]))).toEqual("ⱴ");
expect(fromUtf8(new Uint8Array([0xe2, 0xb5, 0x98]))).toEqual("ⵘ");
});
it("encodes Supplementary Multilingual Plane strings", () => {
// U+1F0A1
expect(toUtf8("🂡")).toEqual(new Uint8Array([0xf0, 0x9f, 0x82, 0xa1]));
// U+1034A
expect(toUtf8("𐍊")).toEqual(new Uint8Array([0xf0, 0x90, 0x8d, 0x8a]));
});
it("decodes Supplementary Multilingual Plane strings", () => {
// U+1F0A1
expect(fromUtf8(new Uint8Array([0xf0, 0x9f, 0x82, 0xa1]))).toEqual("🂡");
// U+1034A
expect(fromUtf8(new Uint8Array([0xf0, 0x90, 0x8d, 0x8a]))).toEqual("𐍊");
});
it("throws on invalid utf8 bytes", () => {
// Broken UTF8 example from https://github.com/nodejs/node/issues/16894
expect(() => fromUtf8(new Uint8Array([0xf0, 0x80, 0x80]))).toThrow();
});
});
+36
View File
@@ -0,0 +1,36 @@
// Global symbols in some environments
// https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
// https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
declare const TextEncoder: any | undefined;
declare const TextDecoder: any | undefined;
function isValidUtf8(data: Uint8Array): boolean {
const toStringAndBack = Buffer.from(Buffer.from(data).toString("utf8"), "utf8");
return Buffer.compare(Buffer.from(data), toStringAndBack) === 0;
}
export function toUtf8(str: string): Uint8Array {
// Browser and future nodejs (https://github.com/nodejs/node/issues/20365)
if (typeof TextEncoder !== "undefined") {
return new TextEncoder().encode(str);
}
// Use Buffer hack instead of nodejs util.TextEncoder to ensure
// webpack does not bundle the util module for browsers.
return new Uint8Array(Buffer.from(str, "utf8"));
}
export function fromUtf8(data: Uint8Array): string {
// Browser and future nodejs (https://github.com/nodejs/node/issues/20365)
if (typeof TextDecoder !== "undefined") {
return new TextDecoder("utf-8", { fatal: true }).decode(data);
}
// Use Buffer hack instead of nodejs util.TextDecoder to ensure
// webpack does not bundle the util module for browsers.
// Buffer.toString has no fatal option
if (!isValidUtf8(data)) {
throw new Error("Invalid UTF8 data");
}
return Buffer.from(data).toString("utf8");
}