From 7ea22c77300a45a6c866c4aa9803901ca2f060f5 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 17 Dec 2019 20:51:43 -0500 Subject: [PATCH 01/71] First attempt at appveyor --- appveyor.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..b62d3f06 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,13 @@ +environment: + nodejs_version: "8.12" + +install: + - ps: Install-Product node $env:nodejs_version + - npm install + +build_script: + - npm build + +test_script: + - npm test + From 3469a7912aa8947dd482eb3e73ef226655765039 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 17 Dec 2019 20:53:17 -0500 Subject: [PATCH 02/71] Second attempt at appveyor --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index b62d3f06..cc042453 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,7 +6,7 @@ install: - npm install build_script: - - npm build + - npm run build test_script: - npm test From d46082bb675ada210ebd82e9cbd75dc442713da1 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 17 Dec 2019 21:10:43 -0500 Subject: [PATCH 03/71] third attempt at appveyor --- package-lock.json | 55 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 4 +++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 6ee0668f..7298532f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,6 +94,12 @@ "integrity": "sha1-ZBqlXft9am8KgUHEucCqULbCTdU=", "dev": true }, + "arg": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.2.tgz", + "integrity": "sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -173,6 +179,12 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, "builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", @@ -1168,6 +1180,12 @@ "semver": "^5.6.0" } }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -2007,6 +2025,16 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -2131,6 +2159,27 @@ "integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=", "dev": true }, + "ts-node": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.5.4.tgz", + "integrity": "sha512-izbVCRV68EasEPQ8MSIGBNK9dc/4sYJJKYA+IarMQct1RtEot6Xp0bXuClsbUSnKpg50ho+aOAx8en5c+y4OFw==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + }, + "dependencies": { + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "dev": true + } + } + }, "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", @@ -2566,6 +2615,12 @@ } } } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true } } } diff --git a/package.json b/package.json index fbcb2655..5a7a4de9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "license": "LGPL-2.1-or-later", "scripts": { "build": "tsc -p ./", - "madge": "node node_modules/madge/bin/cli.js --warning --circular --extension ts ./" + "madge": "node node_modules/madge/bin/cli.js --warning --circular --extension ts ./", + "test": "mocha --require ts-node/register ./test/**/*.ts" }, "dependencies": { "arraybuffer-to-buffer": "^0.0.5", @@ -25,6 +26,7 @@ "mocha": "^6.1.2", "mocha-typescript": "^1.1.17", "stream-buffers": "^3.0.2", + "ts-node": "^8.5.4", "tslint": "^5.13.1", "typemoq": "^2.1.0", "typescript": "^3.4.3" From e18ca892dd93e99f4dae066738bd261d064915de Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 21 Dec 2019 12:59:01 -0500 Subject: [PATCH 04/71] constructor tests for comments frame --- src/id3v2/frames/commentsFrame.ts | 3 +- test/id3v2/commentsFrameTests.ts | 279 ++++++++++++++++++++++++++++++ 2 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 test/id3v2/commentsFrameTests.ts diff --git a/src/id3v2/frames/commentsFrame.ts b/src/id3v2/frames/commentsFrame.ts index be1a66ee..8627d040 100644 --- a/src/id3v2/frames/commentsFrame.ts +++ b/src/id3v2/frames/commentsFrame.ts @@ -244,13 +244,12 @@ export default class CommentsFrame extends Frame { } protected parseFields(data: ByteVector, version: number): void { - Guards.byte(version, "version"); if (data.length < 4) { throw new CorruptFileError("Not enough bytes in field"); } this.textEncoding = data.get(0); - this._language = data.toString(StringType.Latin1, 1, 3); + this._language = data.toString(3, StringType.Latin1, 1); // Instead of splitting into two strings, in the format [{desc}\0{value}], try splitting // into three strings in case of a misformatted [{desc}\0{value}\0]. diff --git a/test/id3v2/commentsFrameTests.ts b/test/id3v2/commentsFrameTests.ts new file mode 100644 index 00000000..61f612fe --- /dev/null +++ b/test/id3v2/commentsFrameTests.ts @@ -0,0 +1,279 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import CommentsFrame from "../../src/id3v2/frames/commentsFrame"; +import FrameConstructorTests from "./frameConstructorTests"; +import FrameTypes from "../../src/id3v2/frameTypes"; +import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; +import {ByteVector, StringType} from "../../src/byteVector"; +import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; +import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; + +// Setup Chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(timeout(3000), slow(1000)) +class CommentsFrameConstructorTests extends FrameConstructorTests { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + return CommentsFrame.fromOffsetRawData; + } + + public get fromRawData(): (d: ByteVector, v: number) => Frame { + return CommentsFrame.fromRawData; + } + + @test + public fromDescription_falsyDescription() { + // Act/Assert + assert.throws(() => { CommentsFrame.fromDescription(undefined, undefined, undefined); }); + assert.throws(() => { CommentsFrame.fromDescription(null, undefined, undefined); }); + } + + @test + public fromDescription_withoutLanguage() { + // Arrange + const description = "fux"; + + // Act + const frame = CommentsFrame.fromDescription(description); + + // Assert + CommentsFrameConstructorTests.validateFrame(frame, description, "XXX", Id3v2TagSettings.defaultEncoding, ""); + } + + @test + public fromDescription_withLanguageWithoutEncoding() { + // Arrange + const description = "fux"; + const language = "bux"; + + // Act + const frame = CommentsFrame.fromDescription(description, language); + + // Assert + CommentsFrameConstructorTests.validateFrame(frame, description, language, Id3v2TagSettings.defaultEncoding, ""); + } + + @test + public fromDescription_withLanguageWithEncoding() { + // Arrange + const description = "fux"; + const language = "bux"; + const encoding = StringType.Latin1; + + // Act + const frame = CommentsFrame.fromDescription(description, language, encoding); + + // Assert + CommentsFrameConstructorTests.validateFrame(frame, description, language, encoding, ""); + } + + @test + public fromOffsetRawData_tooFewBytes_throws() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + header.frameSize = 1; + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, + StringType.Latin1 + ); + + // Act/Assert + assert.throws(() => { CommentsFrame.fromOffsetRawData(data, 2, header); }); + } + + @test + public fromOffsetRawData_noData_returnsEmptyFrame() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + header.frameSize = 4; + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, + StringType.Latin1, + ByteVector.fromString("eng", StringType.Latin1) + ); + + // Act + const frame = CommentsFrame.fromOffsetRawData(data, 2, header); + + // Assert + CommentsFrameConstructorTests.validateFrame(frame, "", "eng", StringType.Latin1, ""); + } + + @test + public fromOffsetRawData_oneData_returnsFrameWithoutDescription() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + header.frameSize = 7; + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, + StringType.Latin1, + ByteVector.fromString("eng", StringType.Latin1), + ByteVector.fromString("fux", StringType.Latin1) + ); + + // Act + const frame = CommentsFrame.fromOffsetRawData(data, 2, header); + + // Assert + CommentsFrameConstructorTests.validateFrame(frame, "", "eng", StringType.Latin1, "fux"); + } + + @test + public fromOffsetRawData_twoData_returnsFrameWithDescriptionAndText() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + header.frameSize = 11; + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, + StringType.Latin1, + ByteVector.fromString("eng", StringType.Latin1), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + ByteVector.fromString("bux", StringType.Latin1) + ); + + // Act + const frame = CommentsFrame.fromOffsetRawData(data, 2, header); + + // Assert + CommentsFrameConstructorTests.validateFrame(frame, "fux", "eng", StringType.Latin1, "bux"); + } + + @test + public fromOffsetRawData_threeData_returnsFrameWithDescriptionAndText() { + const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + header.frameSize = 12; + const data = ByteVector.concatenate( + header.render(4), + StringType.Latin1, + 0x00, 0x00, + ByteVector.fromString("eng", StringType.Latin1), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + ByteVector.fromString("bux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1) + ); + + // Act + const frame = CommentsFrame.fromOffsetRawData(data, 2, header); + + // Assert + CommentsFrameConstructorTests.validateFrame(frame, "fux", "eng", StringType.Latin1, "bux"); + } + + @test + public fromRawData_tooFewBytes_throws() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + header.frameSize = 1; + const data = ByteVector.concatenate( + header.render(4), + StringType.Latin1 + ); + + // Act/Assert + assert.throws(() => { CommentsFrame.fromRawData(data, 4); }); + } + + @test + public fromRawData_noData_returnsEmptyFrame() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + header.frameSize = 4; + const data = ByteVector.concatenate( + header.render(4), + StringType.Latin1, + ByteVector.fromString("eng", StringType.Latin1) + ); + + // Act + const frame = CommentsFrame.fromRawData(data, 4); + + // Assert + CommentsFrameConstructorTests.validateFrame(frame, "", "eng", StringType.Latin1, ""); + } + + @test + public fromRawData_oneData_returnsFrameWithoutDescription() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + header.frameSize = 7; + const data = ByteVector.concatenate( + header.render(4), + StringType.Latin1, + ByteVector.fromString("eng", StringType.Latin1), + ByteVector.fromString("fux", StringType.Latin1), + ); + + // Act + const frame = CommentsFrame.fromRawData(data, 4); + + // Assert + CommentsFrameConstructorTests.validateFrame(frame, "", "eng", StringType.Latin1, "fux"); + } + + @test + public fromRawData_twoData_returnsFrameWithDescriptionAndText() { + const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + header.frameSize = 11; + const data = ByteVector.concatenate( + header.render(4), + StringType.Latin1, + ByteVector.fromString("eng", StringType.Latin1), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + ByteVector.fromString("bux", StringType.Latin1) + ); + + // Act + const frame = CommentsFrame.fromRawData(data, 4); + + // Assert + CommentsFrameConstructorTests.validateFrame(frame, "fux", "eng", StringType.Latin1, "bux"); + } + + @test + public fromRawData_threeData_returnsFrameWithDescriptionAndText() { + const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + header.frameSize = 12; + const data = ByteVector.concatenate( + header.render(4), + StringType.Latin1, + ByteVector.fromString("eng", StringType.Latin1), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + ByteVector.fromString("bux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1) + ); + + // Act + const frame = CommentsFrame.fromRawData(data, 4); + + // Assert + CommentsFrameConstructorTests.validateFrame(frame, "fux", "eng", StringType.Latin1, "bux"); + } + + private static validateFrame( + frame: CommentsFrame, + expectedDesc: string, + expectedLang: string, + expectedEncoding: StringType, + expectedText: string + ) { + assert.isOk(frame); + assert.equal(frame.frameClassType, FrameClassType.CommentsFrame); + assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.COMM)); + + assert.strictEqual(frame.description, expectedDesc); + assert.strictEqual(frame.language, expectedLang); + assert.strictEqual(frame.textEncoding, expectedEncoding); + assert.strictEqual(frame.text, expectedText); + } +} From c19cca61ea67f9cdf65a011d8c1d3be10422df04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2019 18:19:47 +0000 Subject: [PATCH 05/71] Bump lodash from 4.17.11 to 4.17.15 (#2) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.15. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.15) Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ee0668f..587e4a35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1097,9 +1097,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, "lodash.unescape": { From a06b5245945cc80712a98bb4bf940bcf963a1e3f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2019 18:23:16 +0000 Subject: [PATCH 06/71] Bump js-yaml from 3.12.2 to 3.13.0 (#3) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.12.2 to 3.13.0. - [Release notes](https://github.com/nodeca/js-yaml/releases) - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.12.2...3.13.0) Signed-off-by: dependabot[bot] --- package-lock.json | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 587e4a35..ffc47aea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1059,9 +1059,9 @@ "dev": true }, "js-yaml": { - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.2.tgz", - "integrity": "sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz", + "integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -1313,16 +1313,6 @@ "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", "dev": true }, - "js-yaml": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz", - "integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, "lcid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", From 013c9515e84c3c5f8f60368aed99f9e09b13f166 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 21 Dec 2019 13:51:19 -0500 Subject: [PATCH 07/71] Adding a readme w/appveyor badges Adding dist to gitignore --- .gitignore | 3 +++ README.md | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 README.md diff --git a/.gitignore b/.gitignore index 47309b0f..19b3443d 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,6 @@ com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties + +# Ignore build output +dist/** \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..05cdbb4b --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# TagLib# for Node + +| Master | Develop | Latest | +|--------|---------|--------| +|[![BuildStatus](https://ci.appveyor.com/api/projects/status/7hdfrbc4ecvvruwv/branch/master?svg=true)](https://ci.appveyor.com/project/benrr101/node-taglib-sharp/branch/master)|[![Build Status](https://ci.appveyor.com/api/projects/status/7hdfrbc4ecvvruwv/branch/develop?svg=true)](https://ci.appveyor.com/project/benrr101/node-taglib-sharp/branch/develop)|[![Build Status](https://ci.appveyor.com/api/projects/status/7hdfrbc4ecvvruwv?svg=true)](https://ci.appveyor.com/project/benrr101/node-taglib-sharp) + +## Description +TagLib# is a .NET library that has been around for years. It provides a unified interface for +accessing metadata from a vast selection of media files. Until now there hasn't been a port of this +library for Node.js. This project is a mostly wholesale translation of the original TagLib#. + +Note: A port of TagLib already exists for Node.js. Despite TagLib being the origin of TabLib#, it +is substantially lacking in the variety of media formats that can be handled. TagLib# greatly +improved on the original TagLib, hence why this project exists + +## Supported Formats +(TODO) \ No newline at end of file From c1b8d9899c0bff12059682cff150ae220feca00d Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 24 Dec 2019 14:50:07 -0500 Subject: [PATCH 08/71] More tests --- test/id3v2/commentsFrameTests.ts | 182 ++++++++++++++++++++++++++++ test/id3v2/frameConstructorTests.ts | 2 +- test/id3v2/framePropertiesTests.ts | 26 ++++ 3 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 test/id3v2/framePropertiesTests.ts diff --git a/test/id3v2/commentsFrameTests.ts b/test/id3v2/commentsFrameTests.ts index 61f612fe..90c4b604 100644 --- a/test/id3v2/commentsFrameTests.ts +++ b/test/id3v2/commentsFrameTests.ts @@ -4,6 +4,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import CommentsFrame from "../../src/id3v2/frames/commentsFrame"; import FrameConstructorTests from "./frameConstructorTests"; +import FramePropertiesTests from "./framePropertiesTests"; import FrameTypes from "../../src/id3v2/frameTypes"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import {ByteVector, StringType} from "../../src/byteVector"; @@ -14,6 +15,21 @@ import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; Chai.use(ChaiAsPromised); const assert = Chai.assert; +function getTestFrame(): CommentsFrame { + const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + header.frameSize = 11; + const data = ByteVector.concatenate( + header.render(4), + StringType.Latin1, + ByteVector.fromString("eng", StringType.Latin1), + ByteVector.fromString("foo", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + ByteVector.fromString("bar", StringType.Latin1) + ); + + return CommentsFrame.fromRawData(data, 4); +} + @suite(timeout(3000), slow(1000)) class CommentsFrameConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { @@ -277,3 +293,169 @@ class CommentsFrameConstructorTests extends FrameConstructorTests { assert.strictEqual(frame.text, expectedText); } } + +@suite(timeout(3000), slow(1000)) +class CommentsFramePropertiesTests extends FramePropertiesTests { + @test + public description() { + const frame = getTestFrame(); + + const set = (v: string) => { frame.description = v; }; + const get = () => frame.description; + this.propertyRoundTrip(set, get, "fux"); + this.propertyNormalized(set, get, undefined, ""); + this.propertyNormalized(set, get, null, ""); + } + + @test + public language() { + const frame = getTestFrame(); + + const set = (v: string) => { frame.language = v; }; + const get = () => frame.language; + this.propertyRoundTrip(set, get, "jpn"); + this.propertyNormalized(set, get, undefined, "XXX"); + this.propertyNormalized(set, get, null, "XXX"); + this.propertyNormalized(set, get, "ab", "XXX"); + this.propertyNormalized(set, get, "abcd", "abc"); + } + + @test + public text() { + const frame = getTestFrame(); + + const set = (v: string) => { frame.text = v; }; + const get = () => frame.text; + this.propertyRoundTrip(set, get, "fux"); + this.propertyNormalized(set, get, undefined, ""); + this.propertyNormalized(set, get, null, ""); + } + + @test + public textEncoding() { + const frame = getTestFrame(); + + this.propertyRoundTrip((v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16); + } +} + +@suite(timeout(3000), slow(1000)) +class CommentsFrameMethodTests { + @test + public find_falsyFrames() { + // Act/Assert + assert.throws(() => { CommentsFrame.find(undefined, "fux"); }); + assert.throws(() => { CommentsFrame.find(null, "fux"); }); + } + + @test + public find_frameDoesNotExist() { + // Arrange + const frames = [ + CommentsFrame.fromDescription("fux", "jpn"), // nothing matches + CommentsFrame.fromDescription("bux", "jpn"), // desc matches, not language + CommentsFrame.fromDescription("qux", "eng") // language matches, not desc + ]; + + // Act + const output = CommentsFrame.find(frames, "bux", "eng"); + + // Assert + assert.isUndefined(output); + } + + @test + public find_frameExistsWithoutLanguage() { + // Arrange + const frames = [ + CommentsFrame.fromDescription("fux", "jpn"), // nothing matches + CommentsFrame.fromDescription("bux", "jpn"), // desc matches, not language + CommentsFrame.fromDescription("qux", "jpn") // desc does not match + ]; + + // Act + const output = CommentsFrame.find(frames, "bux"); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[1]); + } + + @test + public find_frameExistsWithLanguage() { + // Arrange + const frames = [ + CommentsFrame.fromDescription("fux", "jpn"), // nothing matches + CommentsFrame.fromDescription("bux", "jpn"), // desc matches, not language + CommentsFrame.fromDescription("qux", "eng"), // language matches, not desc + CommentsFrame.fromDescription("bux", "eng") // everything matches + ]; + + // Act + const output = CommentsFrame.find(frames, "bux", "eng"); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[3]); + } + + @test + public findAll_falsyFrames() { + // Act/Assert + assert.throws(() => { CommentsFrame.findAll(undefined, "fux"); }); + assert.throws(() => { CommentsFrame.findAll(null, "fux"); }); + } + + @test + public findAll_frameDoesNotExist() { + // Arrange + const frames = [ + CommentsFrame.fromDescription("fux", "jpn"), + CommentsFrame.fromDescription("bux", "jpn"), + CommentsFrame.fromDescription("qux", "eng") + ]; + + // Act + const output = CommentsFrame.findAll(frames, "fux", "eng"); + + // Assert + assert.isArray(output); + assert.isEmpty(output); + } + + @test + public findAll_frameExistsWithoutLanguage() { + // Arrange + const frames = [ + CommentsFrame.fromDescription("fux", "jpn"), + CommentsFrame.fromDescription("bux", "jpn"), + CommentsFrame.fromDescription("bux", "eng"), + CommentsFrame.fromDescription("qux", "eng") + ]; + + // Act + const output = CommentsFrame.findAll(frames, "bux"); + + // Assert + assert.isArray(output); + assert.sameMembers(output, [frames[1], frames[2]]); + } + + @test + public findAll_frameExistsWithLanguage() { + // Arrange + const frames = [ + CommentsFrame.fromDescription("fux", "jpn"), + CommentsFrame.fromDescription("bux", "jpn"), + CommentsFrame.fromDescription("bux", "eng"), + CommentsFrame.fromDescription("qux", "eng") + ]; + + // Act + const output = CommentsFrame.findAll(frames, "bux", "eng"); + + // Assert + assert.isArray(output); + assert.sameMembers(output, [frames[2]]); + } +} diff --git a/test/id3v2/frameConstructorTests.ts b/test/id3v2/frameConstructorTests.ts index fcee2200..72305160 100644 --- a/test/id3v2/frameConstructorTests.ts +++ b/test/id3v2/frameConstructorTests.ts @@ -1,6 +1,6 @@ import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; -import {slow, suite, test, timeout} from "mocha-typescript"; +import {test} from "mocha-typescript"; import {ByteVector} from "../../src/byteVector"; import {Frame} from "../../src/id3v2/frames/frame"; diff --git a/test/id3v2/framePropertiesTests.ts b/test/id3v2/framePropertiesTests.ts new file mode 100644 index 00000000..4cdeed3a --- /dev/null +++ b/test/id3v2/framePropertiesTests.ts @@ -0,0 +1,26 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; + +// Setup Chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +export default abstract class FramePropertiesTests { + protected propertyRoundTrip(set: (v: T) => void, get: () => T, val: T) { + // Act + set(val); + const output = get(); + + // Assert + assert.strictEqual(output, val); + } + + protected propertyNormalized(set: (v: T) => void, get: () => T, input: T, output: T) { + // Act + set(input); + const result = get(); + + // Assert + assert.strictEqual(result, output); + } +} From c4be427c507ac829e57086f07a542a9947e08045 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 24 Dec 2019 23:40:43 -0500 Subject: [PATCH 09/71] Unit tests for terms of use frame --- src/id3v2/frames/termsOfUseFrame.ts | 26 +-- test/id3v2/termsOfUseFrameTests.ts | 291 ++++++++++++++++++++++++++++ 2 files changed, 301 insertions(+), 16 deletions(-) create mode 100644 test/id3v2/termsOfUseFrameTests.ts diff --git a/src/id3v2/frames/termsOfUseFrame.ts b/src/id3v2/frames/termsOfUseFrame.ts index 54e43fc9..e9ac0fa6 100644 --- a/src/id3v2/frames/termsOfUseFrame.ts +++ b/src/id3v2/frames/termsOfUseFrame.ts @@ -20,21 +20,15 @@ export default class TermsOfUseFrame extends Frame { /** * Constructs and initializes a new instance with a specified language. * @param language ISO-639-2 language code for the new frame + * @param textEncoding Optional, text encoding to use when rendering the new frame. If not + * provided defaults to {@see Id3v2TagSettings.defaultEncoding} */ - public static fromLanguage(language: string): TermsOfUseFrame { - const f = new TermsOfUseFrame(new Id3v2FrameHeader(FrameTypes.USER, 4)); - f._language = language; - return f; - } - - /** - * Constructs and initializes a new instance with a specified language and encoding. - * @param language ISO-639-2 language code for the new frame - * @param encoding Text encoding to use when rendering the new frame - */ - public static fromLanguageAndEncoding(language: string, encoding: StringType): TermsOfUseFrame { + public static fromFields( + language: string, + textEncoding: StringType = Id3v2TagSettings.defaultEncoding + ): TermsOfUseFrame { const f = new TermsOfUseFrame(new Id3v2FrameHeader(FrameTypes.USER, 4)); - f.textEncoding = encoding; + f.textEncoding = textEncoding; f._language = language; return f; } @@ -129,7 +123,7 @@ export default class TermsOfUseFrame extends Frame { * @returns TermsOfUseFrame A matching frame if found or `undefined` if a matching frame was * not found */ - public static get(frames: TermsOfUseFrame[], language: string): TermsOfUseFrame { + public static find(frames: TermsOfUseFrame[], language?: string): TermsOfUseFrame { Guards.truthy(frames, "frames"); return frames.find((f) => !language || f.language === language); } @@ -142,7 +136,7 @@ export default class TermsOfUseFrame extends Frame { * @returns TermsOfUseFrame Frame containing the matching frame or `undefined` if a match was * not found */ - public static getPreferred(frames: TermsOfUseFrame[], language: string) { + public static findPreferred(frames: TermsOfUseFrame[], language: string) { Guards.truthy(frames, "frames"); let bestFrame: TermsOfUseFrame; @@ -160,7 +154,7 @@ export default class TermsOfUseFrame extends Frame { /** @inheritDoc */ public clone(): Frame { - const frame = TermsOfUseFrame.fromLanguageAndEncoding(this._language, this.textEncoding); + const frame = TermsOfUseFrame.fromFields(this._language, this.textEncoding); frame.text = this.text; return frame; } diff --git a/test/id3v2/termsOfUseFrameTests.ts b/test/id3v2/termsOfUseFrameTests.ts new file mode 100644 index 00000000..1e1a659e --- /dev/null +++ b/test/id3v2/termsOfUseFrameTests.ts @@ -0,0 +1,291 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import FrameConstructorTests from "./frameConstructorTests"; +import FramePropertiesTests from "./framePropertiesTests"; +import FrameTypes from "../../src/id3v2/frameTypes"; +import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; +import TermsOfUseFrame from "../../src/id3v2/frames/termsOfUseFrame"; +import {ByteVector, StringType} from "../../src/byteVector"; +import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; +import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; + + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(timeout(3000), slow(1000)) +class TermsOfUseFrameConstructorsTests extends FrameConstructorTests { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + return TermsOfUseFrame.fromOffsetRawData; + } + + public get fromRawData(): (d: ByteVector, v: number) => Frame { + return TermsOfUseFrame.fromRawData; + } + + @test + public fromFields_withoutTextEncoding() { + // Act + const output = TermsOfUseFrame.fromFields("fux"); + + // Assert + this.assertFrame(output, "fux", undefined, Id3v2TagSettings.defaultEncoding); + } + + @test + public fromFields_withTextEncoding() { + // Act + const output = TermsOfUseFrame.fromFields("fux", StringType.UTF16BE); + + // Assert + this.assertFrame(output, "fux", undefined, StringType.UTF16BE); + } + + @test + public fromOffsetRawData_notEnoughBytes() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.USER, 4); + header.frameSize = 2; + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, // Offset data + 0x00, 0x00 + ); + + // Act / Assert + assert.throws(() => { TermsOfUseFrame.fromOffsetRawData(data, 2, header); }); + } + + @test + public fromOffsetRawData_enoughData() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.USER, 4); + header.frameSize = 10; + const data = ByteVector.concatenate( + header.render(4), + 0x01, 0x00, // Offset data + StringType.Latin1, + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.fromString("buxqux", StringType.Latin1) + ); + + // Act + const output = TermsOfUseFrame.fromOffsetRawData(data, 2, header); + + // Assert + this.assertFrame(output, "fux", "buxqux", StringType.Latin1); + } + + @test + public fromRawData_notEnoughBytes() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.USER, 4); + header.frameSize = 2; + const data = ByteVector.concatenate( + header.render(4), + 0x01, 0x02 + ); + + // Act / Assert + assert.throws(() => { TermsOfUseFrame.fromRawData(data, 4); }); + } + + @test + public fromRawData_enoughData() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.USER, 4); + header.frameSize = 10; + const data = ByteVector.concatenate( + header.render(4), + StringType.Latin1, + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.fromString("buxqux", StringType.Latin1) + ); + + // Act + const output = TermsOfUseFrame.fromRawData(data, 4); + + // Assert + this.assertFrame(output, "fux", "buxqux", StringType.Latin1); + } + + private assertFrame(frame: TermsOfUseFrame, language: string, text: string, textEncoding: StringType) { + assert.isOk(frame); + assert.strictEqual(frame.frameClassType, FrameClassType.TermsOfUseFrame); + assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.USER)); + + assert.strictEqual(frame.language, language); + assert.strictEqual(frame.text, text); + assert.strictEqual(frame.textEncoding, textEncoding); + } +} + +@suite(timeout(3000), slow(1000)) +class TermsOfUseFramePropertyTests extends FramePropertiesTests { + @test + public language() { + // Arrange + const set = (v: string) => { frame.language = v; }; + const get = () => frame.language; + + // Act/Assert + const frame = TermsOfUseFrame.fromFields("eng"); + this.propertyNormalized(set, get, "fu", "XXX"); + this.propertyNormalized(set, get, "fuxx", "fux"); + this.propertyNormalized(set, get, undefined, "XXX"); + this.propertyNormalized(set, get, null, "XXX"); + this.propertyRoundTrip(set, get, "fux"); + } + + @test + public text() { + const frame = TermsOfUseFrame.fromFields("eng"); + this.propertyRoundTrip((v) => { frame.text = v; }, () => frame.text, "fux"); + this.propertyRoundTrip((v) => { frame.text = v; }, () => frame.text, undefined); + } + + @test + public textEncoding() { + const frame = TermsOfUseFrame.fromFields("eng", StringType.Latin1); + this.propertyRoundTrip((v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16); + } +} + +@suite(timeout(3000), slow(1000)) +class TermsOfUseFrameMethodTests { + @test + public get_falsyFrames() { + // Act/Assert + assert.throws(() => { TermsOfUseFrame.find(undefined); }); + } + + @test + public get_noFrames() { + // Act + const output = TermsOfUseFrame.find([]); + + // Assert + assert.isUndefined(output); + } + + @test + public get_matchWithFrames_returnsFirst() { + // Arrange + const frames = [ + TermsOfUseFrame.fromFields("eng"), + TermsOfUseFrame.fromFields("jpn") + ]; + + // Act + const output = TermsOfUseFrame.find(frames); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[0]); + } + + @test + public get_matchWithFramesWithLanguage() { + // Arrange + const frames = [ + TermsOfUseFrame.fromFields("eng"), + TermsOfUseFrame.fromFields("jpn") + ]; + + // Act + const output = TermsOfUseFrame.find(frames, "jpn"); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[1]); + } + + @test + public getPreferred_falsyFrames() { + // Act/Assert + assert.throws(() => TermsOfUseFrame.findPreferred(undefined, "eng")); + } + + @test + public getPreferred_noFrames() { + // Act + const output = TermsOfUseFrame.findPreferred([], "eng"); + + // Assert + assert.isUndefined(output); + } + + @test + public getPrefeerred_nonExactMatch() { + // Arrange + const frames = [ + TermsOfUseFrame.fromFields("eng"), + TermsOfUseFrame.fromFields("jpn") + ]; + + // Act + const output = TermsOfUseFrame.findPreferred(frames, "foo"); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[0]); + } + + @test + public getPreferred_exactMatch() { + // Arrange + const frames = [ + TermsOfUseFrame.fromFields("eng"), + TermsOfUseFrame.fromFields("jpn") + ]; + + // Act + const output = TermsOfUseFrame.findPreferred(frames, "jpn"); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[1]); + } + + @test + public clone() { + // Arrange + const frame = TermsOfUseFrame.fromFields("eng", StringType.Latin1); + frame.text = "foobarbux"; + + // Act + const output = frame.clone(); + + // Assert + assert.isOk(output); + assert.strictEqual(output.frameClassType, frame.frameClassType); + assert.isTrue(ByteVector.equal(output.frameId, frame.frameId)); + + assert.strictEqual(output.language, frame.language); + assert.strictEqual(output.text, frame.text); + assert.strictEqual(output.textEncoding, frame.textEncoding); + } + + @test + public render() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.USER, 4); + header.frameSize = 10; + const data = ByteVector.concatenate( + header.render(4), + StringType.Latin1, + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.fromString("buxqux", StringType.Latin1) + ); + const frame = TermsOfUseFrame.fromRawData(data, 4); + + // Act + const output = frame.render(4); + + // Assert + assert.isTrue(ByteVector.equal(output, data)); + } +} From 45c5a93f23c65757e6c94c5beeaeca7cbbefbbf4 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Mon, 30 Dec 2019 23:15:05 -0500 Subject: [PATCH 10/71] Constructor tests for synchronized lyrics/text frame --- src/id3v2/frames/synchronizedLyricsFrame.ts | 42 +-- test/id3v2/synchronizedLyricsFrameTests.ts | 342 ++++++++++++++++++++ 2 files changed, 363 insertions(+), 21 deletions(-) create mode 100644 test/id3v2/synchronizedLyricsFrameTests.ts diff --git a/src/id3v2/frames/synchronizedLyricsFrame.ts b/src/id3v2/frames/synchronizedLyricsFrame.ts index 70582cd2..e73794ca 100644 --- a/src/id3v2/frames/synchronizedLyricsFrame.ts +++ b/src/id3v2/frames/synchronizedLyricsFrame.ts @@ -40,6 +40,14 @@ export class SynchronizedText { public clone(): SynchronizedText { return new SynchronizedText(this.time, this.text); } + + public render(encoding: StringType): ByteVector { + return ByteVector.concatenate( + ByteVector.fromString(this.text, encoding), + ByteVector.getTextDelimiter(encoding), + ByteVector.fromUInt(this.time) + ); + } } /** @@ -50,7 +58,7 @@ export class SynchronizedLyricsFrame extends Frame { private _description: string; private _format: TimestampFormat = TimestampFormat.Unknown; private _language: string; - private _text: SynchronizedText[]; + private _text: SynchronizedText[] = []; private _textEncoding: StringType = Id3v2TagSettings.defaultEncoding; private _textType: SynchronizedTextType = SynchronizedTextType.Other; @@ -150,10 +158,11 @@ export class SynchronizedLyricsFrame extends Frame { public get language(): string { return this._language; } /** * Sets the ISO-639-2 language code stored in the current instance. - * There should only be one frame with a matching description, type, and ISO-639=2 language + * There should only be one frame with a matching description, type, and ISO-639-2 language * code per tag. * @param value ISO-639-2 language code stored in the current instance */ + // @TODO: Should this be normalized like other ISO-639-2 fields? public set language(value: string) { this._language = value; } /** @@ -285,9 +294,6 @@ export class SynchronizedLyricsFrame extends Frame { /** @inheritDoc */ protected parseFields(data: ByteVector, version: number): void { - Guards.truthy(data, "data"); - Guards.byte(version, "version"); - if (data.length < 6) { throw new CorruptFileError("Not enough bytes in field"); } @@ -336,22 +342,16 @@ export class SynchronizedLyricsFrame extends Frame { Guards.byte(version, "version"); const encoding = SynchronizedLyricsFrame.correctEncoding(this.textEncoding, version); - const delim = ByteVector.getTextDelimiter(encoding); - - const v = ByteVector.empty(); - v.addByte(encoding); - v.addByteVector(ByteVector.fromString(this.language, StringType.Latin1)); - v.addByte(this.format); - v.addByte(this.textType); - v.addByteVector(ByteVector.fromString(this.description, encoding)); - v.addByteVector(delim); - - for (const t of this.text) { - v.addByteVector(ByteVector.fromString(t.text, encoding)); - v.addByteVector(delim); - v.addByteVector(ByteVector.fromUInt(t.time)); - } + const renderedText = this.text.map((t) => t.render(encoding)); - return v; + return ByteVector.concatenate( + encoding, + ByteVector.fromString(this.language, StringType.Latin1), + this.format, + this.textType, + ByteVector.fromString(this.description, encoding), + ByteVector.getTextDelimiter(encoding), + ... renderedText + ); } } diff --git a/test/id3v2/synchronizedLyricsFrameTests.ts b/test/id3v2/synchronizedLyricsFrameTests.ts new file mode 100644 index 00000000..fc85055f --- /dev/null +++ b/test/id3v2/synchronizedLyricsFrameTests.ts @@ -0,0 +1,342 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import ConstructorTests from "./frameConstructorTests"; +import FrameTypes from "../../src/id3v2/frameTypes"; +import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; +import PropertiesTests from "./framePropertiesTests"; +import {ByteVector, StringType} from "../../src/byteVector"; +import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; +import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {SynchronizedLyricsFrame, SynchronizedText} from "../../src/id3v2/frames/synchronizedLyricsFrame"; +import {SynchronizedTextType, TimestampFormat} from "../../src/id3v2/utilTypes"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(timeout(3000), slow(1000)) +class SynchronizedTextTests { + @test + public synchronizedText_construct() { + // Act + const text = new SynchronizedText(123, "fux"); + + // Assert + assert.strictEqual(text.time, 123); + assert.strictEqual(text.text, "fux"); + } + + @test + public synchronizedText_render() { + // Arrange + const text = new SynchronizedText(123, "fux"); + + // Act + const output = text.render(StringType.UTF16BE); + + // Assert + const expected = ByteVector.concatenate( + ByteVector.fromString(text.text, StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromUInt(text.time), + ); + assert.isTrue(ByteVector.equal(output, expected)); + } +} + +@suite(timeout(3000), slow(1000)) +class SynchronizedLyricsFrameConstructorTests extends ConstructorTests { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + return SynchronizedLyricsFrame.fromOffsetRawData; + } + + public get fromRawData(): (d: ByteVector, v: number) => Frame { + return SynchronizedLyricsFrame.fromRawData; + } + + @test + public fromInfo_withoutEncoding() { + // Arrange + const description = "fux"; + const language = "bux"; + const textType = SynchronizedTextType.Lyrics; + + // Act + const frame = SynchronizedLyricsFrame.fromInfo(description, language, textType); + + // Assert + this.assertFrame( + frame, + description, + TimestampFormat.Unknown, + language, + [], + Id3v2TagSettings.defaultEncoding, + textType + ); + } + + @test + public fromInfo_withEncoding() { + // Arrange + const description = "fux"; + const encoding = StringType.UTF16BE; + const language = "bux"; + const textType = SynchronizedTextType.Lyrics; + + // Act + const frame = SynchronizedLyricsFrame.fromInfo(description, language, textType, encoding); + + // Assert + this.assertFrame( + frame, + description, + TimestampFormat.Unknown, + language, + [], + encoding, + textType + ); + } + + @test + public fromRawData_notEnoughBytes() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + header.frameSize = 5; + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, 0x00, 0x00, 0x00 + ); + + // Act / Assert + assert.throws(() => { SynchronizedLyricsFrame.fromRawData(data, 4); }); + } + + @test + public fromRawData_missingDelimiter() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + header.frameSize = 10; + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x02, 0x03, 0x04, 0x05 + ); + + // Act / Assert + assert.throws(() => { SynchronizedLyricsFrame.fromRawData(data, 4); }); + } + + @test + public fromRawData_noDelimiterForSynchronizedText() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + header.frameSize = 16; + const data = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("fux", StringType.Latin1), + TimestampFormat.AbsoluteMilliseconds, + SynchronizedTextType.Trivia, + ByteVector.fromString("bux", StringType.UTF16BE), + 0x01, 0x02, 0x03, 0x04 + ); + + // Act / Assert + assert.throws(() => { SynchronizedLyricsFrame.fromRawData(data, 4); }); + } + + @test + public fromRawData_incompleteSynchronizedText() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + header.frameSize = 34; + const content1 = new SynchronizedText(123, "qux"); + const content2 = new SynchronizedText(456, "zux"); + const data = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("fux", StringType.Latin1), + TimestampFormat.AbsoluteMilliseconds, + SynchronizedTextType.Trivia, + ByteVector.fromString("bux", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + content1.render(StringType.UTF16BE), + content2.render(StringType.UTF16BE) + ); + + // Act + const frame = SynchronizedLyricsFrame.fromRawData(data, 4); + + // Assert + this.assertFrame( + frame, + "bux", + TimestampFormat.AbsoluteMilliseconds, + "fux", + [content1], + StringType.UTF16BE, + SynchronizedTextType.Trivia + ); + } + + @test + public fromRawData_noData() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + header.frameSize = 14; + const data = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("fux", StringType.Latin1), + TimestampFormat.AbsoluteMilliseconds, + SynchronizedTextType.Trivia, + ByteVector.fromString("bux", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ); + + // Act + const frame = SynchronizedLyricsFrame.fromRawData(data, 4); + + // Assert + this.assertFrame( + frame, + "bux", + TimestampFormat.AbsoluteMilliseconds, + "fux", + [], + StringType.UTF16BE, + SynchronizedTextType.Trivia + ); + } + + @test + public fromRawData_singleData() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + header.frameSize = 26; + const content1 = new SynchronizedText(123, "qux"); + const data = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("fux", StringType.Latin1), + TimestampFormat.AbsoluteMilliseconds, + SynchronizedTextType.Trivia, + ByteVector.fromString("bux", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + content1.render(StringType.UTF16BE) + ); + + // Act + const frame = SynchronizedLyricsFrame.fromRawData(data, 4); + + // Assert + this.assertFrame( + frame, + "bux", + TimestampFormat.AbsoluteMilliseconds, + "fux", + [content1], + StringType.UTF16BE, + SynchronizedTextType.Trivia + ); + } + + @test + public fromRawData_multipleData() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + header.frameSize = 38; + const content1 = new SynchronizedText(123, "qux"); + const content2 = new SynchronizedText(456, "zux"); + const data = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("fux", StringType.Latin1), + TimestampFormat.AbsoluteMilliseconds, + SynchronizedTextType.Trivia, + ByteVector.fromString("bux", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + content1.render(StringType.UTF16BE), + content2.render(StringType.UTF16BE) + ); + + // Act + const frame = SynchronizedLyricsFrame.fromRawData(data, 4); + + // Assert + this.assertFrame( + frame, + "bux", + TimestampFormat.AbsoluteMilliseconds, + "fux", + [content1, content2], + StringType.UTF16BE, + SynchronizedTextType.Trivia + ); + } + + @test + public fromOffsetRawData_multipleData() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + header.frameSize = 38; + const content1 = new SynchronizedText(123, "qux"); + const content2 = new SynchronizedText(456, "zux"); + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, + StringType.UTF16BE, + ByteVector.fromString("fux", StringType.Latin1), + TimestampFormat.AbsoluteMilliseconds, + SynchronizedTextType.Trivia, + ByteVector.fromString("bux", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + content1.render(StringType.UTF16BE), + content2.render(StringType.UTF16BE) + ); + + // Act + const frame = SynchronizedLyricsFrame.fromOffsetRawData(data, 2, header); + + // Assert + this.assertFrame( + frame, + "bux", + TimestampFormat.AbsoluteMilliseconds, + "fux", + [content1, content2], + StringType.UTF16BE, + SynchronizedTextType.Trivia + ); + } + + private assertFrame( + frame: SynchronizedLyricsFrame, + d: string, + f: TimestampFormat, + l: string, + t: SynchronizedText[], + te: StringType, + tt: SynchronizedTextType + ) { + assert.isOk(frame); + assert.strictEqual(frame.frameClassType, FrameClassType.SynchronizedLyricsFrame); + assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.SYLT)); + + assert.strictEqual(frame.description, d); + assert.strictEqual(frame.format, f); + assert.strictEqual(frame.language, l); + assert.strictEqual(frame.textEncoding, te); + assert.strictEqual(frame.textType, tt); + + assert.isArray(frame.text); + assert.deepStrictEqual(frame.text, t); + } +} + +@suite(timeout(3000), slow(1000)) +class SynchronizedLyricsFramePropertyTests extends PropertiesTests {} From c08641f83210bfe39944c76526f28a636f6a6070 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 31 Dec 2019 19:32:37 -0500 Subject: [PATCH 11/71] The rest of the tests for SynchronizedLyricsFrame --- src/id3v2/frames/synchronizedLyricsFrame.ts | 8 +- test/id3v2/framePropertiesTests.ts | 4 +- test/id3v2/synchronizedLyricsFrameTests.ts | 307 +++++++++++++++++++- 3 files changed, 311 insertions(+), 8 deletions(-) diff --git a/src/id3v2/frames/synchronizedLyricsFrame.ts b/src/id3v2/frames/synchronizedLyricsFrame.ts index e73794ca..95a7a739 100644 --- a/src/id3v2/frames/synchronizedLyricsFrame.ts +++ b/src/id3v2/frames/synchronizedLyricsFrame.ts @@ -205,16 +205,16 @@ export class SynchronizedLyricsFrame extends Frame { * Gets a specified lyrics frame from a list of synchronized lyrics frames * @param frames List of frames to search * @param description Description to match - * @param language ISO-639-2 language code to match * @param textType Text type to match + * @param language Optionally, ISO-639-2 language code to match * @returns SynchronizedLyricsFrame Frame containing the matching user, `undefined` if a match * was not found */ public static find( frames: SynchronizedLyricsFrame[], description: string, - language: string, - textType: SynchronizedTextType + textType: SynchronizedTextType, + language?: string ) { Guards.truthy(frames, "frames"); return frames.find((f) => { @@ -339,8 +339,6 @@ export class SynchronizedLyricsFrame extends Frame { /** @inheritDoc */ protected renderFields(version: number): ByteVector { - Guards.byte(version, "version"); - const encoding = SynchronizedLyricsFrame.correctEncoding(this.textEncoding, version); const renderedText = this.text.map((t) => t.render(encoding)); diff --git a/test/id3v2/framePropertiesTests.ts b/test/id3v2/framePropertiesTests.ts index 4cdeed3a..bda629b4 100644 --- a/test/id3v2/framePropertiesTests.ts +++ b/test/id3v2/framePropertiesTests.ts @@ -12,7 +12,7 @@ export default abstract class FramePropertiesTests { const output = get(); // Assert - assert.strictEqual(output, val); + assert.deepStrictEqual(output, val); } protected propertyNormalized(set: (v: T) => void, get: () => T, input: T, output: T) { @@ -21,6 +21,6 @@ export default abstract class FramePropertiesTests { const result = get(); // Assert - assert.strictEqual(result, output); + assert.deepStrictEqual(result, output); } } diff --git a/test/id3v2/synchronizedLyricsFrameTests.ts b/test/id3v2/synchronizedLyricsFrameTests.ts index fc85055f..6543b3ba 100644 --- a/test/id3v2/synchronizedLyricsFrameTests.ts +++ b/test/id3v2/synchronizedLyricsFrameTests.ts @@ -339,4 +339,309 @@ class SynchronizedLyricsFrameConstructorTests extends ConstructorTests { } @suite(timeout(3000), slow(1000)) -class SynchronizedLyricsFramePropertyTests extends PropertiesTests {} +class SynchronizedLyricsFramePropertyTests extends PropertiesTests { + @test + public description() { + // Arrange + const frame = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord); + const set = (v: string) => { frame.description = v; }; + const get = () => frame.description; + + // Act / Assert + this.propertyRoundTrip(set, get, "fux" ); + this.propertyRoundTrip(set, get, undefined); + } + + @test + public format() { + // Arrange + const frame = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord); + + // Act / Assert + this.propertyRoundTrip((v) => { frame.format = v; }, () => frame.format, TimestampFormat.AbsoluteMilliseconds); + } + + @test + public language() { + // Arrange + const frame = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord); + const set = (v: string) => { frame.language = v; }; + const get = () => frame.language; + + // Act / Assert + this.propertyRoundTrip(set, get, "fux"); + this.propertyRoundTrip(set, get, "shoe"); + this.propertyRoundTrip(set, get, "ab"); + } + + @test + public text() { + // Arrange + const frame = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord); + const set = (v: SynchronizedText[]) => { frame.text = v; }; + const get = () => frame.text; + const value = [new SynchronizedText(123, "foo")]; + + // Act / Assert + this.propertyRoundTrip(set, get, value); + this.propertyNormalized(set, get, undefined, []); + this.propertyNormalized(set, get, null, []); + } + + @test + public textEncoding() { + // Arrange + const frame = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord); + + // Act / Assert + this.propertyRoundTrip((v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16BE); + } + + @test + public textType() { + // Arrange + const frame = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord); + + // Act / Assert + this.propertyRoundTrip((v) => { frame.textType = v; }, () => frame.textType, SynchronizedTextType.Trivia); + } +} + +@suite(timeout(3000), slow(1000)) +class SynchronizedLyricsFrameMethodTests { + @test + public find_falsyFrames() { + // Act / Assert + assert.throws(() => { SynchronizedLyricsFrame.find(undefined, "fux", SynchronizedTextType.Chord); }); + assert.throws(() => { SynchronizedLyricsFrame.find(null, "fux", SynchronizedTextType.Chord); }); + } + + @test + public find_emptyFrames() { + // Act + const output = SynchronizedLyricsFrame.find([], "fux", SynchronizedTextType.Chord); + + // Assert + assert.isUndefined(output); + } + + @test + public find_noMatchWithoutLanguage() { + // Arrange + const frames = [ + SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Trivia), // desc does not match + SynchronizedLyricsFrame.fromInfo("fux", "bux", SynchronizedTextType.Trivia) // type does not match + ]; + + // Act + const output = SynchronizedLyricsFrame.find(frames, "fux", SynchronizedTextType.Chord); + + // Assert + assert.isUndefined(output); + } + + @test + public find_noMatchWithLanguage() { + // Arrange + const frames = [ + SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Trivia), // desc does not match + SynchronizedLyricsFrame.fromInfo("fux", "bar", SynchronizedTextType.Trivia), // lang goes not match + SynchronizedLyricsFrame.fromInfo("fux", "bux", SynchronizedTextType.Trivia) // type does not match + ]; + + // Act + const output = SynchronizedLyricsFrame.find(frames, "fux", SynchronizedTextType.Chord, "bux"); + + // Assert + assert.isUndefined(output); + } + + @test + public find_matchExists() { + // Arrange + const frames = [ + SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Trivia), // desc does not match + SynchronizedLyricsFrame.fromInfo("fux", "bar", SynchronizedTextType.Trivia), // lang goes not match + SynchronizedLyricsFrame.fromInfo("fux", "bux", SynchronizedTextType.Trivia), // type does not match + SynchronizedLyricsFrame.fromInfo("fux", "bux", SynchronizedTextType.Chord) // Is match + ]; + + // Act + const output = SynchronizedLyricsFrame.find(frames, "fux", SynchronizedTextType.Chord, "bux"); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[3]); + } + + @test + public findPreferred_falsyFrames() { + // Act / Assert + assert.throws(() => { + SynchronizedLyricsFrame.findPreferred(undefined, "fux", "bux", SynchronizedTextType.Chord); + }); + assert.throws(() => { + SynchronizedLyricsFrame.findPreferred(null, "fux", "bux", SynchronizedTextType.Chord); + }); + } + + @test + public findPreferred_noFrames() { + // Act + const output = SynchronizedLyricsFrame.findPreferred([], "fux", "bux", SynchronizedTextType.Chord); + + // Assert + assert.isUndefined(output); + } + + @test + public findPreferred_perfectMatch() { + // Arrange + const frames = [ + SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Trivia), // desc does not match + SynchronizedLyricsFrame.fromInfo("fux", "bar", SynchronizedTextType.Trivia), // lang goes not match + SynchronizedLyricsFrame.fromInfo("fux", "bux", SynchronizedTextType.Trivia), // type does not match + SynchronizedLyricsFrame.fromInfo("fux", "bux", SynchronizedTextType.Chord) // perfect match + ]; + + // Act + const output = SynchronizedLyricsFrame.findPreferred(frames, "fux", "bux", SynchronizedTextType.Chord); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[3]); + } + + @test + public findPreferred_descLangMatch() { + // Arrange + const frames = [ + SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Trivia), // desc does not match + SynchronizedLyricsFrame.fromInfo("fux", "bar", SynchronizedTextType.Trivia), // lang goes not match + SynchronizedLyricsFrame.fromInfo("fux", "bux", SynchronizedTextType.Trivia), // type does not match + ]; + + // Act + const output = SynchronizedLyricsFrame.findPreferred(frames, "fux", "bux", SynchronizedTextType.Chord); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[2]); + } + + @test + public findPreferred_langMatch() { + // Arrange + const frames = [ + SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Trivia), // nothing matches + SynchronizedLyricsFrame.fromInfo("foo", "bux", SynchronizedTextType.Trivia), // lang matches + ]; + + // Act + const output = SynchronizedLyricsFrame.findPreferred(frames, "fux", "bux", SynchronizedTextType.Chord); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[1]); + } + + @test + public findPreferred_descMatch() { + // Arrange + const frames = [ + SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Trivia), // nothing matches + SynchronizedLyricsFrame.fromInfo("fux", "bar", SynchronizedTextType.Trivia), // desc matches + ]; + + // Act + const output = SynchronizedLyricsFrame.findPreferred(frames, "fux", "bux", SynchronizedTextType.Chord); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[1]); + } + + @test + public findPreferred_typeMatch() { + // Arrange + const frames = [ + SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Trivia), // nothing matches + SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord), // type matches + ]; + + // Act + const output = SynchronizedLyricsFrame.findPreferred(frames, "fux", "bux", SynchronizedTextType.Chord); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[1]); + } + + @test + public findPreferred_nothingMatches() { + // Arrange + const frames = [ + SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Trivia), // nothing matches + SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Trivia), // nothing matches + ]; + + // Act + const output = SynchronizedLyricsFrame.findPreferred(frames, "fux", "bux", SynchronizedTextType.Chord); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[0]); + } + + @test + public clone() { + // Arrange + const frame = SynchronizedLyricsFrame.fromInfo("fux", "bux", SynchronizedTextType.Chord); + + // Act + const output = frame.clone(); + + // Assert + assert.isOk(output); + assert.notStrictEqual(output, frame); + assert.strictEqual(output.frameClassType, FrameClassType.SynchronizedLyricsFrame); + assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.SYLT)); + + assert.strictEqual(output.description, frame.description); + assert.strictEqual(output.format, frame.format); + assert.strictEqual(output.language, frame.language); + assert.strictEqual(output.textEncoding, frame.textEncoding); + assert.strictEqual(output.textType, frame.textType); + + assert.isArray(output.text); + assert.deepStrictEqual(output.text, frame.text); + } + + @test + public render() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + header.frameSize = 38; + const content1 = new SynchronizedText(123, "qux"); + const content2 = new SynchronizedText(456, "zux"); + const data = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("fux", StringType.Latin1), + TimestampFormat.AbsoluteMilliseconds, + SynchronizedTextType.Trivia, + ByteVector.fromString("bux", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + content1.render(StringType.UTF16BE), + content2.render(StringType.UTF16BE) + ); + const frame = SynchronizedLyricsFrame.fromRawData(data, 4); + + // Act + const output = frame.render(4); + + // Assert + assert.isTrue(ByteVector.equal(output, data)); + } +} + From 2285148c1400f2c7acf1f94289efc470c3e4d2f3 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 31 Dec 2019 19:40:57 -0500 Subject: [PATCH 12/71] Finally fixing that broken test for text information frame encoding --- src/id3v2/frames/textInformationFrame.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/id3v2/frames/textInformationFrame.ts b/src/id3v2/frames/textInformationFrame.ts index 9d4458f6..e8d86c19 100644 --- a/src/id3v2/frames/textInformationFrame.ts +++ b/src/id3v2/frames/textInformationFrame.ts @@ -132,7 +132,6 @@ export class TextInformationFrame extends Frame { private static REMIX_STRING = "Remix"; protected _encoding: StringType = Id3v2TagSettings.defaultEncoding; - protected _rawEncoding: StringType = StringType.Latin1; protected _rawData: ByteVector; protected _rawVersion: number; protected _textFields: string[] = []; @@ -215,7 +214,7 @@ export class TextInformationFrame extends Frame { * Sets the text contained in the current instance. */ public set text(value: string[]) { - this._rawData = undefined; + this.parseRawData(); this._textFields = value ? value.slice() : []; } @@ -231,11 +230,8 @@ export class TextInformationFrame extends Frame { * This value will be overridden if {@see Id3v2Tag.forceDefaultEncoding} is `true`. */ public set textEncoding(value: StringType) { - if (this._rawEncoding) { - this._rawEncoding = value; - } else { - this._encoding = value; - } + this.parseRawData(); + this._encoding = value; } // #endregion @@ -328,9 +324,6 @@ export class TextInformationFrame extends Frame { protected parseFields(data: ByteVector, version: number): void { this._rawData = data; this._rawVersion = version; - - // Read the string data type (first byte of the field data) - this._rawEncoding = data.get(0); } /** @@ -450,7 +443,7 @@ export class TextInformationFrame extends Frame { /** @inheritDoc */ protected renderFields(version: number): ByteVector { - if (this._rawData && this._rawVersion === version && this._rawEncoding === Id3v2TagSettings.defaultEncoding) { + if (this._rawData && this._rawVersion === version) { return this._rawData; } From 2d8ea6b71fcfe0833ca53fccf9181d63cf548e26 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 31 Dec 2019 20:18:26 -0500 Subject: [PATCH 13/71] Adding support for coverage to builds Somehow there's a bunch of new tests failures? --- .nycrc | 3 + appveyor.yml | 2 + package-lock.json | 1872 +++++++++++++++++++++++++++++++++++++++++---- package.json | 6 +- test/mocha.opts | 3 + 5 files changed, 1750 insertions(+), 136 deletions(-) create mode 100644 .nycrc create mode 100644 test/mocha.opts diff --git a/.nycrc b/.nycrc new file mode 100644 index 00000000..504abf9e --- /dev/null +++ b/.nycrc @@ -0,0 +1,3 @@ +{ + "extends": "@istanbuljs/nyc-config-typescript" +} \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index cc042453..57b89732 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,4 +10,6 @@ build_script: test_script: - npm test + + diff --git a/package-lock.json b/package-lock.json index 4ba97cdb..f073e3a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,12 +4,303 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/core": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.7.tgz", + "integrity": "sha512-jlSjuj/7z138NLZALxVgrx13AOtqip42ATZP7+kYl53GvDV6+4dCek1mVUo8z8c8Xnw/mx2q3d9HWh3griuesQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.7.7", + "@babel/helpers": "^7.7.4", + "@babel/parser": "^7.7.7", + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "json5": "^2.1.0", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "@babel/parser": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", + "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.7.tgz", + "integrity": "sha512-/AOIBpHh/JU1l0ZFS4kiRCBnLi6OTHzh0RPk3h9isBxkkqELtQNFi1Vr/tiG9p1yfoUdKVwISuXWQR+hwwM4VQ==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz", + "integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.7.4", + "@babel/template": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz", + "integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz", + "integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helpers": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.7.4.tgz", + "integrity": "sha512-ak5NGZGJ6LV85Q1Zc9gn2n+ayXOizryhjSUBTdu5ih1tlVCJeuQENzc4ItyCVhINVXvIT/ZQ4mheGIsfBkpskg==", + "dev": true, + "requires": { + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/highlight": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", + "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + } + } + }, "@babel/parser": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==", "dev": true }, + "@babel/template": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", + "integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.4", + "@babel/types": "^7.7.4" + }, + "dependencies": { + "@babel/parser": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", + "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", + "dev": true + } + } + }, + "@babel/traverse": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz", + "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.7.4", + "@babel/helper-function-name": "^7.7.4", + "@babel/helper-split-export-declaration": "^7.7.4", + "@babel/parser": "^7.7.4", + "@babel/types": "^7.7.4", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "@babel/parser": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", + "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "@babel/types": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz", + "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", + "integrity": "sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "@istanbuljs/nyc-config-typescript": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.1.tgz", + "integrity": "sha512-/gz6LgVpky205LuoOfwEZmnUtaSmdk0QIMcNFj9OvxhiMhPpKftMgZmGN7jNj7jR+lr8IB1Yks3QSSSNSxfoaQ==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2" + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, "@types/chai": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", @@ -25,6 +316,12 @@ "@types/chai": "*" } }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, "@types/dateformat": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/dateformat/-/dateformat-3.0.0.tgz", @@ -70,6 +367,28 @@ } } }, + "aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "ansi-colors": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", @@ -94,10 +413,19 @@ "integrity": "sha1-ZBqlXft9am8KgUHEucCqULbCTdU=", "dev": true }, - "arg": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.2.tgz", - "integrity": "sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg==", + "append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "requires": { + "default-require-extensions": "^3.0.0" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, "argparse": { @@ -114,6 +442,21 @@ "resolved": "https://registry.npmjs.org/arraybuffer-to-buffer/-/arraybuffer-to-buffer-0.0.5.tgz", "integrity": "sha512-0trREOEceZLcLWDwAStIoEARDLXw8OZsUgot15FQkHT+ey6MG54yt4aWWsAaZfI55BDEIeAHd2Ua7K3I1pwlIQ==" }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, "assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -126,6 +469,24 @@ "integrity": "sha512-dP6vhvatex3Q+OThhvcyGRvHn4noQBg1b8lCNKUAFL05up80hr2pAExveU3YQNDGMhfNPhQit/vzIkkvBPbSXw==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.0.tgz", + "integrity": "sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==", + "dev": true + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -158,6 +519,15 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, "big-integer": { "version": "1.6.42", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.42.tgz", @@ -191,12 +561,47 @@ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", "dev": true }, + "caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "requires": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "dependencies": { + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "camelcase": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "dev": true }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, "chai": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", @@ -263,6 +668,12 @@ "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", "dev": true }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -333,6 +744,15 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", @@ -351,12 +771,52 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "coveralls": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.9.tgz", + "integrity": "sha512-nNBg3B1+4iDox5A5zqHKzUTiwl2ey4k2o0NEcVZYvl+GOSJdKBj4AJGKLv6h3SvWch7tABHePAQOSZWM9E2hMg==", + "dev": true, + "requires": { + "js-yaml": "^3.13.1", + "lcov-parse": "^1.0.0", + "log-driver": "^1.2.7", + "minimist": "^1.2.0", + "request": "^2.88.0" + }, + "dependencies": { + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -369,6 +829,15 @@ "which": "^1.2.9" } }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, "dateformat": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", @@ -410,6 +879,15 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "dev": true, + "requires": { + "strip-bom": "^4.0.0" + } + }, "defaults": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", @@ -428,6 +906,12 @@ "object-keys": "^1.0.12" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, "dependency-tree": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/dependency-tree/-/dependency-tree-7.0.2.tgz", @@ -602,6 +1086,16 @@ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -661,6 +1155,12 @@ "is-symbol": "^1.0.2" } }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -720,16 +1220,40 @@ "strip-eof": "^1.0.0" } }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, - "file-exists-dazinatorfork": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/file-exists-dazinatorfork/-/file-exists-dazinatorfork-1.0.2.tgz", - "integrity": "sha512-r70c72ln2YHzQINNfxDp02hAhbGkt1HffZ+Du8oetWDLjDtFja/Lm10lUaSh9e+wD+7VDvPee0b0C9SAy8pWZg==", + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "file-exists-dazinatorfork": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/file-exists-dazinatorfork/-/file-exists-dazinatorfork-1.0.2.tgz", + "integrity": "sha512-r70c72ln2YHzQINNfxDp02hAhbGkt1HffZ+Du8oetWDLjDtFja/Lm10lUaSh9e+wD+7VDvPee0b0C9SAy8pWZg==", "dev": true }, "filing-cabinet": { @@ -781,6 +1305,34 @@ "traverse-chain": "~0.1.0" } }, + "find-cache-dir": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.2.0.tgz", + "integrity": "sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.0", + "pkg-dir": "^4.1.0" + }, + "dependencies": { + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -805,6 +1357,82 @@ "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", "dev": true }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", + "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fromentries": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.0.tgz", + "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -853,6 +1481,15 @@ "pump": "^3.0.0" } }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, "glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", @@ -867,6 +1504,12 @@ "path-is-absolute": "^1.0.0" } }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, "gonzales-pe": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.2.4.tgz", @@ -905,6 +1548,22 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -935,12 +1594,47 @@ "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", "dev": true }, + "hasha": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.1.0.tgz", + "integrity": "sha512-OFPDWmzPN1l7atOV1TgBVmNtBxaIysToK6Ve9DK+vT6pYuklw/nPNT+HJbZi0KDcI6vWB+9tgvZ5YD7fA3CXcA==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + } + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "html-escaper": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.0.tgz", + "integrity": "sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -949,6 +1643,18 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, "indexes-of": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", @@ -1047,12 +1753,24 @@ "has-symbols": "^1.0.0" } }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, "is-url": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", "dev": true }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1064,6 +1782,204 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "requires": { + "append-transform": "^2.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.0.tgz", + "integrity": "sha512-Nm4wVHdo7ZXSG30KjZ2Wl5SU/Bw7bDx1PdaiIFzEStdjs0H12mOTncn1GVYuqQSaZxpg87VGBRsVRPGD2cD1AQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@babel/parser": "^7.7.5", + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "@babel/parser": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", + "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", + "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-2osTcC8zcOSUkImzN2EWQta3Vdi4WjjKw99P2yWx5mLnigAM0Rd5uYFn1cf2i/Ois45GkNjaoTqc5CxgMSX80A==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -1080,6 +1996,65 @@ "esprima": "^4.0.0" } }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", + "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, "lcid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-3.0.0.tgz", @@ -1088,6 +2063,12 @@ "invert-kv": "^3.0.0" } }, + "lcov-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha1-6w1GtUER68VhrLTECO+TY73I9+A=", + "dev": true + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -1114,12 +2095,24 @@ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, "lodash.unescape": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", "dev": true }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true + }, "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", @@ -1180,12 +2173,6 @@ "semver": "^5.6.0" } }, - "make-error": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", - "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", - "dev": true - }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -1214,6 +2201,21 @@ "readable-stream": "^2.0.1" } }, + "mime-db": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", + "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.25", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz", + "integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==", + "dev": true, + "requires": { + "mime-db": "1.42.0" + } + }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -1452,20 +2454,383 @@ "cliui": "^4.0.0", "find-up": "^3.0.0", "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.0.0" + } + }, + "yargs-parser": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", + "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "mocha-typescript": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/mocha-typescript/-/mocha-typescript-1.1.17.tgz", + "integrity": "sha512-Ge6pCQkZumkkhxVNdAf3JxunskShgaynCb30HYD7TT1Yhog/7NW2+6w5RcRHI+nuQrCMTX6z1+qf2pD8qwCoQA==", + "dev": true, + "requires": { + "@types/mocha": "^5.2.0", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "yargs": "^11.0.0" + } + }, + "module-definition": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-3.2.0.tgz", + "integrity": "sha512-PO6o0BajpdRR+fb3FUSeDISgJpnyxg8UDUEalR8LPQajl0M5+m4jHWhgrMGGSEl6D9+sVl/l1fjOCvpBXIQ+2Q==", + "dev": true, + "requires": { + "ast-module-types": "^2.4.0", + "node-source-walk": "^4.0.0" + } + }, + "module-lookup-amd": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/module-lookup-amd/-/module-lookup-amd-6.2.0.tgz", + "integrity": "sha512-uxHCj5Pw9psZiC1znjU2qPsubt6haCSsN9m7xmIdoTciEgfxUkE1vhtDvjHPuOXEZrVJhjKgkmkP+w73rRuelQ==", + "dev": true, + "requires": { + "commander": "^2.8.1", + "debug": "^4.1.0", + "file-exists-dazinatorfork": "^1.0.2", + "find": "^0.3.0", + "requirejs": "^2.3.5", + "requirejs-config-file": "^3.1.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "node-environment-flags": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.4.tgz", + "integrity": "sha512-M9rwCnWVLW7PX+NUWe3ejEdiLYinRpsEre9hMkU/6NS4h+EEulYaDH1gCEZ2gyXsmw+RXYDaV2JkkTNcsPDJ0Q==", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "requires": { + "process-on-spawn": "^1.0.0" + } + }, + "node-source-walk": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/node-source-walk/-/node-source-walk-4.2.0.tgz", + "integrity": "sha512-hPs/QMe6zS94f5+jG3kk9E7TNm4P2SulrKiLWMzKszBfNZvL/V6wseHlTd7IvfW0NZWqPtK3+9yYNr+3USGteA==", + "dev": true, + "requires": { + "@babel/parser": "^7.0.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "nyc": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.0.0.tgz", + "integrity": "sha512-qcLBlNCKMDVuKb7d1fpxjPR8sHeMVX0CHarXAVzrVWoFrigCkYR8xcrjfXSPi5HXM7EU78L6ywO7w1c5rZNCNg==", + "dev": true, + "requires": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.0", + "js-yaml": "^3.13.1", + "make-dir": "^3.0.0", + "node-preload": "^0.2.0", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "uuid": "^3.3.3", + "yargs": "^15.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.0.tgz", + "integrity": "sha512-7kFQgnEaMdRtwf6uSfUnVr9gSGC7faurn+J/Mv90/W+iTtN0405/nLdopfMWwchyxhbGYl6TC4Sccn9TUkGAgg==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.0.2.tgz", + "integrity": "sha512-GH/X/hYt+x5hOat4LMnCqMd8r5Cv78heOMIJn1hr7QPPBqfeC6p89Y78+WB9yGDvfpCvgasfmWLzNzEioOUD9Q==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^3.0.0", + "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" + "yargs-parser": "^16.1.0" } }, "yargs-parser": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", - "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-16.1.0.tgz", + "integrity": "sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -1474,94 +2839,10 @@ } } }, - "mocha-typescript": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/mocha-typescript/-/mocha-typescript-1.1.17.tgz", - "integrity": "sha512-Ge6pCQkZumkkhxVNdAf3JxunskShgaynCb30HYD7TT1Yhog/7NW2+6w5RcRHI+nuQrCMTX6z1+qf2pD8qwCoQA==", - "dev": true, - "requires": { - "@types/mocha": "^5.2.0", - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", - "yargs": "^11.0.0" - } - }, - "module-definition": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-3.2.0.tgz", - "integrity": "sha512-PO6o0BajpdRR+fb3FUSeDISgJpnyxg8UDUEalR8LPQajl0M5+m4jHWhgrMGGSEl6D9+sVl/l1fjOCvpBXIQ+2Q==", - "dev": true, - "requires": { - "ast-module-types": "^2.4.0", - "node-source-walk": "^4.0.0" - } - }, - "module-lookup-amd": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/module-lookup-amd/-/module-lookup-amd-6.2.0.tgz", - "integrity": "sha512-uxHCj5Pw9psZiC1znjU2qPsubt6haCSsN9m7xmIdoTciEgfxUkE1vhtDvjHPuOXEZrVJhjKgkmkP+w73rRuelQ==", - "dev": true, - "requires": { - "commander": "^2.8.1", - "debug": "^4.1.0", - "file-exists-dazinatorfork": "^1.0.2", - "find": "^0.3.0", - "requirejs": "^2.3.5", - "requirejs-config-file": "^3.1.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" - }, - "node-environment-flags": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.4.tgz", - "integrity": "sha512-M9rwCnWVLW7PX+NUWe3ejEdiLYinRpsEre9hMkU/6NS4h+EEulYaDH1gCEZ2gyXsmw+RXYDaV2JkkTNcsPDJ0Q==", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "node-source-walk": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/node-source-walk/-/node-source-walk-4.2.0.tgz", - "integrity": "sha512-hPs/QMe6zS94f5+jG3kk9E7TNm4P2SulrKiLWMzKszBfNZvL/V6wseHlTd7IvfW0NZWqPtK3+9yYNr+3USGteA==", - "dev": true, - "requires": { - "@babel/parser": "^7.0.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "^2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "dev": true }, "object-keys": { @@ -1705,12 +2986,33 @@ "p-limit": "^1.1.0" } }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, "p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, + "package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, "parse-ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", @@ -1746,12 +3048,78 @@ "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", "dev": true }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, "pluralize": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", @@ -1850,6 +3218,15 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "requires": { + "fromentries": "^1.2.0" + } + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -1862,6 +3239,12 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "psl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", + "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==", + "dev": true + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -1871,6 +3254,18 @@ "once": "^1.3.1" } }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -1906,6 +3301,43 @@ "util-deprecate": "~1.0.1" } }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -1950,6 +3382,12 @@ "integrity": "sha512-DIgu+0Dv+6v2XwRaNWnumKu7GPufBBOr5I1gRPJHkvghrfCGOooJODFvgFimX/KRxk9j0whD2MnKHzM1jYvk9w==", "dev": true }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -1960,6 +3398,15 @@ "signal-exit": "^3.0.2" } }, + "rimraf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -2025,12 +3472,69 @@ "source-map": "^0.6.0" } }, + "spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "requires": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "dependencies": { + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, "stream-buffers": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz", @@ -2093,6 +3597,12 @@ "ansi-regex": "^2.0.0" } }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -2143,33 +3653,63 @@ "integrity": "sha1-ZxrWPVe+D+nXKUZks/xABjZnimA=", "dev": true }, - "traverse-chain": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", - "integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=", + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, - "ts-node": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.5.4.tgz", - "integrity": "sha512-izbVCRV68EasEPQ8MSIGBNK9dc/4sYJJKYA+IarMQct1RtEot6Xp0bXuClsbUSnKpg50ho+aOAx8en5c+y4OFw==", + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "dev": true, "requires": { - "arg": "^4.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "source-map-support": "^0.5.6", - "yn": "^3.0.0" + "psl": "^1.1.24", + "punycode": "^1.4.1" }, "dependencies": { - "diff": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", - "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", "dev": true } } }, + "traverse-chain": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", + "integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=", + "dev": true + }, "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", @@ -2206,6 +3746,21 @@ "tslib": "^1.8.1" } }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -2221,6 +3776,21 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, "typemoq": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz", @@ -2244,12 +3814,38 @@ "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", "dev": true }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "walkdir": { "version": "0.0.12", "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.12.tgz", @@ -2331,6 +3927,18 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "write-file-atomic": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.1.tgz", + "integrity": "sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "y18n": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", @@ -2605,12 +4213,6 @@ } } } - }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true } } } diff --git a/package.json b/package.json index 5a7a4de9..1e9284cb 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "tsc -p ./", "madge": "node node_modules/madge/bin/cli.js --warning --circular --extension ts ./", - "test": "mocha --require ts-node/register ./test/**/*.ts" + "test": "mocha" }, "dependencies": { "arraybuffer-to-buffer": "^0.0.5", @@ -15,6 +15,7 @@ "os-locale": "^4.0.0" }, "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.1", "@types/chai": "^4.1.7", "@types/chai-as-promised": "^7.1.0", "@types/dateformat": "^3.0.0", @@ -22,10 +23,13 @@ "@types/stream-buffers": "^3.0.2", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", + "coveralls": "^3.0.9", "madge": "^3.4.4", "mocha": "^6.1.2", "mocha-typescript": "^1.1.17", + "nyc": "^15.0.0", "stream-buffers": "^3.0.2", + "source-map-support": "^0.5.16", "ts-node": "^8.5.4", "tslint": "^5.13.1", "typemoq": "^2.1.0", diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 00000000..efb568a0 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,3 @@ +--require ts-node/register +--require source-map-support/register +./test/**/*.ts \ No newline at end of file From 80ef5ad9caf80832c0093a7557f91e21511fd25b Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 31 Dec 2019 20:30:25 -0500 Subject: [PATCH 14/71] Changing the options for mocha to match what is run by webstorm --- package.json | 1 + test/mocha.opts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e9284cb..f7ada4c3 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "license": "LGPL-2.1-or-later", "scripts": { "build": "tsc -p ./", + "coverage": "nyc mocha", "madge": "node node_modules/madge/bin/cli.js --warning --circular --extension ts ./", "test": "mocha" }, diff --git a/test/mocha.opts b/test/mocha.opts index efb568a0..9665c28b 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,3 +1,5 @@ --require ts-node/register --require source-map-support/register -./test/**/*.ts \ No newline at end of file +--ui mocha-typescript +--recursive +dist/test/ \ No newline at end of file From a18ce0129e727e8bd6097928b3341d7d742389dd Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 31 Dec 2019 20:46:43 -0500 Subject: [PATCH 15/71] First attempt at code coverage on appveyor --- appveyor.yml | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 57b89732..ba7707c5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,7 +9,7 @@ build_script: - npm run build test_script: - - npm test + - npm run test - - +after_test: + - npm run coverage diff --git a/package.json b/package.json index f7ada4c3..cb0ab4d2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "LGPL-2.1-or-later", "scripts": { "build": "tsc -p ./", - "coverage": "nyc mocha", + "coverage": "nyc report --reporter=text-lcov | coveralls", "madge": "node node_modules/madge/bin/cli.js --warning --circular --extension ts ./", "test": "mocha" }, From 64ba6741808fa3629ce302a9b71356c4d1152766 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 31 Dec 2019 19:40:57 -0500 Subject: [PATCH 16/71] Finally fixing that broken test for text information frame encoding --- src/id3v2/frames/textInformationFrame.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/id3v2/frames/textInformationFrame.ts b/src/id3v2/frames/textInformationFrame.ts index 9d4458f6..e8d86c19 100644 --- a/src/id3v2/frames/textInformationFrame.ts +++ b/src/id3v2/frames/textInformationFrame.ts @@ -132,7 +132,6 @@ export class TextInformationFrame extends Frame { private static REMIX_STRING = "Remix"; protected _encoding: StringType = Id3v2TagSettings.defaultEncoding; - protected _rawEncoding: StringType = StringType.Latin1; protected _rawData: ByteVector; protected _rawVersion: number; protected _textFields: string[] = []; @@ -215,7 +214,7 @@ export class TextInformationFrame extends Frame { * Sets the text contained in the current instance. */ public set text(value: string[]) { - this._rawData = undefined; + this.parseRawData(); this._textFields = value ? value.slice() : []; } @@ -231,11 +230,8 @@ export class TextInformationFrame extends Frame { * This value will be overridden if {@see Id3v2Tag.forceDefaultEncoding} is `true`. */ public set textEncoding(value: StringType) { - if (this._rawEncoding) { - this._rawEncoding = value; - } else { - this._encoding = value; - } + this.parseRawData(); + this._encoding = value; } // #endregion @@ -328,9 +324,6 @@ export class TextInformationFrame extends Frame { protected parseFields(data: ByteVector, version: number): void { this._rawData = data; this._rawVersion = version; - - // Read the string data type (first byte of the field data) - this._rawEncoding = data.get(0); } /** @@ -450,7 +443,7 @@ export class TextInformationFrame extends Frame { /** @inheritDoc */ protected renderFields(version: number): ByteVector { - if (this._rawData && this._rawVersion === version && this._rawEncoding === Id3v2TagSettings.defaultEncoding) { + if (this._rawData && this._rawVersion === version) { return this._rawData; } From 4f00ce87d9d0afaa220cd1aef4f5cae2703e7cd9 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Wed, 1 Jan 2020 19:55:03 -0500 Subject: [PATCH 17/71] Event time code tests --- src/id3v2/frames/eventTimeCode.ts | 33 ------- src/id3v2/frames/eventTimeCodeFrame.ts | 56 ++++++++--- src/id3v2/frames/frameFactory.ts | 2 +- test/id3v2/eventTimeCodeFrameTests.ts | 131 +++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 49 deletions(-) delete mode 100644 src/id3v2/frames/eventTimeCode.ts create mode 100644 test/id3v2/eventTimeCodeFrameTests.ts diff --git a/src/id3v2/frames/eventTimeCode.ts b/src/id3v2/frames/eventTimeCode.ts deleted file mode 100644 index d918daea..00000000 --- a/src/id3v2/frames/eventTimeCode.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {Guards} from "../../utils"; -import {EventType} from "../utilTypes"; - -export default class EventTimeCode { - private _time: number; - private _eventType: EventType; - - public constructor(eventType: EventType, time: number) { - Guards.int(time, "time"); - this._eventType = eventType; - this._time = time; - } - - public static fromEmpty(): EventTimeCode { - return new EventTimeCode(EventType.Padding, 0); - } - - public get time(): number { return this._time; } - public set time(value: number) { - Guards.int(value, "value"); - this._time = value; - } - - public get eventType(): EventType { return this._eventType; } - public set eventType(value: EventType) { this._eventType = value; } - - /** - * Creates a copy of this instance - */ - public clone(): EventTimeCode { - return new EventTimeCode(this.eventType, this.time); - } -} diff --git a/src/id3v2/frames/eventTimeCodeFrame.ts b/src/id3v2/frames/eventTimeCodeFrame.ts index c22ac1cd..6fc66852 100644 --- a/src/id3v2/frames/eventTimeCodeFrame.ts +++ b/src/id3v2/frames/eventTimeCodeFrame.ts @@ -1,14 +1,44 @@ -import EventTimeCode from "./eventTimeCode"; import FrameTypes from "../frameTypes"; import {ByteVector} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameFlags, Id3v2FrameHeader} from "./frameHeader"; import {Guards} from "../../utils"; -import {TimestampFormat} from "../utilTypes"; +import {EventType, TimestampFormat} from "../utilTypes"; -export default class EventTimeCodeFrame extends Frame { - private _events: EventTimeCode[]; - private _timestampFormat: TimestampFormat; +export class EventTimeCode { + private _time: number; + private _eventType: EventType; + + public constructor(eventType: EventType, time: number) { + Guards.int(time, "time"); + this._eventType = eventType; + this._time = time; + } + + public static fromEmpty(): EventTimeCode { + return new EventTimeCode(EventType.Padding, 0); + } + + public get time(): number { return this._time; } + public set time(value: number) { + Guards.int(value, "value"); + this._time = value; + } + + public get eventType(): EventType { return this._eventType; } + public set eventType(value: EventType) { this._eventType = value; } + + /** + * Creates a copy of this instance + */ + public clone(): EventTimeCode { + return new EventTimeCode(this.eventType, this.time); + } +} + +export class EventTimeCodeFrame extends Frame { + private _events: EventTimeCode[] = []; + private _timestampFormat: TimestampFormat = TimestampFormat.Unknown; // #region Constructors @@ -113,21 +143,17 @@ export default class EventTimeCodeFrame extends Frame { /** @inheritDoc */ protected parseFields(data: ByteVector, version: number): void { - Guards.truthy(data, "data"); - Guards.byte(version, "version"); - this._events = []; this._timestampFormat = data.get(0); - const incomingEventsData = data.mid(1); - for (let i = 0; i < incomingEventsData.length; i += 5) { - const eventType = incomingEventsData.get(i); + for (let i = 1; i < data.length; i += 5) { + const eventType = data.get(i); const timestampData = ByteVector.concatenate( - incomingEventsData.get(i + 1), - incomingEventsData.get(i + 2), - incomingEventsData.get(i + 3), - incomingEventsData.get(i + 4) + data.get(i + 1), + data.get(i + 2), + data.get(i + 3), + data.get(i + 4) ); const timestamp = timestampData.toInt(); diff --git a/src/id3v2/frames/frameFactory.ts b/src/id3v2/frames/frameFactory.ts index 502fc7b1..343e31fc 100644 --- a/src/id3v2/frames/frameFactory.ts +++ b/src/id3v2/frames/frameFactory.ts @@ -17,8 +17,8 @@ import PopularimeterFrame from "./popularimeterFrame"; import UnknownFrame from "./unknownFrame"; import TermsOfUseFrame from "./termsOfUseFrame"; import PrivateFrame from "./privateFrame"; +import {EventTimeCodeFrame} from "./eventTimeCodeFrame"; import {UrlLinkFrame, UserUrlLinkFrame} from "./urlLinkFrame"; -import EventTimeCodeFrame from "./eventTimeCodeFrame"; import {NotImplementedError} from "../../errors"; const customFrameCreators: Array<(data: ByteVector, offset: number, header: Id3v2FrameHeader, version: number) => Frame> diff --git a/test/id3v2/eventTimeCodeFrameTests.ts b/test/id3v2/eventTimeCodeFrameTests.ts new file mode 100644 index 00000000..f3f1a57c --- /dev/null +++ b/test/id3v2/eventTimeCodeFrameTests.ts @@ -0,0 +1,131 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import FrameConstructorTests from "./frameConstructorTests"; +import FramePropertiesTests from "./framePropertiesTests"; +import FrameTypes from "../../src/id3v2/frameTypes"; +import {ByteVector} from "../../src/byteVector"; +import {EventTimeCode, EventTimeCodeFrame} from "../../src/id3v2/frames/eventTimeCodeFrame"; +import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; +import {Id3v2FrameFlags, Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {EventType, TimestampFormat} from "../../src/id3v2/utilTypes"; + +// Setup Chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(timeout(3000), slow(1000)) +class EventTimeCodeTests extends FramePropertiesTests { + @test + public constructor_invalidTime() { + // Act/Assert + assert.throws(() => { const _ = new EventTimeCode(EventType.AudioEnd, 1.23); }); + assert.throws(() => { const _ = new EventTimeCode(EventType.AudioEnd, Number.MAX_SAFE_INTEGER + 1); }); + } + + @test + public construct_valid() { + // Act + const output = new EventTimeCode(EventType.AudioFileEnd, 123); + + // Assert + assert.strictEqual(output.eventType, EventType.AudioFileEnd); + assert.strictEqual(output.time, 123); + } + + @test + public fromEmpty() { + // Act + const output = EventTimeCode.fromEmpty(); + + // Assert + assert.strictEqual(output.eventType, EventType.Padding); + assert.strictEqual(output.time, 0); + } + + @test + public time_invalid() { + // Arrange + const output = EventTimeCode.fromEmpty(); + + // Act/Assert + assert.throws(() => { output.time = 1.23; }); + assert.throws(() => { output.time = Number.MAX_SAFE_INTEGER + 1; }); + } + + @test + public time_valid() { + // Arrange + const output = EventTimeCode.fromEmpty(); + + // Act/Assert + this.propertyRoundTrip((v) => { output.time = v; }, () => output.time, 123); + } + + @test + public eventType() { + // Arrange + const output = EventTimeCode.fromEmpty(); + + // Act/Assert + this.propertyRoundTrip((v) => { output.eventType = v; }, () => output.eventType, EventType.IntroEnd); + } +} + +@suite(timeout(3000), slow(1000)) +class EventTimeCodeFrameConstructorTests extends FrameConstructorTests { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + return EventTimeCodeFrame.fromOffsetRawData; + } + + public get fromRawData(): (d: ByteVector, v: number) => Frame { + return EventTimeCodeFrame.fromRawData; + } + + @test + public fromEmpty() { + // Act + const output = EventTimeCodeFrame.fromEmpty(); + + // Assert + this.assertFrame(output, [], TimestampFormat.Unknown); + } + + @test + public fromTimestampFormat() { + // Act + const output = EventTimeCodeFrame.fromTimestampFormat(TimestampFormat.AbsoluteMilliseconds); + + // Assert + this.assertFrame(output, [], TimestampFormat.AbsoluteMilliseconds); + } + + @test + public fromRawData_noEvents() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.ETCO, 4); + header.frameSize = 1; + const data = ByteVector.concatenate( + header.render(4), + TimestampFormat.AbsoluteMilliseconds + ); + + // Act + const frame = EventTimeCodeFrame.fromRawData(data, 4); + + // Assert + this.assertFrame(frame, [], TimestampFormat.AbsoluteMilliseconds); + } + + private assertFrame(frame: EventTimeCodeFrame, e: EventTimeCode[], t: TimestampFormat) { + assert.isOk(frame); + assert.strictEqual(frame.frameClassType, FrameClassType.EventTimeCodeFrame); + assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.ETCO)); + assert.isTrue((frame.flags | Id3v2FrameFlags.FileAlterPreservation) > 0); + + assert.deepStrictEqual(frame.events, e); + assert.strictEqual(frame.timestampFormat, t); + } +} + From c6a667ce889e5078ae54f40da514b94d79028d65 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Wed, 1 Jan 2020 20:20:37 -0500 Subject: [PATCH 18/71] 2nd attempt at coveralls --- appveyor.yml | 4 ++-- package-lock.json | 39 +++++++++++++++++++++++++++++++++++++++ package.json | 4 ++-- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index ba7707c5..4466d974 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,7 +9,7 @@ build_script: - npm run build test_script: - - npm run test + - npm run test-with-coverage after_test: - - npm run coverage + - nyc report --reporter=text-lcov | coveralls \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f073e3a8..d39eecdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -428,6 +428,12 @@ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, + "arg": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.2.tgz", + "integrity": "sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2173,6 +2179,12 @@ "semver": "^5.6.0" } }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -3710,6 +3722,27 @@ "integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=", "dev": true }, + "ts-node": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.5.4.tgz", + "integrity": "sha512-izbVCRV68EasEPQ8MSIGBNK9dc/4sYJJKYA+IarMQct1RtEot6Xp0bXuClsbUSnKpg50ho+aOAx8en5c+y4OFw==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + }, + "dependencies": { + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "dev": true + } + } + }, "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", @@ -4213,6 +4246,12 @@ } } } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true } } } diff --git a/package.json b/package.json index cb0ab4d2..78969168 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "license": "LGPL-2.1-or-later", "scripts": { "build": "tsc -p ./", - "coverage": "nyc report --reporter=text-lcov | coveralls", "madge": "node node_modules/madge/bin/cli.js --warning --circular --extension ts ./", - "test": "mocha" + "test": "mocha", + "test-with-coverage": "nyc mocha" }, "dependencies": { "arraybuffer-to-buffer": "^0.0.5", From 090636e698374f6c5f262fd3a984e667628cc80a Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Wed, 1 Jan 2020 20:24:52 -0500 Subject: [PATCH 19/71] 3rd attempt at coveralls :hammer: --- appveyor.yml | 2 +- package.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 4466d974..47b8ca7f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,4 +12,4 @@ test_script: - npm run test-with-coverage after_test: - - nyc report --reporter=text-lcov | coveralls \ No newline at end of file + - npm run publish-coverage \ No newline at end of file diff --git a/package.json b/package.json index 78969168..01d6c609 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "build": "tsc -p ./", "madge": "node node_modules/madge/bin/cli.js --warning --circular --extension ts ./", + "publish-coverage": "nyc report --reporter=text-lcov | coveralls", "test": "mocha", "test-with-coverage": "nyc mocha" }, From cbd616ef6cf5c26e7e156d24e90c289399c102ca Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Wed, 1 Jan 2020 20:48:45 -0500 Subject: [PATCH 20/71] Adding badges --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 05cdbb4b..c003176c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ | Master | Develop | Latest | |--------|---------|--------| -|[![BuildStatus](https://ci.appveyor.com/api/projects/status/7hdfrbc4ecvvruwv/branch/master?svg=true)](https://ci.appveyor.com/project/benrr101/node-taglib-sharp/branch/master)|[![Build Status](https://ci.appveyor.com/api/projects/status/7hdfrbc4ecvvruwv/branch/develop?svg=true)](https://ci.appveyor.com/project/benrr101/node-taglib-sharp/branch/develop)|[![Build Status](https://ci.appveyor.com/api/projects/status/7hdfrbc4ecvvruwv?svg=true)](https://ci.appveyor.com/project/benrr101/node-taglib-sharp) +|[![Build Status](https://ci.appveyor.com/api/projects/status/7hdfrbc4ecvvruwv/branch/master?svg=true)](https://ci.appveyor.com/project/benrr101/node-taglib-sharp/branch/master)|[![Build Status](https://ci.appveyor.com/api/projects/status/7hdfrbc4ecvvruwv/branch/develop?svg=true)](https://ci.appveyor.com/project/benrr101/node-taglib-sharp/branch/develop)|[![Build Status](https://ci.appveyor.com/api/projects/status/7hdfrbc4ecvvruwv?svg=true)](https://ci.appveyor.com/project/benrr101/node-taglib-sharp) +|[![Coverage Status](https://coveralls.io/repos/github/benrr101/node-taglib-sharp/badge.svg?branch=master)](https://coveralls.io/github/benrr101/node-taglib-sharp?branch=feature/coveralls)|[![Coverage Status](https://coveralls.io/repos/github/benrr101/node-taglib-sharp/badge.svg?branch=feature/coveralls)](https://coveralls.io/github/benrr101/node-taglib-sharp?branch=develop)|[![Coverage Status](https://coveralls.io/repos/github/benrr101/node-taglib-sharp/badge.svg?latest)](https://coveralls.io/github/benrr101/node-taglib-sharp) ## Description TagLib# is a .NET library that has been around for years. It provides a unified interface for From 7ef653089dfc1c329d02a6cc8b0f0db59cd6bb68 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Wed, 1 Jan 2020 22:00:58 -0500 Subject: [PATCH 21/71] Finishing up eventtimecode frames --- src/id3v2/frames/eventTimeCodeFrame.ts | 27 +++-- test/id3v2/eventTimeCodeFrameTests.ts | 159 +++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 10 deletions(-) diff --git a/src/id3v2/frames/eventTimeCodeFrame.ts b/src/id3v2/frames/eventTimeCodeFrame.ts index 6fc66852..453ad657 100644 --- a/src/id3v2/frames/eventTimeCodeFrame.ts +++ b/src/id3v2/frames/eventTimeCodeFrame.ts @@ -34,6 +34,14 @@ export class EventTimeCode { public clone(): EventTimeCode { return new EventTimeCode(this.eventType, this.time); } + + public render(): ByteVector { + // @TODO: Do we need to store 0 time as one byte 0? It's in the docs like that + return ByteVector.concatenate( + this.eventType, + ByteVector.fromInt(this.time) + ); + } } export class EventTimeCodeFrame extends Frame { @@ -114,7 +122,7 @@ export class EventTimeCodeFrame extends Frame { * Gets the event this frame contains. Each {@see EventTimeCode} represents a single event at a * certain point in time. */ - public get events(): EventTimeCode[] { return this._events; } + public get events(): EventTimeCode[] { return this._events || []; } /** * Sets the event this frame contains */ @@ -163,15 +171,14 @@ export class EventTimeCodeFrame extends Frame { /** @inheritDoc */ protected renderFields(version: number): ByteVector { - const data: Array = [this.timestampFormat]; - - for (const event of this.events) { - const timeData = ByteVector.fromInt(event.time); - data.push(event.eventType); - data.push(timeData); - } - - return ByteVector.concatenate(... data); + // Docs state event codes must be sorted chronologically + const events = this.events.sort((a, b) => a.time - b.time) + .map((e) => e.render()); + + return ByteVector.concatenate( + this.timestampFormat, + ... events + ); } // #endregion diff --git a/test/id3v2/eventTimeCodeFrameTests.ts b/test/id3v2/eventTimeCodeFrameTests.ts index f3f1a57c..0623b260 100644 --- a/test/id3v2/eventTimeCodeFrameTests.ts +++ b/test/id3v2/eventTimeCodeFrameTests.ts @@ -71,6 +71,22 @@ class EventTimeCodeTests extends FramePropertiesTests { // Act/Assert this.propertyRoundTrip((v) => { output.eventType = v; }, () => output.eventType, EventType.IntroEnd); } + + @test + public render() { + // Arrange + const etc = new EventTimeCode(EventType.KeyChange, 123); + + // Act + const output = etc.render(); + + // Assert + const expected = ByteVector.concatenate( + EventType.KeyChange, + ByteVector.fromInt(123) + ); + assert.isTrue(ByteVector.equal(output, expected)); + } } @suite(timeout(3000), slow(1000)) @@ -118,6 +134,67 @@ class EventTimeCodeFrameConstructorTests extends FrameConstructorTests { this.assertFrame(frame, [], TimestampFormat.AbsoluteMilliseconds); } + @test + public fromRawData_withEvents() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.ETCO, 4); + header.frameSize = 11; + const event1 = new EventTimeCode(EventType.Profanity, 123); + const event2 = new EventTimeCode(EventType.KeyChange, 456); + const data = ByteVector.concatenate( + header.render(4), + TimestampFormat.AbsoluteMilliseconds, + event1.render(), + event2.render() + ); + + // Act + const frame = EventTimeCodeFrame.fromRawData(data, 4); + + // Assert + this.assertFrame(frame, [event1, event2], TimestampFormat.AbsoluteMilliseconds); + } + + @test + public fromOffsetRawData_noEvents() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.ETCO, 4); + header.frameSize = 1; + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, + TimestampFormat.AbsoluteMilliseconds + ); + + // Act + const frame = EventTimeCodeFrame.fromOffsetRawData(data, 2, header); + + // Assert + this.assertFrame(frame, [], TimestampFormat.AbsoluteMilliseconds); + } + + @test + public fromOffsetRawData_withEvents() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.ETCO, 4); + header.frameSize = 11; + const event1 = new EventTimeCode(EventType.Profanity, 123); + const event2 = new EventTimeCode(EventType.KeyChange, 456); + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, + TimestampFormat.AbsoluteMilliseconds, + event1.render(), + event2.render() + ); + + // Act + const frame = EventTimeCodeFrame.fromOffsetRawData(data, 2, header); + + // Assert + this.assertFrame(frame, [event1, event2], TimestampFormat.AbsoluteMilliseconds); + } + private assertFrame(frame: EventTimeCodeFrame, e: EventTimeCode[], t: TimestampFormat) { assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.EventTimeCodeFrame); @@ -129,3 +206,85 @@ class EventTimeCodeFrameConstructorTests extends FrameConstructorTests { } } +@suite(timeout(3000), slow(1000)) +class EventTimeCodeFramePropertyTests extends FramePropertiesTests { + @test + public events() { + // Arrange + const frame = EventTimeCodeFrame.fromTimestampFormat(TimestampFormat.AbsoluteMilliseconds); + const set = (v: EventTimeCode[]) => { frame.events = v; }; + const get = () => frame.events; + + // Act / Assert + this.propertyRoundTrip(set, get, [new EventTimeCode(EventType.Profanity, 123)]); + this.propertyRoundTrip(set, get, []); + this.propertyNormalized(set, get, undefined, []); + this.propertyNormalized(set, get, null, []); + } + + @test + public timeStampFormat() { + // Arrange + const frame = EventTimeCodeFrame.fromTimestampFormat(TimestampFormat.AbsoluteMilliseconds); + + // Act / Assert + this.propertyRoundTrip( + (v) => { frame.timestampFormat = v; }, + () => frame.timestampFormat, + TimestampFormat.AbsoluteMpegFrames); + } +} + +@suite(timeout(3000), slow(1000)) +class EventTimeCodeFrameMethodTests { + @test + public clone() { + // Arrange + const frame = EventTimeCodeFrame.fromTimestampFormat(TimestampFormat.AbsoluteMilliseconds); + const event1 = new EventTimeCode(EventType.Profanity, 123); + const event2 = new EventTimeCode(EventType.ProfanityEnd, 456); + frame.events = [event1, event2]; + + // Act + const output = frame.clone(); + + // Assert + assert.isOk(output); + assert.strictEqual(output.frameClassType, FrameClassType.EventTimeCodeFrame); + assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.ETCO)); + assert.isTrue((output.flags | Id3v2FrameFlags.FileAlterPreservation) > 0); + + assert.deepStrictEqual(output.events, frame.events); + assert.strictEqual(output.timestampFormat, frame.timestampFormat); + } + + @test + public render() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.ETCO, 4); + header.frameSize = 11; + const event2 = new EventTimeCode(EventType.KeyChange, 456); + const event1 = new EventTimeCode(EventType.Profanity, 123); + const data = ByteVector.concatenate( + header.render(4), + TimestampFormat.AbsoluteMilliseconds, + event2.render(), // Force events to be sorted + event1.render() + ); + const frame = EventTimeCodeFrame.fromRawData(data, 4); + + // Act + const output = frame.render(4); + + // Assert + const expected = ByteVector.concatenate( + header.render(4), + TimestampFormat.AbsoluteMilliseconds, + event1.render(), + event2.render() + ); + + assert.isOk(output); + assert.isTrue(ByteVector.equal(output, expected)); + } +} From 36962e4c7afebce3d42ca3ca8fef7e151fde183a Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 4 Jan 2020 14:38:28 -0500 Subject: [PATCH 22/71] Adding unit tests for private frames, fixing a couple bugs --- src/byteVector.ts | 2 +- src/id3v2/frames/frame.ts | 1 - src/id3v2/frames/privateFrame.ts | 24 ++-- test/id3v2/privateFrameTests.ts | 231 +++++++++++++++++++++++++++++++ 4 files changed, 241 insertions(+), 17 deletions(-) create mode 100644 test/id3v2/privateFrameTests.ts diff --git a/src/byteVector.ts b/src/byteVector.ts index 357df4f4..520bfe95 100644 --- a/src/byteVector.ts +++ b/src/byteVector.ts @@ -1097,7 +1097,7 @@ export class ByteVector { let previousOffset = 0; for (let offset = this.find(separator, 0, byteAlign); - offset !== -1 && (max < 1 || max > this.length + 1); + offset !== -1 && (max < 1 || max > list.length + 1); offset = this.find(separator, offset + separator.length, byteAlign)) { list.push(this.mid(previousOffset, offset - previousOffset)); previousOffset = offset + separator.length; diff --git a/src/id3v2/frames/frame.ts b/src/id3v2/frames/frame.ts index d12e4ece..ae89850a 100644 --- a/src/id3v2/frames/frame.ts +++ b/src/id3v2/frames/frame.ts @@ -135,7 +135,6 @@ export abstract class Frame { * overridden by child classes. */ public abstract clone(): Frame; - // return FrameFactory.createFrame(this.render(4), undefined, 0, 4, false).frame; /** * Renders the current instance, encoded in a specified ID3v2 version. diff --git a/src/id3v2/frames/privateFrame.ts b/src/id3v2/frames/privateFrame.ts index 3a8c3473..2b361dc8 100644 --- a/src/id3v2/frames/privateFrame.ts +++ b/src/id3v2/frames/privateFrame.ts @@ -27,6 +27,7 @@ export default class PrivateFrame extends Frame { public static fromOwner(owner: string): PrivateFrame { const frame = new PrivateFrame(new Id3v2FrameHeader(FrameTypes.PRIV, 4)); frame._owner = owner; + frame._privateData = ByteVector.empty(); return frame; } @@ -108,38 +109,31 @@ export default class PrivateFrame extends Frame { /** @inheritDoc */ public clone(): Frame { const frame = PrivateFrame.fromOwner(this.owner); - if (this.privateData) { - frame.privateData = ByteVector.fromByteVector(this.privateData); - } + frame.privateData = ByteVector.fromByteVector(this.privateData); return frame; } /** @inheritDoc */ protected parseFields(data: ByteVector, version: number): void { - Guards.truthy(data, "data"); - Guards.byte(version, "version"); - if (data.length < 1) { throw new CorruptFileError("A private frame must contain at least 1 byte"); } const l = data.split(ByteVector.getTextDelimiter(StringType.Latin1), 1, 2); - if (l.length === 2) { - this._owner = l[0].toString(l[0].length, StringType.Latin1); - this.privateData = l[1]; - } + this._owner = l[0].toString(l[0].length, StringType.Latin1); + this._privateData = l.length === 2 ? l[1] : ByteVector.empty(); } /** @inheritDoc */ protected renderFields(version: number): ByteVector { - Guards.byte(version, "version"); if (version < 3) { throw new NotImplementedError(); } - const v = ByteVector.fromString(this.owner, StringType.Latin1); - v.addByteVector(ByteVector.getTextDelimiter(StringType.Latin1)); - v.addByteVector(this.privateData); - return v; + return ByteVector.concatenate( + ByteVector.fromString(this.owner, StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + this._privateData + ); } } diff --git a/test/id3v2/privateFrameTests.ts b/test/id3v2/privateFrameTests.ts new file mode 100644 index 00000000..6d4eef8a --- /dev/null +++ b/test/id3v2/privateFrameTests.ts @@ -0,0 +1,231 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import ConstructorTests from "./frameConstructorTests"; +import FrameTypes from "../../src/id3v2/frameTypes"; +import PrivateFrame from "../../src/id3v2/frames/privateFrame"; +import PropertyTests from "./framePropertiesTests"; +import {ByteVector, StringType} from "../../src/byteVector"; +import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; +import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(timeout(3000), slow(1000)) +class PrivateFrameConstructorTests extends ConstructorTests { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + return PrivateFrame.fromOffsetRawData; + } + + public get fromRawData(): (d: ByteVector, v: number) => Frame { + return PrivateFrame.fromRawData; + } + + @test + public fromOwner() { + // Act + const frame = PrivateFrame.fromOwner("foo"); + + // Assert + this.assertFrame(frame, "foo", ByteVector.empty()); + } + + @test + public fromRawData_tooFewBytes() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.PRIV, 4); + header.frameSize = 0; + const data = ByteVector.concatenate( + header.render(4) + ); + + // Act / Assert + assert.throws(() => { PrivateFrame.fromRawData(data, 4); }); + } + + @test + public fromRawData_owner() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.PRIV, 4); + header.frameSize = 4; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("fux", StringType.Latin1) + ); + + // Act + const frame = PrivateFrame.fromRawData(data, 4); + + // Assert + this.assertFrame(frame, "fux", ByteVector.empty()); + } + + @test + public fromRawData_ownerAndData() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.PRIV, 4); + header.frameSize = 9; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + 0x01, 0x02, 0x03, 0x04 + ); + + // Act + const frame = PrivateFrame.fromRawData(data, 4); + + // Assert + this.assertFrame(frame, "fux", ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04]))); + } + + @test + public fromOffsetRawData_ownerAndData() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.PRIV, 4); + header.frameSize = 8; + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + 0x01, 0x02, 0x03, 0x04 + ); + + // Act + const frame = PrivateFrame.fromOffsetRawData(data, 2, header); + + // Assert + this.assertFrame(frame, "fux", ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04]))); + } + + private assertFrame(frame: PrivateFrame, o: string, d: ByteVector) { + assert.isOk(frame); + assert.strictEqual(frame.frameClassType, FrameClassType.PrivateFrame); + assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.PRIV)); + + assert.strictEqual(frame.owner, o); + assert.isTrue(ByteVector.equal(frame.privateData, d)); + } +} + +@suite(timeout(3000), slow(1000)) +class PrivateFramePropertyTests extends PropertyTests { + @test + public privateData() { + // Arrange + const frame = PrivateFrame.fromOwner("fux"); + + // Act / Assert + this.propertyRoundTrip( + (v) => { frame.privateData = v; }, + () => frame.privateData, + ByteVector.fromString("bux") + ); + } +} + +@suite(timeout(3000), slow(1000)) +class PrivateFrameMethodTests { + @test + public find_noFrames() { + // Arrange + const frames: PrivateFrame[] = []; + + // Act + const output = PrivateFrame.find(frames, "fux"); + + // Assert + assert.isUndefined(output); + } + + @test + public find_noMatch() { + // Arrange + const frames = [ + PrivateFrame.fromOwner("fux") + ]; + + // Act + const output = PrivateFrame.find(frames, "bux"); + + // Assert + assert.isUndefined(output); + } + + @test + public find_match() { + // Arrange + const frames = [ + PrivateFrame.fromOwner("fux"), + PrivateFrame.fromOwner("bux") + ]; + + // Act + const output = PrivateFrame.find(frames, "bux"); + + // Assert + assert.strictEqual(output, frames[1]); + } + + @test + public clone() { + // Arrange + const frame = PrivateFrame.fromOwner("fux"); + + // Act + const output = frame.clone(); + + // Assert + assert.isOk(output); + assert.notEqual(frame, output); + + assert.strictEqual(output.frameClassType, FrameClassType.PrivateFrame); + assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.PRIV)); + + assert.strictEqual(output.owner, frame.owner); + assert.notEqual(output.privateData, frame.privateData); + assert.isTrue(ByteVector.equal(output.privateData, frame.privateData)); + } + + @test + public render_v4() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.PRIV, 4); + header.frameSize = 8; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + 0x01, 0x02, 0x03, 0x04 + ); + const frame = PrivateFrame.fromRawData(data, 4); + + // Act + const output = frame.render(4); + + // Assert + assert.isOk(output); + assert.isTrue(ByteVector.equal(output, data)); + } + + @test + public render_v2_throws() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.PRIV, 4); + header.frameSize = 9; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + 0x01, 0x02, 0x03, 0x04 + ); + const frame = PrivateFrame.fromRawData(data, 4); + + // Act / Assert + assert.throws(() => { const _ = frame.render(2); }); + } +} From 3bb74ab9942a798993b1c5897858b72b422a480e Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 4 Jan 2020 17:32:55 -0500 Subject: [PATCH 23/71] Unit tests for popularimeter frames --- src/id3v2/frames/popularimeterFrame.ts | 52 +++- test/id3v2/PopularimeterFrameTests.ts | 382 +++++++++++++++++++++++++ test/id3v2/framePropertiesTests.ts | 5 + 3 files changed, 424 insertions(+), 15 deletions(-) create mode 100644 test/id3v2/PopularimeterFrameTests.ts diff --git a/src/id3v2/frames/popularimeterFrame.ts b/src/id3v2/frames/popularimeterFrame.ts index fcad57ed..f40424ed 100644 --- a/src/id3v2/frames/popularimeterFrame.ts +++ b/src/id3v2/frames/popularimeterFrame.ts @@ -84,7 +84,9 @@ export default class PopularimeterFrame extends Frame { * @param value Play count of the current instance */ public set playCount(value: BigInt.BigInteger) { - Guards.ulong(value, "value"); + if (value !== undefined) { + Guards.ulong(value, "value"); + } this._playCount = value; } @@ -127,7 +129,7 @@ export default class PopularimeterFrame extends Frame { /** @inheritDoc */ public clone(): Frame { - const frame = new PopularimeterFrame(new Id3v2FrameHeader(FrameTypes.POPM, 4)); + const frame = PopularimeterFrame.fromUser(this.user); frame.playCount = this.playCount; frame.rating = this.rating; return frame; @@ -137,29 +139,49 @@ export default class PopularimeterFrame extends Frame { protected parseFields(data: ByteVector, version: number): void { const delim = ByteVector.getTextDelimiter(StringType.Latin1); - const index = data.find(delim); - if (index < 0) { + const delimIndex = data.find(delim); + if (delimIndex < 0) { throw new CorruptFileError("Popularimeter frame does not contain a text delimeter"); } - if (index + 2 > data.length) { - throw new CorruptFileError("Popularimeter frame is too short"); + + const bytesAfterOwner = data.length - delimIndex - 1; + if (bytesAfterOwner < 1) { + throw new CorruptFileError("Popularimeter frame is missing rating"); + } + if (bytesAfterOwner > 1 && bytesAfterOwner < 5) { + throw new CorruptFileError("Popularimeter frame with play count must have at least 4 bytes of play count"); } - this._user = data.toString(index, StringType.Latin1, 0); - this.rating = data.get(index + 1); - this.playCount = data.mid(index + 2).toULong(); + this._user = data.toString(delimIndex, StringType.Latin1, 0); + this._rating = data.get(delimIndex + 1); + + // Play count may be omitted + if (bytesAfterOwner > 1) { + this._playCount = data.mid(delimIndex + 2).toULong(); + } } /** @inheritDoc */ protected renderFields(version: number): ByteVector { - const data = ByteVector.fromULong(this.playCount); - while (data.length > 0 && data.get(0) === 0x0) { - data.removeAtIndex(0); + const data = ByteVector.concatenate( + ByteVector.fromString(this._user, StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + this.rating + ); + + // Only include personal play count if it's desired + if (this.playCount !== undefined) { + const playCountData = ByteVector.fromULong(this.playCount); + + // Remove zero bytes from beginning of play count, leaving at least 4 bytes + let firstNonZeroIndex = 0; + while (playCountData.get(firstNonZeroIndex) === 0x00 && firstNonZeroIndex < playCountData.length - 4) { + firstNonZeroIndex++; + } + + data.addByteVector(playCountData.mid(firstNonZeroIndex)); } - data.insertByte(0, this.rating); - data.insertByte(0, 0); - data.insertByteVector(0, ByteVector.fromString(this._user, StringType.Latin1)); return data; } } diff --git a/test/id3v2/PopularimeterFrameTests.ts b/test/id3v2/PopularimeterFrameTests.ts new file mode 100644 index 00000000..ef9f34cd --- /dev/null +++ b/test/id3v2/PopularimeterFrameTests.ts @@ -0,0 +1,382 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import * as BigInt from "big-integer"; +import ConstructorTests from "./frameConstructorTests"; +import FrameTypes from "../../src/id3v2/frameTypes"; +import PopularimeterFrame from "../../src/id3v2/frames/popularimeterFrame"; +import PropertyTests from "./framePropertiesTests"; +import {ByteVector, StringType} from "../../src/byteVector"; +import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; +import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(timeout(3000), slow(1000)) +class PopularimeterFrameConstructorTests extends ConstructorTests { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + return PopularimeterFrame.fromOffsetRawData; + } + + public get fromRawData(): (d: ByteVector, v: number) => Frame { + return PopularimeterFrame.fromRawData; + } + + @test + public fromUser() { + // Act + const frame = PopularimeterFrame.fromUser("fux"); + + // Assert + this.assertFrame(frame, "fux", undefined, undefined); + } + + @test + public fromRawData_noDelimiter() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + header.frameSize = 3; + const data = ByteVector.concatenate( + header.render(4), + 0x01, 0x02, 0x03 + ); + + // Act / Assert + assert.throws(() => { PopularimeterFrame.fromRawData(data, 4); }); + } + + @test + public fromRawData_tooShort() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + header.frameSize = 3; + const data = ByteVector.concatenate( + header.render(4), + 0x01, + ByteVector.getTextDelimiter(StringType.Latin1) + ); + + // Act / Assert + assert.throws(() => { PopularimeterFrame.fromRawData(data, 4); }); + } + + @test + public fromRawData_noPlayCount() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + header.frameSize = 9; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + 0x05 + ); + + // Act + const frame = PopularimeterFrame.fromRawData(data, 4); + + // Assert + this.assertFrame(frame, "fux", undefined, 0x05); + } + + @test + public fromRawData_invalidPlayCountBytes() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + header.frameSize = 9; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + 0x05, + 0x01, 0x02, 0x03 + ); + + // Act / Assert + assert.throws(() => { PopularimeterFrame.fromRawData(data, 4); }); + } + + @test + public fromRawData_intSizedPlayCount() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + header.frameSize = 9; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + 0x05, + ByteVector.fromUInt(1234) + ); + + // Act + const frame = PopularimeterFrame.fromRawData(data, 4); + + // Assert + this.assertFrame(frame, "fux", BigInt(1234), 0x05); + } + + @test + public fromRawData_longSizedPlayCount() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + header.frameSize = 13; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + 0x05, + ByteVector.fromULong(BigInt(1234)) + ); + + // Act + const frame = PopularimeterFrame.fromRawData(data, 4); + + // Assert + this.assertFrame(frame, "fux", BigInt(1234), 0x05); + } + + @test + public fromOffsetRawData_longSizedPlayCount() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + header.frameSize = 13; + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + 0x05, + ByteVector.fromULong(BigInt(1234)) + ); + + // Act + const frame = PopularimeterFrame.fromOffsetRawData(data, 2, header); + + // Assert + this.assertFrame(frame, "fux", BigInt(1234), 0x05); + } + + private assertFrame(frame: PopularimeterFrame, u: string, p: BigInt.BigInteger, r: number) { + assert.isOk(frame); + assert.strictEqual(frame.frameClassType, FrameClassType.PopularimeterFrame); + assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.POPM)); + + if (p === undefined) { + assert.isUndefined(frame.playCount); + } else { + assert.isOk(frame.playCount); + assert.isTrue(p.equals(frame.playCount)); + } + + assert.strictEqual(frame.rating, r); + assert.strictEqual(frame.user, u); + } +} + +@suite(timeout(3000), slow(1000)) +class PopularimeterFramePropertyTests extends PropertyTests { + @test + public playCount() { + // Arrange + const frame = PopularimeterFrame.fromUser("fux"); + const set = (v: BigInt.BigInteger) => { frame.playCount = v; }; + const get = () => frame.playCount; + + // Act + this.propertyRoundTrip(set, get, BigInt(1234)); + this.propertyRoundTrip(set, get, undefined); + this.propertyThrows(set, null); + this.propertyThrows(set, BigInt(-1)); + } + + @test + public rating() { + // Arrange + const frame = PopularimeterFrame.fromUser("fux"); + const set = (v: number) => { frame.rating = v; }; + const get = () => frame.rating; + + // Act + this.propertyRoundTrip(set, get, 5); + this.propertyThrows(set, -1); + this.propertyThrows(set, 1.23); + this.propertyThrows(set, 0x100); + } + + @test + public user() { + // Arrange + const frame = PopularimeterFrame.fromUser("fux"); + const set = (v: string) => { frame.user = v; }; + const get = () => frame.user; + + // Act + this.propertyRoundTrip(set, get, "bux"); + this.propertyNormalized(set, get, undefined, ""); + this.propertyNormalized(set, get, null, ""); + } +} + +@suite(timeout(3000), slow(1000)) +class PopularimeterFrameMethodTests { + @test + public find_falsyFrames() { + // Act / Assert + assert.throws(() => { PopularimeterFrame.find(undefined, "fux"); }); + assert.throws(() => { PopularimeterFrame.find(null, "fux"); }); + } + + @test + public find_noFrames() { + // Act + const output = PopularimeterFrame.find([], "fux"); + + // Assert + assert.isUndefined(output); + } + + @test + public find_noMatches() { + // Arrange + const frames = [ + PopularimeterFrame.fromUser("fux") + ]; + + // Act + const output = PopularimeterFrame.find(frames, "bux"); + + // Assert + assert.isUndefined(output); + } + + @test + public find_matches() { + // Arrange + const frames = [ + PopularimeterFrame.fromUser("fux"), + PopularimeterFrame.fromUser("bux") + ]; + + // Act + const output = PopularimeterFrame.find(frames, "bux"); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[1]); + } + + @test + public clone() { + // Arrange + const frame = PopularimeterFrame.fromUser("fux"); + frame.playCount = BigInt(1234); + frame.rating = 5; + + // Act + const output = frame.clone(); + + // Assert + assert.isOk(output); + assert.strictEqual(output.frameClassType, FrameClassType.PopularimeterFrame); + assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.POPM)); + + assert.isOk(output.playCount); + assert.isTrue(frame.playCount.equals(output.playCount)); + + assert.strictEqual(output.rating, frame.rating); + assert.strictEqual(output.user, frame.user); + } + + @test + public render_noPlayCount() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + header.frameSize = 5; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + 0x05 + ); + const frame = PopularimeterFrame.fromRawData(data, 4); + + // Act + const output = frame.render(4); + + // Assert + assert.isOk(output); + assert.isTrue(ByteVector.equal(output, data)); + } + + @test + public render_intPlayCount() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + header.frameSize = 9; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + 0x05, + ByteVector.fromUInt(1234) + ); + const frame = PopularimeterFrame.fromRawData(data, 4); + + // Act + const output = frame.render(4); + + // Assert + assert.isOk(output); + assert.isTrue(ByteVector.equal(output, data)); + } + + @test + public render_intermediateSizePlayCount() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + header.frameSize = 10; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + 0x05, + 0x01, 0x02, 0x03, 0x04, 0x05 + ); + const frame = PopularimeterFrame.fromRawData(data, 4); + + // Act + const output = frame.render(4); + + // Assert + assert.isOk(output); + assert.isTrue(ByteVector.equal(output, data)); + } + + @test + public render_longPlayCount() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + header.frameSize = 13; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + 0x05, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 + ); + const frame = PopularimeterFrame.fromRawData(data, 4); + + // Act + const output = frame.render(4); + + // Assert + assert.isOk(output); + assert.isTrue(ByteVector.equal(output, data)); + } +} + + diff --git a/test/id3v2/framePropertiesTests.ts b/test/id3v2/framePropertiesTests.ts index bda629b4..ec94b8a1 100644 --- a/test/id3v2/framePropertiesTests.ts +++ b/test/id3v2/framePropertiesTests.ts @@ -23,4 +23,9 @@ export default abstract class FramePropertiesTests { // Assert assert.deepStrictEqual(result, output); } + + protected propertyThrows(set: (v: T) => void, input: T) { + // Act + assert.throws(() => { set(input); }); + } } From a649f065c3ffa555971db7cbfdca1122db996070 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sun, 5 Jan 2020 14:24:10 -0500 Subject: [PATCH 24/71] Adding MusicCdIdentifier frame tests --- src/id3v2/frames/musicCdIdentifierFrame.ts | 30 ++--- test/id3v2/framePropertiesTests.ts | 8 +- test/id3v2/musicCdIdentifierFrameTests.ts | 148 +++++++++++++++++++++ test/tslint.json | 6 + 4 files changed, 173 insertions(+), 19 deletions(-) create mode 100644 test/id3v2/musicCdIdentifierFrameTests.ts create mode 100644 test/tslint.json diff --git a/src/id3v2/frames/musicCdIdentifierFrame.ts b/src/id3v2/frames/musicCdIdentifierFrame.ts index 0e1f26b2..342c9786 100644 --- a/src/id3v2/frames/musicCdIdentifierFrame.ts +++ b/src/id3v2/frames/musicCdIdentifierFrame.ts @@ -12,19 +12,6 @@ import {Guards} from "../../utils"; export default class MusicCdIdentifierFrame extends Frame { private _data: ByteVector; - /** - * Gets the identifier data stored in the current instance - */ - public get data(): ByteVector { return this._data; } - /** - * Sets the identifier data stored in the current instance - * @param value ByteVector containing the identifier stored in the current instance - */ - public set data(value: ByteVector) { this._data = value; } - - /** @inheritDoc */ - public get frameClassType(): FrameClassType { return FrameClassType.MusicCdIdentiferFrame; } - private constructor(header: Id3v2FrameHeader) { super(header); } @@ -37,7 +24,7 @@ export default class MusicCdIdentifierFrame extends Frame { * positive, safe integer * @param header Header of the frame found at {@paramref offset} in the data */ - public static fromOffsetRawHeader(data: ByteVector, offset: number, header: Id3v2FrameHeader) { + public static fromOffsetRawData(data: ByteVector, offset: number, header: Id3v2FrameHeader) { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); @@ -53,7 +40,7 @@ export default class MusicCdIdentifierFrame extends Frame { * @param data ByteVector object starting with the raw representation of the new frame * @param version The ID3v2 version the raw frame is encoded in. Must be positive 8-bit integer */ - public static fromRawHeader(data: ByteVector, version: number): MusicCdIdentifierFrame { + public static fromRawData(data: ByteVector, version: number): MusicCdIdentifierFrame { Guards.truthy(data, "data"); Guards.byte(version, "version"); @@ -62,6 +49,19 @@ export default class MusicCdIdentifierFrame extends Frame { return frame; } + /** @inheritDoc */ + public get frameClassType(): FrameClassType { return FrameClassType.MusicCdIdentiferFrame; } + + /** + * Gets the identifier data stored in the current instance + */ + public get data(): ByteVector { return this._data; } + /** + * Sets the identifier data stored in the current instance + * @param value ByteVector containing the identifier stored in the current instance + */ + public set data(value: ByteVector) { this._data = value; } + /** @inheritDoc */ public clone(): Frame { const frame = new MusicCdIdentifierFrame(new Id3v2FrameHeader(FrameTypes.MCDI, 4)); diff --git a/test/id3v2/framePropertiesTests.ts b/test/id3v2/framePropertiesTests.ts index ec94b8a1..ab052d19 100644 --- a/test/id3v2/framePropertiesTests.ts +++ b/test/id3v2/framePropertiesTests.ts @@ -5,8 +5,8 @@ import * as ChaiAsPromised from "chai-as-promised"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -export default abstract class FramePropertiesTests { - protected propertyRoundTrip(set: (v: T) => void, get: () => T, val: T) { +export default class FramePropertiesTests { + public static propertyRoundTrip(set: (v: T) => void, get: () => T, val: T) { // Act set(val); const output = get(); @@ -15,7 +15,7 @@ export default abstract class FramePropertiesTests { assert.deepStrictEqual(output, val); } - protected propertyNormalized(set: (v: T) => void, get: () => T, input: T, output: T) { + public static propertyNormalized(set: (v: T) => void, get: () => T, input: T, output: T) { // Act set(input); const result = get(); @@ -24,7 +24,7 @@ export default abstract class FramePropertiesTests { assert.deepStrictEqual(result, output); } - protected propertyThrows(set: (v: T) => void, input: T) { + public static propertyThrows(set: (v: T) => void, input: T) { // Act assert.throws(() => { set(input); }); } diff --git a/test/id3v2/musicCdIdentifierFrameTests.ts b/test/id3v2/musicCdIdentifierFrameTests.ts new file mode 100644 index 00000000..6f09ed69 --- /dev/null +++ b/test/id3v2/musicCdIdentifierFrameTests.ts @@ -0,0 +1,148 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import FrameConstructorTests from "./frameConstructorTests"; +import FramePropertiesTests from "./framePropertiesTests"; +import FrameTypes from "../../src/id3v2/frameTypes"; +import MusicCdIdentifierFrame from "../../src/id3v2/frames/musicCdIdentifierFrame"; +import {ByteVector, StringType} from "../../src/byteVector"; +import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; +import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; + +// Setup Chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(timeout(3000), slow(1000)) +class Id3v2_MusicCdIdentifierFrame extends FrameConstructorTests { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + return MusicCdIdentifierFrame.fromOffsetRawData; + } + + public get fromRawData(): (d: ByteVector, v: number) => Frame { + return MusicCdIdentifierFrame.fromRawData; + } + + @test + public fromRawData_validParameters() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.MCDI, 4); + header.frameSize = 9; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("12345abcd", StringType.Latin1) + ); + + // Act + const frame = MusicCdIdentifierFrame.fromRawData(data, 4); + + // Assert + this.assertFrame(frame, ByteVector.fromString("12345abcd", StringType.Latin1)); + } + + @test + public fromOffsetRawData_validParameters() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.MCDI, 4); + header.frameSize = 9; + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, + ByteVector.fromString("12345abcd", StringType.Latin1) + ); + + // Act + const frame = MusicCdIdentifierFrame.fromOffsetRawData(data, 2, header); + + // Assert + this.assertFrame(frame, ByteVector.fromString("12345abcd", StringType.Latin1)); + } + + @test + public data() { + // Arrange + const frame = this.getTestFrame(); + const data = ByteVector.fromString("fuxbuxqux", StringType.Latin1); + + // Act / Assert + FramePropertiesTests.propertyRoundTrip((v) => { frame.data = v; }, () => frame.data, data); + } + + @test + public clone_withData() { + // Arrange + const frame = this.getTestFrame(); + + // Act + const output = frame.clone(); + + // Assert + this.assertFrame(output, frame.data); + assert.notEqual(output.data, frame.data); + } + + @test + public clone_withoutData() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.MCDI, 4); + header.frameSize = 0; + const frame = MusicCdIdentifierFrame.fromRawData(header.render(4), 4); + + // Act + const output = frame.clone(); + + // Assert + this.assertFrame(output, frame.data); + } + + @test + public render_withData() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.MCDI, 4); + header.frameSize = 9; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("12345abcd", StringType.Latin1) + ); + const frame = MusicCdIdentifierFrame.fromRawData(data, 4); + + // Act + const output = frame.render(4); + + // Assert + assert.isTrue(ByteVector.equal(output, data)); + } + + @test + public render_withoutData() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.MCDI, 4); + header.frameSize = 0; + const frame = MusicCdIdentifierFrame.fromRawData(header.render(4), 4); + + // Act + const output = frame.render(4); + + // Assert + assert.isTrue(output.length === 0); + } + + private assertFrame(frame: MusicCdIdentifierFrame, d: ByteVector) { + assert.isOk(frame); + assert.strictEqual(frame.frameClassType, FrameClassType.MusicCdIdentiferFrame); + assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.MCDI)); + + assert.isTrue(ByteVector.equal(frame.data, d)); + } + + private getTestFrame() { + const header = new Id3v2FrameHeader(FrameTypes.MCDI, 4); + header.frameSize = 9; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("12345abcd", StringType.Latin1) + ); + return MusicCdIdentifierFrame.fromRawData(data, 4); + } +} diff --git a/test/tslint.json b/test/tslint.json new file mode 100644 index 00000000..d8f5946f --- /dev/null +++ b/test/tslint.json @@ -0,0 +1,6 @@ +{ + "extends": "../tslint.json", + "rules": { + "class-name": false + } +} \ No newline at end of file From 0dc24837385b580c145e1a51371a71bc36b20b56 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sun, 5 Jan 2020 16:19:32 -0500 Subject: [PATCH 25/71] Reorganizing existing unit tests to make it easier to tell what they're testing --- src/id3v2/frames/frameFactory.ts | 4 +- test/byteVectorConstructorTests.ts | 289 +++++++++--------- test/byteVectorConversionTests.ts | 134 ++++---- test/byteVectorStaticOperationTests.ts | 21 +- test/byteVectorVoidOperationTests.ts | 46 +-- test/id3v2/attachmentsFrameTests.ts | 13 +- test/id3v2/commentsFrameTests.ts | 60 ++-- test/id3v2/eventTimeCodeFrameTests.ts | 28 +- ...opertiesTests.ts => framePropertyTests.ts} | 2 +- test/id3v2/musicCdIdentifierFrameTests.ts | 6 +- ...ameTests.ts => popularimeterFrameTests.ts} | 34 +-- test/id3v2/privateFrameTests.ts | 12 +- test/id3v2/syncDataTests.ts | 4 +- test/id3v2/synchronizedLyricsFrameTests.ts | 44 ++- ...aderTests.ts => tagExtendedHeaderTests.ts} | 2 +- test/id3v2/tagFooterTests.ts | 22 +- test/id3v2/tagHeaderTests.ts | 30 +- test/id3v2/termsOfUseFrameTests.ts | 44 +-- test/id3v2/textInformationFrameTests.ts | 58 ++-- test/id3v2/uniqueFileIdentifierFrameTests.ts | 89 ++---- test/id3v2/unknownFrameTests.ts | 43 ++- test/id3v2/unsynchronizedLyricsFrameTests.ts | 207 +++---------- test/id3v2/urlLinkFrameTests.ts | 141 +++------ test/id3v2/userTextInformationFrameTests.ts | 18 +- test/id3v2/userUrlLinkFrameTests.ts | 12 +- 25 files changed, 613 insertions(+), 750 deletions(-) rename test/id3v2/{framePropertiesTests.ts => framePropertyTests.ts} (94%) rename test/id3v2/{PopularimeterFrameTests.ts => popularimeterFrameTests.ts} (91%) rename test/id3v2/{extendedHeaderTests.ts => tagExtendedHeaderTests.ts} (98%) diff --git a/src/id3v2/frames/frameFactory.ts b/src/id3v2/frames/frameFactory.ts index 343e31fc..6a40874d 100644 --- a/src/id3v2/frames/frameFactory.ts +++ b/src/id3v2/frames/frameFactory.ts @@ -104,7 +104,7 @@ export default { } // TODO: Support compression - if ((header.flags & Id3v2FrameFlags.Compression) != 0) { + if ((header.flags & Id3v2FrameFlags.Compression) !== 0) { throw new NotImplementedError("Compression is not supported"); } @@ -164,7 +164,7 @@ export default { func = UniqueFileIdentifierFrame.fromOffsetRawData; } else if (ByteVector.equal(header.frameId, FrameTypes.MCDI)) { // Music CD identifier (frames 4.5) - func = MusicCdIdentifierFrame.fromOffsetRawHeader; + func = MusicCdIdentifierFrame.fromOffsetRawData; } else if (ByteVector.equal(header.frameId, FrameTypes.USLT)) { // Unsynchronized lyrics (frames 4.8) func = UnsynchronizedLyricsFrame.fromOffsetRawData; diff --git a/test/byteVectorConstructorTests.ts b/test/byteVectorConstructorTests.ts index 234f546f..6b41a2c1 100644 --- a/test/byteVectorConstructorTests.ts +++ b/test/byteVectorConstructorTests.ts @@ -13,7 +13,8 @@ const AB2B = require("arraybuffer-to-buffer"); Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) class ByteVectorTestsConcatenate { +@suite(timeout(3000), slow(1000)) +class ByteVector_Concatenate { private testArray = new Uint8Array([0x80, 0x08, 0x50]); @test @@ -34,7 +35,7 @@ const assert = Chai.assert; // Assert assert.ok(bv); assert.equal(bv.length, 1); - assert.equal(bv.get(0), 0x08) + assert.equal(bv.get(0), 0x08); } @test @@ -63,7 +64,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorTestsFromByteArray { +@suite(timeout(3000), slow(1000)) +class ByteVector_FromByteArray { @test public NoData() { // Arrange, Act, Assert @@ -132,7 +134,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorTestsFromByteVector { +@suite(timeout(3000), slow(1000)) +class ByteVector_FromByteVector { @test public NoVector() { // Arrange, Act, Assert @@ -175,7 +178,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorTestsFromInt { +@suite(timeout(3000), slow(1000)) +class ByteVector_FromInt { @test public BadInteger() { // Arrange, Act, Assert @@ -195,7 +199,7 @@ const assert = Chai.assert; @test public Zero_BigEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( 0x0, [0x0, 0x0, 0x0, 0x0], undefined, @@ -205,7 +209,7 @@ const assert = Chai.assert; @test public Zero_LittleEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( 0x0, [0x0, 0x0, 0x0, 0x0], undefined, @@ -215,7 +219,7 @@ const assert = Chai.assert; @test public Positive1Byte_BigEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( 0x12, [0x00, 0x00, 0x00, 0x12], undefined, @@ -225,7 +229,7 @@ const assert = Chai.assert; @test public Positive1Byte_LittleEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( 0x12, [0x12, 0x00, 0x00, 0x00], undefined, @@ -235,7 +239,7 @@ const assert = Chai.assert; @test public Positive2Byte_BigEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( 0x1234, [0x00, 0x00, 0x12, 0x34], undefined, @@ -245,7 +249,7 @@ const assert = Chai.assert; @test public Positive2Byte_LittleEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( 0x1234, [0x34, 0x12, 0x00, 0x00], undefined, @@ -255,7 +259,7 @@ const assert = Chai.assert; @test public Positive3Byte_BigEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( 0x123456, [0x00, 0x12, 0x34, 0x56], undefined, @@ -265,7 +269,7 @@ const assert = Chai.assert; @test public Positive3Byte_LittleEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( 0x123456, [0x56, 0x34, 0x12, 0x00], undefined, @@ -275,7 +279,7 @@ const assert = Chai.assert; @test public Positive4Byte_BigEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( 0x12345678, [0x12, 0x34, 0x56, 0x78], undefined, @@ -285,7 +289,7 @@ const assert = Chai.assert; @test public Positive4Byte_LittleEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( 0x12345678, [0x78, 0x56, 0x34, 0x12], undefined, @@ -295,7 +299,7 @@ const assert = Chai.assert; @test public Negative1Byte_BigEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( -0x12, [0xFF, 0xFF, 0xFF, 0xEE], undefined, @@ -305,7 +309,7 @@ const assert = Chai.assert; @test public Negative1Byte_LittleEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( -0x12, [0xEE, 0xFF, 0xFF, 0xFF], undefined, @@ -315,7 +319,7 @@ const assert = Chai.assert; @test public Negative2Byte_BigEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( -0x1234, [0xFF, 0xFF, 0xED, 0xCC], undefined, @@ -325,7 +329,7 @@ const assert = Chai.assert; @test public Negative2Byte_LittleEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( -0x1234, [0xCC, 0xED, 0xFF, 0xFF], undefined, @@ -335,7 +339,7 @@ const assert = Chai.assert; @test public Negative3Byte_BigEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( -0x123456, [0xFF, 0xED, 0xCB, 0xAA], undefined, @@ -345,7 +349,7 @@ const assert = Chai.assert; @test public Negative3Byte_LittleEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( -0x123456, [0xAA, 0xCB, 0xED, 0xFF], undefined, @@ -355,7 +359,7 @@ const assert = Chai.assert; @test public Negative4Byte_BigEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( -0x12345678, [0xED, 0xCB, 0xA9, 0x88], undefined, @@ -365,7 +369,7 @@ const assert = Chai.assert; @test public Negative4Byte_LittleEndian() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( -0x12345678, [0x88, 0xA9, 0xCB, 0xED], undefined, @@ -375,7 +379,7 @@ const assert = Chai.assert; @test public ReadOnly() { - ByteVectorTestsFromInt.TestInt( + ByteVector_FromInt.TestInt( 0, [0x00, 0x00, 0x00, 0x00], true, @@ -400,7 +404,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorTestsFromLong { +@suite(timeout(3000), slow(1000)) +class ByteVector_FromLong { @test public BadValue() { // Arrange, Act, Assert @@ -417,7 +422,7 @@ const assert = Chai.assert; @test public Positive1Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("0x12"), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12], undefined, @@ -427,7 +432,7 @@ const assert = Chai.assert; @test public Positive1Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("0x12"), [0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -437,7 +442,7 @@ const assert = Chai.assert; @test public Positive2Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("0x1234"), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34], undefined, @@ -447,7 +452,7 @@ const assert = Chai.assert; @test public Positive2Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("0x1234"), [0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -457,7 +462,7 @@ const assert = Chai.assert; @test public Positive3Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("0x123456"), [0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56], undefined, @@ -467,7 +472,7 @@ const assert = Chai.assert; @test public Positive3Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("0x123456"), [0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -477,7 +482,7 @@ const assert = Chai.assert; @test public Positive4Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("0x12345678"), [0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78], undefined, @@ -487,7 +492,7 @@ const assert = Chai.assert; @test public Positive4Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("0x12345678"), [0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00], undefined, @@ -497,7 +502,7 @@ const assert = Chai.assert; @test public Positive5Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("0x123456789A"), [0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9A], undefined, @@ -507,7 +512,7 @@ const assert = Chai.assert; @test public Positive5Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("0x123456789A"), [0x9A, 0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00], undefined, @@ -517,7 +522,7 @@ const assert = Chai.assert; @test public Positive6Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("0x123456789ABC"), [0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC], undefined, @@ -527,7 +532,7 @@ const assert = Chai.assert; @test public Positive6Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("0x123456789ABC"), [0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12, 0x00, 0x00], undefined, @@ -537,7 +542,7 @@ const assert = Chai.assert; @test public Positive7Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("0x123456789ABCDE"), [0x00, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE], undefined, @@ -547,7 +552,7 @@ const assert = Chai.assert; @test public Positive7Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("0x123456789ABCDE"), [0xDE, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12, 0x00], undefined, @@ -557,7 +562,7 @@ const assert = Chai.assert; @test public Positive8Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("123456789ABCDEF0", 16), [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0], undefined, @@ -567,7 +572,7 @@ const assert = Chai.assert; @test public Positive8Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("123456789ABCDEF0", 16), [0xF0, 0xDE, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12], undefined, @@ -577,7 +582,7 @@ const assert = Chai.assert; @test public Negative1Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-12", 16), [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEE], undefined, @@ -587,7 +592,7 @@ const assert = Chai.assert; @test public Negative1Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-12", 16), [0xEE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -597,7 +602,7 @@ const assert = Chai.assert; @test public Negative2Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-1234", 16), [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xED, 0xCC], undefined, @@ -607,7 +612,7 @@ const assert = Chai.assert; @test public Negative2Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-1234", 16), [0xCC, 0xED, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -617,7 +622,7 @@ const assert = Chai.assert; @test public Negative3Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-123456", 16), [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xED, 0xCB, 0xAA], undefined, @@ -627,7 +632,7 @@ const assert = Chai.assert; @test public Negative3Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-123456", 16), [0xAA, 0xCB, 0xED, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -637,7 +642,7 @@ const assert = Chai.assert; @test public Negative4Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-12345678", 16), [0xFF, 0xFF, 0xFF, 0xFF, 0xED, 0xCB, 0xA9, 0x88], undefined, @@ -647,7 +652,7 @@ const assert = Chai.assert; @test public Negative4Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-12345678", 16), [0x88, 0xA9, 0xCB, 0xED, 0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -657,7 +662,7 @@ const assert = Chai.assert; @test public Negative5Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-123456789A", 16), [0xFF, 0xFF, 0xFF, 0xED, 0xCB, 0xA9, 0x87, 0x66], undefined, @@ -667,7 +672,7 @@ const assert = Chai.assert; @test public Negative5Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-123456789A", 16), [0x66, 0x87, 0xA9, 0xCB, 0xED, 0xFF, 0xFF, 0xFF], undefined, @@ -677,7 +682,7 @@ const assert = Chai.assert; @test public Negative6Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-123456789ABC", 16), [0xFF, 0xFF, 0xED, 0xCB, 0xA9, 0x87, 0x65, 0x44], undefined, @@ -687,7 +692,7 @@ const assert = Chai.assert; @test public Negative6Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-123456789ABC", 16), [0x44, 0x65, 0x87, 0xA9, 0xCB, 0xED, 0xFF, 0xFF], undefined, @@ -697,7 +702,7 @@ const assert = Chai.assert; @test public Negative7Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-123456789ABCDE", 16), [0xFF, 0xED, 0xCB, 0xA9, 0x87, 0x65, 0x43, 0x22], undefined, @@ -707,7 +712,7 @@ const assert = Chai.assert; @test public Negative7Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-123456789ABCDE", 16), [0x22, 0x43, 0x65, 0x87, 0xA9, 0xCB, 0xED, 0xFF], undefined, @@ -717,7 +722,7 @@ const assert = Chai.assert; @test public Negative8Byte_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-123456789ABCDEF0", 16), [0xED, 0xCB, 0xA9, 0x87, 0x65, 0x43, 0x21, 0x10], undefined, @@ -727,7 +732,7 @@ const assert = Chai.assert; @test public Negative8Byte_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt("-123456789ABCDEF0", 16), [0x10, 0x21, 0x43, 0x65, 0x87, 0xA9, 0xCB, 0xED], undefined, @@ -737,7 +742,7 @@ const assert = Chai.assert; @test public Zero_BigEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt(0), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -747,7 +752,7 @@ const assert = Chai.assert; @test public Zero_LittleEndian() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt(0), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -757,7 +762,7 @@ const assert = Chai.assert; @test public ReadOnly() { - ByteVectorTestsFromLong.TestLong( + ByteVector_FromLong.TestLong( BigInt(0), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], true, @@ -787,7 +792,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorTestsFromPath { +@suite(timeout(3000), slow(1000)) +class ByteVector_FromPath { @test public NoPath() { // Arrange, Act, Assert @@ -823,7 +829,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorTestsFromShort { +@suite(timeout(3000), slow(1000)) +class ByteVector_FromShort { @test public BadIntShort() { // Arrange, Act, Assert @@ -842,7 +849,7 @@ const assert = Chai.assert; @test public Zero_BigEndian() { - ByteVectorTestsFromShort.TestShort( + ByteVector_FromShort.TestShort( 0x0, [0x0, 0x0], undefined, @@ -852,7 +859,7 @@ const assert = Chai.assert; @test public Zero_LittleEndian() { - ByteVectorTestsFromShort.TestShort( + ByteVector_FromShort.TestShort( 0x0, [0x0, 0x0], undefined, @@ -862,7 +869,7 @@ const assert = Chai.assert; @test public Positive1Byte_BigEndian() { - ByteVectorTestsFromShort.TestShort( + ByteVector_FromShort.TestShort( 0x12, [0x00, 0x12], undefined, @@ -872,7 +879,7 @@ const assert = Chai.assert; @test public Positive1Byte_LittleEndian() { - ByteVectorTestsFromShort.TestShort( + ByteVector_FromShort.TestShort( 0x12, [0x12, 0x00], undefined, @@ -882,7 +889,7 @@ const assert = Chai.assert; @test public Positive2Byte_BigEndian() { - ByteVectorTestsFromShort.TestShort( + ByteVector_FromShort.TestShort( 0x1234, [0x12, 0x34], undefined, @@ -892,7 +899,7 @@ const assert = Chai.assert; @test public Positive2Byte_LittleEndian() { - ByteVectorTestsFromShort.TestShort( + ByteVector_FromShort.TestShort( 0x1234, [0x34, 0x12], undefined, @@ -902,7 +909,7 @@ const assert = Chai.assert; @test public Negative1Byte_BigEndian() { - ByteVectorTestsFromShort.TestShort( + ByteVector_FromShort.TestShort( -0x12, [0xFF, 0xEE], undefined, @@ -912,7 +919,7 @@ const assert = Chai.assert; @test public Negative1Byte_LittleEndian() { - ByteVectorTestsFromShort.TestShort( + ByteVector_FromShort.TestShort( -0x12, [0xEE, 0xFF], undefined, @@ -922,7 +929,7 @@ const assert = Chai.assert; @test public Negative2Byte_BigEndian() { - ByteVectorTestsFromShort.TestShort( + ByteVector_FromShort.TestShort( -0x1234, [0xED, 0xCC], undefined, @@ -932,7 +939,7 @@ const assert = Chai.assert; @test public Negative2Byte_LittleEndian() { - ByteVectorTestsFromShort.TestShort( + ByteVector_FromShort.TestShort( -0x1234, [0xCC, 0xED], undefined, @@ -942,7 +949,7 @@ const assert = Chai.assert; @test public ReadOnly() { - ByteVectorTestsFromShort.TestShort( + ByteVector_FromShort.TestShort( 0x0, [0x0, 0x0], true, @@ -967,7 +974,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorTestsFromSize { +@suite(timeout(3000), slow(1000)) +class ByteVector_FromSize { @test public BadSize() { // Arrange, Act, Assert @@ -1039,7 +1047,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorTestsFromStream { +@suite(timeout(3000), slow(1000)) +class ByteVector_FromStream { @test public async NoStream() { await Promise.all([ @@ -1107,7 +1116,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorTestsFromString { +@suite(timeout(3000), slow(1000)) +class ByteVector_FromString { @test public InvalidLength() { // Arrange, Act, Assert @@ -1118,7 +1128,7 @@ const assert = Chai.assert; @test public Utf8Full() { - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( TestConstants.testStrings.UTF8.str, TestConstants.testStrings.UTF8.bytes, undefined, @@ -1129,7 +1139,7 @@ const assert = Chai.assert; @test public Utf8Partial() { - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( TestConstants.testStrings.UTF8.str, TestConstants.testStrings.UTF8.bytes.slice(0, 9), undefined, @@ -1140,7 +1150,7 @@ const assert = Chai.assert; @test public Utf8Empty() { - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( "", [], undefined, @@ -1152,7 +1162,7 @@ const assert = Chai.assert; @test public Utf16LittleEndianFull() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( TestConstants.testStrings.UTF16LE.str, TestConstants.testStrings.UTF16LE.bytes, StringType.UTF16LE, @@ -1165,7 +1175,7 @@ const assert = Chai.assert; @test public Utf16LittleEndianPartial() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( TestConstants.testStrings.UTF16LE.str, TestConstants.testStrings.UTF16LE.bytes.slice(0, 12), StringType.UTF16LE, @@ -1178,7 +1188,7 @@ const assert = Chai.assert; @test public Utf16LittleEndianEmpty() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( "", [], StringType.UTF16LE, @@ -1191,7 +1201,7 @@ const assert = Chai.assert; @test public Utf16BigEndianFull() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( TestConstants.testStrings.UTF16BE.str, TestConstants.testStrings.UTF16BE.bytes, StringType.UTF16BE, @@ -1204,7 +1214,7 @@ const assert = Chai.assert; @test public Utf16BigEndianPartial() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( TestConstants.testStrings.UTF16BE.str, TestConstants.testStrings.UTF16BE.bytes.slice(0, 12), StringType.UTF16BE, @@ -1217,7 +1227,7 @@ const assert = Chai.assert; @test public Utf16BigEndianEmpty() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( "", [], StringType.UTF16BE, @@ -1229,7 +1239,7 @@ const assert = Chai.assert; @test public Latin1Full() { - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( TestConstants.testStrings.Latin1.str, TestConstants.testStrings.Latin1.bytes, StringType.Latin1, @@ -1240,7 +1250,7 @@ const assert = Chai.assert; @test public Latin1Partial() { - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( TestConstants.testStrings.Latin1.str, TestConstants.testStrings.Latin1.bytes.slice(0, 6), StringType.Latin1, @@ -1251,7 +1261,7 @@ const assert = Chai.assert; @test public Latin1Empty() { - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( "", [], StringType.Latin1, @@ -1266,7 +1276,7 @@ const assert = Chai.assert; const originalLastEncoding = ByteVector.lastUtf16Encoding; ByteVector.lastUtf16Encoding = "something bogus"; - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( TestConstants.testStrings.UTF16LEWithBOM.str, TestConstants.testStrings.UTF16LEWithBOM.bytes, StringType.UTF16, @@ -1285,7 +1295,7 @@ const assert = Chai.assert; const originalLastEncoding = ByteVector.lastUtf16Encoding; ByteVector.lastUtf16Encoding = "something bogus"; - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( TestConstants.testStrings.UTF16LEWithBOM.str, TestConstants.testStrings.UTF16LEWithBOM.bytes.slice(0, 14), StringType.UTF16, @@ -1304,7 +1314,7 @@ const assert = Chai.assert; const originalLastEncoding = ByteVector.lastUtf16Encoding; ByteVector.lastUtf16Encoding = "something bogus"; - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( "", TestConstants.testStrings.UTF16LEWithBOM.bytes.slice(0, 2), StringType.UTF16, @@ -1319,7 +1329,7 @@ const assert = Chai.assert; @test public ReadOnly() { - ByteVectorTestsFromString.TestString( + ByteVector_FromString.TestString( "", [], StringType.Latin1, @@ -1350,7 +1360,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorTestsFromUInt { +@suite(timeout(3000), slow(1000)) +class ByteVector_FromUInt { @test public BadInteger() { // Arrange, Act, Assert @@ -1369,7 +1380,7 @@ const assert = Chai.assert; @test public Zero_BigEndian() { - ByteVectorTestsFromUInt.TestUInt( + ByteVector_FromUInt.TestUInt( 0x0, [0x0, 0x0, 0x0, 0x0], undefined, @@ -1379,7 +1390,7 @@ const assert = Chai.assert; @test public Zero_LittleEndian() { - ByteVectorTestsFromUInt.TestUInt( + ByteVector_FromUInt.TestUInt( 0x0, [0x0, 0x0, 0x0, 0x0], undefined, @@ -1389,7 +1400,7 @@ const assert = Chai.assert; @test public Positive1Byte_BigEndian() { - ByteVectorTestsFromUInt.TestUInt( + ByteVector_FromUInt.TestUInt( 0x12, [0x00, 0x00, 0x00, 0x12], undefined, @@ -1399,7 +1410,7 @@ const assert = Chai.assert; @test public Positive1Byte_LittleEndian() { - ByteVectorTestsFromUInt.TestUInt( + ByteVector_FromUInt.TestUInt( 0x12, [0x12, 0x00, 0x00, 0x00], undefined, @@ -1409,7 +1420,7 @@ const assert = Chai.assert; @test public Positive2Byte_BigEndian() { - ByteVectorTestsFromUInt.TestUInt( + ByteVector_FromUInt.TestUInt( 0x1234, [0x00, 0x00, 0x12, 0x34], undefined, @@ -1419,7 +1430,7 @@ const assert = Chai.assert; @test public Positive2Byte_LittleEndian() { - ByteVectorTestsFromUInt.TestUInt( + ByteVector_FromUInt.TestUInt( 0x1234, [0x34, 0x12, 0x00, 0x00], undefined, @@ -1429,7 +1440,7 @@ const assert = Chai.assert; @test public Positive3Byte_BigEndian() { - ByteVectorTestsFromUInt.TestUInt( + ByteVector_FromUInt.TestUInt( 0x123456, [0x00, 0x12, 0x34, 0x56], undefined, @@ -1439,7 +1450,7 @@ const assert = Chai.assert; @test public Positive3Byte_LittleEndian() { - ByteVectorTestsFromUInt.TestUInt( + ByteVector_FromUInt.TestUInt( 0x123456, [0x56, 0x34, 0x12, 0x00], undefined, @@ -1449,7 +1460,7 @@ const assert = Chai.assert; @test public Positive4Byte_BigEndian() { - ByteVectorTestsFromUInt.TestUInt( + ByteVector_FromUInt.TestUInt( 0x12345678, [0x12, 0x34, 0x56, 0x78], undefined, @@ -1459,7 +1470,7 @@ const assert = Chai.assert; @test public Positive4Byte_LittleEndian() { - ByteVectorTestsFromUInt.TestUInt( + ByteVector_FromUInt.TestUInt( 0x12345678, [0x78, 0x56, 0x34, 0x12], undefined, @@ -1469,7 +1480,7 @@ const assert = Chai.assert; @test public UnsignedRange_BigEndian() { - ByteVectorTestsFromUInt.TestUInt( + ByteVector_FromUInt.TestUInt( 0xFFFFFFFF, [0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -1479,7 +1490,7 @@ const assert = Chai.assert; @test public UnsignedRange_LittleEndian() { - ByteVectorTestsFromUInt.TestUInt( + ByteVector_FromUInt.TestUInt( 0xFFFFFFFF, [0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -1489,7 +1500,7 @@ const assert = Chai.assert; @test public ReadOnly() { - ByteVectorTestsFromUInt.TestUInt( + ByteVector_FromUInt.TestUInt( 0, [0x00, 0x00, 0x00, 0x00], true, @@ -1514,7 +1525,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorTestsFromULong { +@suite(timeout(3000), slow(1000)) +class ByteVector_FromULong { @test public BadValue() { // Arrange, Act, Assert @@ -1531,7 +1543,7 @@ const assert = Chai.assert; @test public Positive1Byte_BigEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("0x12"), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12], undefined, @@ -1541,7 +1553,7 @@ const assert = Chai.assert; @test public Positive1Byte_LittleEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("0x12"), [0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -1551,7 +1563,7 @@ const assert = Chai.assert; @test public Positive2Byte_BigEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("0x1234"), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34], undefined, @@ -1561,7 +1573,7 @@ const assert = Chai.assert; @test public Positive2Byte_LittleEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("0x1234"), [0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -1571,7 +1583,7 @@ const assert = Chai.assert; @test public Positive3Byte_BigEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("0x123456"), [0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56], undefined, @@ -1581,7 +1593,7 @@ const assert = Chai.assert; @test public Positive3Byte_LittleEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("0x123456"), [0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -1591,7 +1603,7 @@ const assert = Chai.assert; @test public Positive4Byte_BigEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("0x12345678"), [0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78], undefined, @@ -1601,7 +1613,7 @@ const assert = Chai.assert; @test public Positive4Byte_LittleEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("0x12345678"), [0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00], undefined, @@ -1611,7 +1623,7 @@ const assert = Chai.assert; @test public Positive5Byte_BigEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("0x123456789A"), [0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9A], undefined, @@ -1621,7 +1633,7 @@ const assert = Chai.assert; @test public Positive5Byte_LittleEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("0x123456789A"), [0x9A, 0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00], undefined, @@ -1631,7 +1643,7 @@ const assert = Chai.assert; @test public Positive6Byte_BigEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("0x123456789ABC"), [0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC], undefined, @@ -1641,7 +1653,7 @@ const assert = Chai.assert; @test public Positive6Byte_LittleEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("0x123456789ABC"), [0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12, 0x00, 0x00], undefined, @@ -1651,7 +1663,7 @@ const assert = Chai.assert; @test public Positive7Byte_BigEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("0x123456789ABCDE"), [0x00, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE], undefined, @@ -1661,7 +1673,7 @@ const assert = Chai.assert; @test public Positive7Byte_LittleEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("0x123456789ABCDE"), [0xDE, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12, 0x00], undefined, @@ -1671,7 +1683,7 @@ const assert = Chai.assert; @test public Positive8Byte_BigEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("123456789ABCDEF0", 16), [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0], undefined, @@ -1681,7 +1693,7 @@ const assert = Chai.assert; @test public Positive8Byte_LittleEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("123456789ABCDEF0", 16), [0xF0, 0xDE, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12], undefined, @@ -1691,7 +1703,7 @@ const assert = Chai.assert; @test public UnsignedRange_BigEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("FFFFFFFFFFFFFFFF", 16), [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -1701,7 +1713,7 @@ const assert = Chai.assert; @test public UnsignedRange_LittleEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt("FFFFFFFFFFFFFFFF", 16), [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -1711,7 +1723,7 @@ const assert = Chai.assert; @test public Zero_BigEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt(0), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -1721,7 +1733,7 @@ const assert = Chai.assert; @test public Zero_LittleEndian() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt(0), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -1731,7 +1743,7 @@ const assert = Chai.assert; @test public ReadOnly() { - ByteVectorTestsFromULong.TestULong( + ByteVector_FromULong.TestULong( BigInt(0), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], true, @@ -1761,7 +1773,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorTestsFromUShort { +@suite(timeout(3000), slow(1000)) +class ByteVector_FromUShort { @test public BadShort() { // Arrange, Act, Assert @@ -1780,7 +1793,7 @@ const assert = Chai.assert; @test public Zero_BigEndian() { - ByteVectorTestsFromUShort.TestUShort( + ByteVector_FromUShort.TestUShort( 0x0, [0x0, 0x0], undefined, @@ -1790,7 +1803,7 @@ const assert = Chai.assert; @test public Zero_LittleEndian() { - ByteVectorTestsFromUShort.TestUShort( + ByteVector_FromUShort.TestUShort( 0x0, [0x0, 0x0], undefined, @@ -1800,7 +1813,7 @@ const assert = Chai.assert; @test public Positive1Byte_BigEndian() { - ByteVectorTestsFromUShort.TestUShort( + ByteVector_FromUShort.TestUShort( 0x12, [0x00, 0x12], undefined, @@ -1810,7 +1823,7 @@ const assert = Chai.assert; @test public Positive1Byte_LittleEndian() { - ByteVectorTestsFromUShort.TestUShort( + ByteVector_FromUShort.TestUShort( 0x12, [0x12, 0x00], undefined, @@ -1820,7 +1833,7 @@ const assert = Chai.assert; @test public Positive2Byte_BigEndian() { - ByteVectorTestsFromUShort.TestUShort( + ByteVector_FromUShort.TestUShort( 0x1234, [0x12, 0x34], undefined, @@ -1830,7 +1843,7 @@ const assert = Chai.assert; @test public Positive2Byte_LittleEndian() { - ByteVectorTestsFromUShort.TestUShort( + ByteVector_FromUShort.TestUShort( 0x1234, [0x34, 0x12], undefined, @@ -1840,7 +1853,7 @@ const assert = Chai.assert; @test public UnsignedRange_BigEndian() { - ByteVectorTestsFromUShort.TestUShort( + ByteVector_FromUShort.TestUShort( 0xFFFF, [0xFF, 0xFF], undefined, @@ -1850,7 +1863,7 @@ const assert = Chai.assert; @test public UnsignedRange_LittleEndian() { - ByteVectorTestsFromUShort.TestUShort( + ByteVector_FromUShort.TestUShort( 0xFFFF, [0xFF, 0xFF], undefined, @@ -1860,7 +1873,7 @@ const assert = Chai.assert; @test public ReadOnly() { - ByteVectorTestsFromUShort.TestUShort( + ByteVector_FromUShort.TestUShort( 0x0, [0x0, 0x0], true, diff --git a/test/byteVectorConversionTests.ts b/test/byteVectorConversionTests.ts index 51e3a633..a964566a 100644 --- a/test/byteVectorConversionTests.ts +++ b/test/byteVectorConversionTests.ts @@ -7,7 +7,8 @@ import {ByteVector, StringType} from "../src/byteVector"; const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) class ByteVectorToDoubleTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_ToDoubleTests { private static readonly PositiveBV = ByteVector.fromByteArray( // 56.12 new Uint8Array([0x8F, 0xC2, 0xF5, 0x28, 0x5C, 0x0F, 0x4C, 0x40, 0xAA]) ); @@ -35,25 +36,25 @@ const assert = Chai.assert; @test public PositiveBigEndian() { - const double = ByteVectorToDoubleTests.PositiveBV.toDouble(); + const double = ByteVector_ToDoubleTests.PositiveBV.toDouble(); assert.closeTo(double, -9.5397675953257207e-233, 0.000000000001e-233); } @test public PositiveLittleEndian() { - const double = ByteVectorToDoubleTests.PositiveBV.toDouble(false); + const double = ByteVector_ToDoubleTests.PositiveBV.toDouble(false); assert.closeTo(double, 56.12, 0.01); } @test public NegativeBigEndian() { - const double = ByteVectorToDoubleTests.NegativeBV.toDouble(); + const double = ByteVector_ToDoubleTests.NegativeBV.toDouble(); assert.closeTo(double, -9.6037214055410557e-86, 0.00000000001e-86); } @test public NegativeLittleEndian() { - const double = ByteVectorToDoubleTests.NegativeBV.toDouble(false); + const double = ByteVector_ToDoubleTests.NegativeBV.toDouble(false); assert.closeTo(double, -12.34, 0.01); } @@ -72,7 +73,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorToFloatTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_ToFloatTests { private static readonly PositiveBV = ByteVector.fromByteArray( // 56.12 new Uint8Array([0xE1, 0x7A, 0x60, 0x42, 0xAA]) ); @@ -96,30 +98,31 @@ const assert = Chai.assert; @test public PositiveBigEndian() { - const float = ByteVectorToFloatTests.PositiveBV.toFloat(); + const float = ByteVector_ToFloatTests.PositiveBV.toFloat(); assert.closeTo(float, -2.88663883e20, 0.00000001e20); } @test public PositiveLittleEndian() { - const float = ByteVectorToFloatTests.PositiveBV.toFloat(false); + const float = ByteVector_ToFloatTests.PositiveBV.toFloat(false); assert.closeTo(float, 56.12, 0.01); } @test public NegativeBigEndian() { - const float = ByteVectorToFloatTests.NegativeBV.toFloat(); + const float = ByteVector_ToFloatTests.NegativeBV.toFloat(); assert.closeTo(float, -5.21007881e-17, 0.00000001e-17); } @test public NegativeLittleEndian() { - const float = ByteVectorToFloatTests.NegativeBV.toFloat(false); + const float = ByteVector_ToFloatTests.NegativeBV.toFloat(false); assert.closeTo(float, -12.34, 0.01); } } -@suite(timeout(3000), slow(1000)) class ByteVectorToIntTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_ToIntTests { private static readonly NegativeCompleteBV = ByteVector.fromByteArray( new Uint8Array([0xFE, 0xFD, 0xFC, 0xFC, 0xAA]) ); @@ -153,54 +156,55 @@ const assert = Chai.assert; @test public PositiveBigEndian_Complete() { - const int = ByteVectorToIntTests.PositiveCompleteBV.toInt(); + const int = ByteVector_ToIntTests.PositiveCompleteBV.toInt(); assert.strictEqual(int, 0x01020304); } @test public PositiveBigEndian_Incomplete() { - const int = ByteVectorToIntTests.PositiveIncompleteBV.toInt(); + const int = ByteVector_ToIntTests.PositiveIncompleteBV.toInt(); assert.strictEqual(int, 0x00000102); } @test public PositiveLittleEndian_Complete() { - const int = ByteVectorToIntTests.PositiveCompleteBV.toInt(false); + const int = ByteVector_ToIntTests.PositiveCompleteBV.toInt(false); assert.strictEqual(int, 0x04030201); } @test public PositiveLittleEndian_Incomplete() { - const int = ByteVectorToIntTests.PositiveIncompleteBV.toInt(false); + const int = ByteVector_ToIntTests.PositiveIncompleteBV.toInt(false); assert.strictEqual(int, 0x00000201); } @test public NegativeBigEndian_Complete() { - const int = ByteVectorToIntTests.NegativeCompleteBV.toInt(); + const int = ByteVector_ToIntTests.NegativeCompleteBV.toInt(); assert.strictEqual(int, -0x01020304); } @test public NegativeBigEndian_Incomplete() { - const int = ByteVectorToIntTests.NegativeIncompleteBV.toInt(); + const int = ByteVector_ToIntTests.NegativeIncompleteBV.toInt(); assert.strictEqual(int, 0x0000FCFD); } @test public NegativeLittleEndian_Complete() { - const int = ByteVectorToIntTests.NegativeCompleteBV.toInt(false); + const int = ByteVector_ToIntTests.NegativeCompleteBV.toInt(false); assert.strictEqual(int, -0x03030202); } @test public NegativeLittleEndian_Incomplete() { - const int = ByteVectorToIntTests.NegativeIncompleteBV.toInt(false); + const int = ByteVector_ToIntTests.NegativeIncompleteBV.toInt(false); assert.strictEqual(int, 0x0000FDFC); } } -@suite(timeout(3000), slow(1000)) class ByteVectorToLongTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_ToLongTests { private static readonly NegativeCompleteBV = ByteVector.fromByteArray( new Uint8Array([0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xF8, 0xAA]) ); @@ -238,54 +242,55 @@ const assert = Chai.assert; @test public PositiveBigEndian_Complete() { - const long = ByteVectorToLongTests.PositiveCompleteBV.toLong(); + const long = ByteVector_ToLongTests.PositiveCompleteBV.toLong(); assert.isTrue(long.equals(BigInt("0102030405060708", 16))); } @test public PositiveBigEndian_Incomplete() { - const long = ByteVectorToLongTests.PositiveIncompleteBV.toLong(); + const long = ByteVector_ToLongTests.PositiveIncompleteBV.toLong(); assert.isTrue(long.equals(BigInt("01020304", 16))); } @test public PositiveLittleEndian_Complete() { - const long = ByteVectorToLongTests.PositiveCompleteBV.toLong(false); + const long = ByteVector_ToLongTests.PositiveCompleteBV.toLong(false); assert.isTrue(long.equals(BigInt("0807060504030201", 16))); } @test public PositiveLittleEndian_Incomplete() { - const long = ByteVectorToLongTests.PositiveIncompleteBV.toLong(false); + const long = ByteVector_ToLongTests.PositiveIncompleteBV.toLong(false); assert.isTrue(long.equals(BigInt("04030201", 16))); } @test public NegativeBigEndian_Complete() { - const long = ByteVectorToLongTests.NegativeCompleteBV.toLong(); + const long = ByteVector_ToLongTests.NegativeCompleteBV.toLong(); assert.isTrue(long.equals(BigInt("-0102030405060708", 16))); } @test public NegativeBigEndian_Incomplete() { - const long = ByteVectorToLongTests.NegativeIncompleteBV.toLong(); + const long = ByteVector_ToLongTests.NegativeIncompleteBV.toLong(); assert.isTrue(long.equals(BigInt("FEFDFCFC", 16))); } @test public NegativeLittleEndian_Complete() { - const long = ByteVectorToLongTests.NegativeCompleteBV.toLong(false); + const long = ByteVector_ToLongTests.NegativeCompleteBV.toLong(false); assert.isTrue(long.equals(BigInt("-0707060504030202", 16))); } @test public NegativeLittleEndian_Incomplete() { - const long = ByteVectorToLongTests.NegativeIncompleteBV.toLong(false); + const long = ByteVector_ToLongTests.NegativeIncompleteBV.toLong(false); assert.isTrue(long.equals(BigInt("FCFCFDFE", 16))); } } -@suite(timeout(3000), slow(1000)) class ByteVectorToShortTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_ToShortTests { private static readonly NegativeCompleteBV = ByteVector.fromByteArray( new Uint8Array([0xFE, 0xFD, 0xAA]) ); @@ -319,54 +324,55 @@ const assert = Chai.assert; @test public PositiveBigEndian_Complete() { - const int = ByteVectorToShortTests.PositiveCompleteBV.toShort(); + const int = ByteVector_ToShortTests.PositiveCompleteBV.toShort(); assert.strictEqual(int, 0x0102); } @test public PositiveBigEndian_Incomplete() { - const int = ByteVectorToShortTests.PositiveIncompleteBV.toShort(); + const int = ByteVector_ToShortTests.PositiveIncompleteBV.toShort(); assert.strictEqual(int, 0x01); } @test public PositiveLittleEndian_Complete() { - const int = ByteVectorToShortTests.PositiveCompleteBV.toShort(false); + const int = ByteVector_ToShortTests.PositiveCompleteBV.toShort(false); assert.strictEqual(int, 0x0201); } @test public PositiveLittleEndian_Incomplete() { - const int = ByteVectorToShortTests.PositiveIncompleteBV.toShort(false); + const int = ByteVector_ToShortTests.PositiveIncompleteBV.toShort(false); assert.strictEqual(int, 0x01); } @test public NegativeBigEndian_Complete() { - const int = ByteVectorToShortTests.NegativeCompleteBV.toShort(); + const int = ByteVector_ToShortTests.NegativeCompleteBV.toShort(); assert.strictEqual(int, -0x0103); } @test public NegativeBigEndian_Incomplete() { - const int = ByteVectorToShortTests.NegativeIncompleteBV.toShort(); + const int = ByteVector_ToShortTests.NegativeIncompleteBV.toShort(); assert.strictEqual(int, 0x00FC); } @test public NegativeLittleEndian_Complete() { - const int = ByteVectorToShortTests.NegativeCompleteBV.toShort(false); + const int = ByteVector_ToShortTests.NegativeCompleteBV.toShort(false); assert.strictEqual(int, -0x0202); } @test public NegativeLittleEndian_Incomplete() { - const int = ByteVectorToShortTests.NegativeIncompleteBV.toShort(false); + const int = ByteVector_ToShortTests.NegativeIncompleteBV.toShort(false); assert.strictEqual(int, 0x00FC); } } -@suite(timeout(3000), slow(1000)) class ByteVectorToStringTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_ToStringTests { @test public InvalidOffset() { // Arrange, Act, Assert @@ -547,7 +553,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorToStringsTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_ToStringsTests { @test public InvalidOffset() { // Arrange, Act, Assert @@ -565,7 +572,7 @@ const assert = Chai.assert; @test public Utf8Single() { - ByteVectorToStringsTests.TestString( + ByteVector_ToStringsTests.TestString( "\0\0" + TestConstants.testStrings.UTF8.str, StringType.UTF8, 2, @@ -576,7 +583,7 @@ const assert = Chai.assert; @test public Utf8Multiple() { - ByteVectorToStringsTests.TestString( + ByteVector_ToStringsTests.TestString( TestConstants.testStrings.UTF8.str + "\0\0" + TestConstants.testStrings.UTF8.str, StringType.UTF8, 0, @@ -587,7 +594,7 @@ const assert = Chai.assert; @test public Utf16LittleEndianSingle() { - ByteVectorToStringsTests.TestString( + ByteVector_ToStringsTests.TestString( "\0\0" + TestConstants.testStrings.UTF16LE.str, StringType.UTF16LE, 2, @@ -598,7 +605,7 @@ const assert = Chai.assert; @test public Ut16LittleEndianMultiple() { - ByteVectorToStringsTests.TestString( + ByteVector_ToStringsTests.TestString( TestConstants.testStrings.UTF16LE.str + "\0\0" + TestConstants.testStrings.UTF16LE.str, StringType.UTF16LE, 0, @@ -609,7 +616,7 @@ const assert = Chai.assert; @test public Utf16BigEndianSingle() { - ByteVectorToStringsTests.TestString( + ByteVector_ToStringsTests.TestString( "\0\0" + TestConstants.testStrings.UTF16BE.str, StringType.UTF16BE, 2, @@ -620,7 +627,7 @@ const assert = Chai.assert; @test public Ut16BigEndianMultiple() { - ByteVectorToStringsTests.TestString( + ByteVector_ToStringsTests.TestString( TestConstants.testStrings.UTF16BE.str + "\0\0" + TestConstants.testStrings.UTF16BE.str, StringType.UTF16BE, 0, @@ -635,7 +642,7 @@ const assert = Chai.assert; const originalLastEncoding = ByteVector.lastUtf16Encoding; ByteVector.lastUtf16Encoding = "utf16-le"; - ByteVectorToStringsTests.TestString( + ByteVector_ToStringsTests.TestString( "\0\0" + TestConstants.testStrings.UTF16LEWithBOM.str, StringType.UTF16, 2, @@ -654,7 +661,7 @@ const assert = Chai.assert; const originalLastEncoding = ByteVector.lastUtf16Encoding; ByteVector.lastUtf16Encoding = "utf16-le"; - ByteVectorToStringsTests.TestString( + ByteVector_ToStringsTests.TestString( TestConstants.testStrings.UTF16LEWithBOM.str + "\0\0" + TestConstants.testStrings.UTF16LEWithBOM.str, StringType.UTF16, 0, @@ -685,7 +692,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorToUIntTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_ToUIntTests { private static readonly PositiveCompleteBV = ByteVector.fromByteArray( new Uint8Array([0x01, 0x02, 0x03, 0x04, 0xAA]) ); @@ -713,25 +721,25 @@ const assert = Chai.assert; @test public PositiveBigEndian_Complete() { - const int = ByteVectorToUIntTests.PositiveCompleteBV.toUInt(); + const int = ByteVector_ToUIntTests.PositiveCompleteBV.toUInt(); assert.strictEqual(int, 0x01020304); } @test public PositiveBigEndian_Incomplete() { - const int = ByteVectorToUIntTests.PositiveIncompleteBV.toUInt(); + const int = ByteVector_ToUIntTests.PositiveIncompleteBV.toUInt(); assert.strictEqual(int, 0x00000102); } @test public PositiveLittleEndian_Complete() { - const int = ByteVectorToUIntTests.PositiveCompleteBV.toUInt(false); + const int = ByteVector_ToUIntTests.PositiveCompleteBV.toUInt(false); assert.strictEqual(int, 0x04030201); } @test public PositiveLittleEndian_Incomplete() { - const int = ByteVectorToUIntTests.PositiveIncompleteBV.toUInt(false); + const int = ByteVector_ToUIntTests.PositiveIncompleteBV.toUInt(false); assert.strictEqual(int, 0x00000201); } @@ -742,7 +750,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorToULongTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_ToULongTests { private static readonly PositiveCompleteBV = ByteVector.fromByteArray( new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0xAA]) ); @@ -774,25 +783,25 @@ const assert = Chai.assert; @test public PositiveBigEndian_Complete() { - const long = ByteVectorToULongTests.PositiveCompleteBV.toULong(); + const long = ByteVector_ToULongTests.PositiveCompleteBV.toULong(); assert.isTrue(long.equals(BigInt("0102030405060708", 16))); } @test public PositiveBigEndian_Incomplete() { - const long = ByteVectorToULongTests.PositiveIncompleteBV.toULong(); + const long = ByteVector_ToULongTests.PositiveIncompleteBV.toULong(); assert.isTrue(long.equals(BigInt("01020304", 16))); } @test public PositiveLittleEndian_Complete() { - const long = ByteVectorToULongTests.PositiveCompleteBV.toULong(false); + const long = ByteVector_ToULongTests.PositiveCompleteBV.toULong(false); assert.isTrue(long.equals(BigInt("0807060504030201", 16))); } @test public PositiveLittleEndian_Incomplete() { - const long = ByteVectorToULongTests.PositiveIncompleteBV.toULong(false); + const long = ByteVector_ToULongTests.PositiveIncompleteBV.toULong(false); assert.isTrue(long.equals(BigInt("04030201", 16))); } @@ -805,7 +814,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorToUShortTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_ToUShortTests { private static readonly PositiveCompleteBV = ByteVector.fromByteArray( new Uint8Array([0x01, 0x02, 0xAA]) ); @@ -833,25 +843,25 @@ const assert = Chai.assert; @test public PositiveBigEndian_Complete() { - const int = ByteVectorToUShortTests.PositiveCompleteBV.toUShort(); + const int = ByteVector_ToUShortTests.PositiveCompleteBV.toUShort(); assert.strictEqual(int, 0x0102); } @test public PositiveBigEndian_Incomplete() { - const int = ByteVectorToUShortTests.PositiveIncompleteBV.toUShort(); + const int = ByteVector_ToUShortTests.PositiveIncompleteBV.toUShort(); assert.strictEqual(int, 0x01); } @test public PositiveLittleEndian_Complete() { - const int = ByteVectorToUShortTests.PositiveCompleteBV.toUShort(false); + const int = ByteVector_ToUShortTests.PositiveCompleteBV.toUShort(false); assert.strictEqual(int, 0x0201); } @test public PositiveLittleEndian_Incomplete() { - const int = ByteVectorToUShortTests.PositiveIncompleteBV.toUShort(false); + const int = ByteVector_ToUShortTests.PositiveIncompleteBV.toUShort(false); assert.strictEqual(int, 0x01); } diff --git a/test/byteVectorStaticOperationTests.ts b/test/byteVectorStaticOperationTests.ts index a1609b12..28671434 100644 --- a/test/byteVectorStaticOperationTests.ts +++ b/test/byteVectorStaticOperationTests.ts @@ -8,7 +8,8 @@ import {ByteVector} from "../src/byteVector"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) class ByteVectorAddTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_AddTests { @test public InvalidParameters() { // Arrange @@ -103,7 +104,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorEqualTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_EqualTests { @test public BothNullUndefined() { // Arrange, Act, Assert @@ -151,7 +153,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorNotEqualTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_NotEqualTests { @test public BothNullUndefined() { // Arrange, Act, Assert @@ -199,7 +202,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorGreaterThanTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_GreaterThanTests { @test public InvalidParameters() { // Arrange @@ -248,7 +252,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorGreaterThanEqualTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_GreaterThanEqualTests { @test public InvalidParameters() { // Arrange @@ -297,7 +302,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorLessThanTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_LessThanTests { @test public InvalidParameters() { // Arrange @@ -346,7 +352,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ByteVectorLessThanEqualTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_LessThanEqualTests { @test public InvalidParameters() { // Arrange diff --git a/test/byteVectorVoidOperationTests.ts b/test/byteVectorVoidOperationTests.ts index 2c7a62b9..79a8ef8c 100644 --- a/test/byteVectorVoidOperationTests.ts +++ b/test/byteVectorVoidOperationTests.ts @@ -8,7 +8,8 @@ import {ByteVector} from "../src/byteVector"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) class AddByteTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_AddByteTests { @test public ReadOnly() { // Arrange - Create readonly ByteVector @@ -61,7 +62,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class AddByteArrayTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_AddByteArrayTests { @test public ReadOnly() { // Arrange - Create readonly ByteVector @@ -168,7 +170,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class AddByteVectorTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_AddByteVectorTests { @test public ReadOnly() { // Arrange - Create readonly ByteVector @@ -266,7 +269,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class ClearTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_ClearTests { @test public ReadOnly() { // Arrange - Create readonly bytevector @@ -308,7 +312,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class InsertByteTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_InsertByteTests { @test public ReadOnly() { // Arrange @@ -409,7 +414,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class InsertByteArrayTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_InsertByteArrayTests { @test public ReadOnly() { // Arrange @@ -523,7 +529,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class InsertByteVectorTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_InsertByteVectorTests { private static readonly vectorToAdd: ByteVector = ByteVector.fromByteArray( new Uint8Array([0xAA, 0xBB]) ); @@ -534,7 +541,7 @@ const assert = Chai.assert; const bv = ByteVector.fromSize(1, 0x00, true); // Act, Assert - assert.throws(() => { bv.insertByteVector(0, InsertByteVectorTests.vectorToAdd); }); + assert.throws(() => { bv.insertByteVector(0, ByteVector_InsertByteVectorTests.vectorToAdd); }); assert.isTrue(bv.isReadOnly); assert.isFalse(bv.isEmpty); @@ -548,9 +555,9 @@ const assert = Chai.assert; const bv = ByteVector.fromSize(1, 0x00); // Act, Assert - assert.throws(() => { bv.insertByteVector(0.1, InsertByteVectorTests.vectorToAdd); }); - assert.throws(() => { bv.insertByteVector(-1, InsertByteVectorTests.vectorToAdd); }); - assert.throws(() => { bv.insertByteVector(2, InsertByteVectorTests.vectorToAdd); }); + assert.throws(() => { bv.insertByteVector(0.1, ByteVector_InsertByteVectorTests.vectorToAdd); }); + assert.throws(() => { bv.insertByteVector(-1, ByteVector_InsertByteVectorTests.vectorToAdd); }); + assert.throws(() => { bv.insertByteVector(2, ByteVector_InsertByteVectorTests.vectorToAdd); }); assert.isFalse(bv.isReadOnly); assert.isFalse(bv.isEmpty); @@ -593,7 +600,7 @@ const assert = Chai.assert; const bv = ByteVector.fromSize(0); // Act - bv.insertByteVector(0, InsertByteVectorTests.vectorToAdd); + bv.insertByteVector(0, ByteVector_InsertByteVectorTests.vectorToAdd); // Assert assert.isFalse(bv.isEmpty); @@ -607,7 +614,7 @@ const assert = Chai.assert; const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); // Act - bv.insertByteVector(0, InsertByteVectorTests.vectorToAdd); + bv.insertByteVector(0, ByteVector_InsertByteVectorTests.vectorToAdd); // Assert assert.strictEqual(bv.length, 6); @@ -620,7 +627,7 @@ const assert = Chai.assert; const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); // Act - bv.insertByteVector(bv.length, InsertByteVectorTests.vectorToAdd); + bv.insertByteVector(bv.length, ByteVector_InsertByteVectorTests.vectorToAdd); // Assert assert.strictEqual(bv.length, 6); @@ -633,7 +640,7 @@ const assert = Chai.assert; const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); // Act - bv.insertByteVector(2, InsertByteVectorTests.vectorToAdd); + bv.insertByteVector(2, ByteVector_InsertByteVectorTests.vectorToAdd); // Assert assert.strictEqual(bv.length, 6); @@ -641,7 +648,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class RemoveAtIndexTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_RemoveAtIndexTests { @test public ReadOnly() { // Arrange @@ -712,7 +720,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class RemoveRangeTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_RemoveRangeTests { @test public ReadOnly() { // Arrange @@ -798,7 +807,8 @@ const assert = Chai.assert; } } -@suite(timeout(3000), slow(1000)) class SetTests { +@suite(timeout(3000), slow(1000)) +class ByteVector_SetTests { @test public ReadOnly() { // Arrange diff --git a/test/id3v2/attachmentsFrameTests.ts b/test/id3v2/attachmentsFrameTests.ts index c56e34f8..4ddcc7a4 100644 --- a/test/id3v2/attachmentsFrameTests.ts +++ b/test/id3v2/attachmentsFrameTests.ts @@ -12,7 +12,6 @@ import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import {ByteVector} from "../../src/byteVector"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; import {IFileAbstraction} from "../../src/fileAbstraction"; -import TestStream from "../utilities/testStream"; // Setup chai Chai.use(ChaiAsPromised); @@ -36,7 +35,8 @@ const rawHeader = ByteVector.concatenate( const testHeader = new Id3v2FrameHeader(FrameTypes.APIC, 3); -@suite(timeout(3000), slow(1000)) class AttachmentFrameFromFileTests { +@suite(timeout(3000), slow(1000)) +class Id3v2_AttachmentFrame_FromFileTests { @test public falsyAbstraction() { // Act/Assert @@ -93,7 +93,8 @@ const testHeader = new Id3v2FrameHeader(FrameTypes.APIC, 3); } } -@suite(timeout(3000), slow(1000)) class AttachmentFrameOffsetRawDataTests { +@suite(timeout(3000), slow(1000)) +class Id3v2_AttachmentFrame_OffsetRawDataTests { @test public falsyData() { // Act/Assert @@ -131,7 +132,8 @@ const testHeader = new Id3v2FrameHeader(FrameTypes.APIC, 3); } } -@suite(timeout(3000), slow(1000)) class AttachmentFrameFromPictureTests { +@suite(timeout(3000), slow(1000)) +class Id3v2_AttachmentFrame_FromPictureTests { @test public falsyPicture() { // Act/Assert @@ -166,7 +168,8 @@ const testHeader = new Id3v2FrameHeader(FrameTypes.APIC, 3); } } -@suite(timeout(3000), slow(1000)) class AttachmentFrameRawDataTests { +@suite(timeout(3000), slow(1000)) +class Id3v2_AttachmentFrame_FromRawDataTests { @test public falsyData() { // Act/Assert diff --git a/test/id3v2/commentsFrameTests.ts b/test/id3v2/commentsFrameTests.ts index 90c4b604..2ddd3982 100644 --- a/test/id3v2/commentsFrameTests.ts +++ b/test/id3v2/commentsFrameTests.ts @@ -4,7 +4,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import CommentsFrame from "../../src/id3v2/frames/commentsFrame"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertiesTests from "./framePropertiesTests"; +import FramePropertyTests from "./framePropertyTests"; import FrameTypes from "../../src/id3v2/frameTypes"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import {ByteVector, StringType} from "../../src/byteVector"; @@ -31,7 +31,7 @@ function getTestFrame(): CommentsFrame { } @suite(timeout(3000), slow(1000)) -class CommentsFrameConstructorTests extends FrameConstructorTests { +class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { return CommentsFrame.fromOffsetRawData; } @@ -56,7 +56,7 @@ class CommentsFrameConstructorTests extends FrameConstructorTests { const frame = CommentsFrame.fromDescription(description); // Assert - CommentsFrameConstructorTests.validateFrame(frame, description, "XXX", Id3v2TagSettings.defaultEncoding, ""); + this.validateFrame(frame, description, "XXX", Id3v2TagSettings.defaultEncoding, ""); } @test @@ -69,7 +69,7 @@ class CommentsFrameConstructorTests extends FrameConstructorTests { const frame = CommentsFrame.fromDescription(description, language); // Assert - CommentsFrameConstructorTests.validateFrame(frame, description, language, Id3v2TagSettings.defaultEncoding, ""); + this.validateFrame(frame, description, language, Id3v2TagSettings.defaultEncoding, ""); } @test @@ -83,7 +83,7 @@ class CommentsFrameConstructorTests extends FrameConstructorTests { const frame = CommentsFrame.fromDescription(description, language, encoding); // Assert - CommentsFrameConstructorTests.validateFrame(frame, description, language, encoding, ""); + this.validateFrame(frame, description, language, encoding, ""); } @test @@ -117,7 +117,7 @@ class CommentsFrameConstructorTests extends FrameConstructorTests { const frame = CommentsFrame.fromOffsetRawData(data, 2, header); // Assert - CommentsFrameConstructorTests.validateFrame(frame, "", "eng", StringType.Latin1, ""); + this.validateFrame(frame, "", "eng", StringType.Latin1, ""); } @test @@ -137,7 +137,7 @@ class CommentsFrameConstructorTests extends FrameConstructorTests { const frame = CommentsFrame.fromOffsetRawData(data, 2, header); // Assert - CommentsFrameConstructorTests.validateFrame(frame, "", "eng", StringType.Latin1, "fux"); + this.validateFrame(frame, "", "eng", StringType.Latin1, "fux"); } @test @@ -159,7 +159,7 @@ class CommentsFrameConstructorTests extends FrameConstructorTests { const frame = CommentsFrame.fromOffsetRawData(data, 2, header); // Assert - CommentsFrameConstructorTests.validateFrame(frame, "fux", "eng", StringType.Latin1, "bux"); + this.validateFrame(frame, "fux", "eng", StringType.Latin1, "bux"); } @test @@ -181,7 +181,7 @@ class CommentsFrameConstructorTests extends FrameConstructorTests { const frame = CommentsFrame.fromOffsetRawData(data, 2, header); // Assert - CommentsFrameConstructorTests.validateFrame(frame, "fux", "eng", StringType.Latin1, "bux"); + this.validateFrame(frame, "fux", "eng", StringType.Latin1, "bux"); } @test @@ -213,7 +213,7 @@ class CommentsFrameConstructorTests extends FrameConstructorTests { const frame = CommentsFrame.fromRawData(data, 4); // Assert - CommentsFrameConstructorTests.validateFrame(frame, "", "eng", StringType.Latin1, ""); + this.validateFrame(frame, "", "eng", StringType.Latin1, ""); } @test @@ -232,7 +232,7 @@ class CommentsFrameConstructorTests extends FrameConstructorTests { const frame = CommentsFrame.fromRawData(data, 4); // Assert - CommentsFrameConstructorTests.validateFrame(frame, "", "eng", StringType.Latin1, "fux"); + this.validateFrame(frame, "", "eng", StringType.Latin1, "fux"); } @test @@ -252,7 +252,7 @@ class CommentsFrameConstructorTests extends FrameConstructorTests { const frame = CommentsFrame.fromRawData(data, 4); // Assert - CommentsFrameConstructorTests.validateFrame(frame, "fux", "eng", StringType.Latin1, "bux"); + this.validateFrame(frame, "fux", "eng", StringType.Latin1, "bux"); } @test @@ -273,10 +273,10 @@ class CommentsFrameConstructorTests extends FrameConstructorTests { const frame = CommentsFrame.fromRawData(data, 4); // Assert - CommentsFrameConstructorTests.validateFrame(frame, "fux", "eng", StringType.Latin1, "bux"); + this.validateFrame(frame, "fux", "eng", StringType.Latin1, "bux"); } - private static validateFrame( + private validateFrame( frame: CommentsFrame, expectedDesc: string, expectedLang: string, @@ -295,16 +295,16 @@ class CommentsFrameConstructorTests extends FrameConstructorTests { } @suite(timeout(3000), slow(1000)) -class CommentsFramePropertiesTests extends FramePropertiesTests { +class Id3v2_CommentsFrame_PropertyTests { @test public description() { const frame = getTestFrame(); const set = (v: string) => { frame.description = v; }; const get = () => frame.description; - this.propertyRoundTrip(set, get, "fux"); - this.propertyNormalized(set, get, undefined, ""); - this.propertyNormalized(set, get, null, ""); + FramePropertyTests.propertyRoundTrip(set, get, "fux"); + FramePropertyTests.propertyNormalized(set, get, undefined, ""); + FramePropertyTests.propertyNormalized(set, get, null, ""); } @test @@ -313,11 +313,11 @@ class CommentsFramePropertiesTests extends FramePropertiesTests { const set = (v: string) => { frame.language = v; }; const get = () => frame.language; - this.propertyRoundTrip(set, get, "jpn"); - this.propertyNormalized(set, get, undefined, "XXX"); - this.propertyNormalized(set, get, null, "XXX"); - this.propertyNormalized(set, get, "ab", "XXX"); - this.propertyNormalized(set, get, "abcd", "abc"); + FramePropertyTests.propertyRoundTrip(set, get, "jpn"); + FramePropertyTests.propertyNormalized(set, get, undefined, "XXX"); + FramePropertyTests.propertyNormalized(set, get, null, "XXX"); + FramePropertyTests.propertyNormalized(set, get, "ab", "XXX"); + FramePropertyTests.propertyNormalized(set, get, "abcd", "abc"); } @test @@ -326,21 +326,25 @@ class CommentsFramePropertiesTests extends FramePropertiesTests { const set = (v: string) => { frame.text = v; }; const get = () => frame.text; - this.propertyRoundTrip(set, get, "fux"); - this.propertyNormalized(set, get, undefined, ""); - this.propertyNormalized(set, get, null, ""); + FramePropertyTests.propertyRoundTrip(set, get, "fux"); + FramePropertyTests.propertyNormalized(set, get, undefined, ""); + FramePropertyTests.propertyNormalized(set, get, null, ""); } @test public textEncoding() { const frame = getTestFrame(); - this.propertyRoundTrip((v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16); + FramePropertyTests.propertyRoundTrip( + (v) => { frame.textEncoding = v; }, + () => frame.textEncoding, + StringType.UTF16 + ); } } @suite(timeout(3000), slow(1000)) -class CommentsFrameMethodTests { +class Id3v2_CommentsFrame_MethodTests { @test public find_falsyFrames() { // Act/Assert diff --git a/test/id3v2/eventTimeCodeFrameTests.ts b/test/id3v2/eventTimeCodeFrameTests.ts index 0623b260..86cf897a 100644 --- a/test/id3v2/eventTimeCodeFrameTests.ts +++ b/test/id3v2/eventTimeCodeFrameTests.ts @@ -3,20 +3,21 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertiesTests from "./framePropertiesTests"; +import FramePropertyTests from "./framePropertyTests"; import FrameTypes from "../../src/id3v2/frameTypes"; import {ByteVector} from "../../src/byteVector"; import {EventTimeCode, EventTimeCodeFrame} from "../../src/id3v2/frames/eventTimeCodeFrame"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameFlags, Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; import {EventType, TimestampFormat} from "../../src/id3v2/utilTypes"; +import framePropertyTests from "./framePropertyTests"; // Setup Chai Chai.use(ChaiAsPromised); const assert = Chai.assert; @suite(timeout(3000), slow(1000)) -class EventTimeCodeTests extends FramePropertiesTests { +class EventTimeCodeTests { @test public constructor_invalidTime() { // Act/Assert @@ -60,7 +61,7 @@ class EventTimeCodeTests extends FramePropertiesTests { const output = EventTimeCode.fromEmpty(); // Act/Assert - this.propertyRoundTrip((v) => { output.time = v; }, () => output.time, 123); + FramePropertyTests.propertyRoundTrip((v) => { output.time = v; }, () => output.time, 123); } @test @@ -69,7 +70,11 @@ class EventTimeCodeTests extends FramePropertiesTests { const output = EventTimeCode.fromEmpty(); // Act/Assert - this.propertyRoundTrip((v) => { output.eventType = v; }, () => output.eventType, EventType.IntroEnd); + FramePropertyTests.propertyRoundTrip( + (v) => { output.eventType = v; }, + () => output.eventType, + EventType.IntroEnd + ); } @test @@ -207,7 +212,7 @@ class EventTimeCodeFrameConstructorTests extends FrameConstructorTests { } @suite(timeout(3000), slow(1000)) -class EventTimeCodeFramePropertyTests extends FramePropertiesTests { +class EventTimeCodeFramePropertyTests { @test public events() { // Arrange @@ -216,10 +221,10 @@ class EventTimeCodeFramePropertyTests extends FramePropertiesTests { const get = () => frame.events; // Act / Assert - this.propertyRoundTrip(set, get, [new EventTimeCode(EventType.Profanity, 123)]); - this.propertyRoundTrip(set, get, []); - this.propertyNormalized(set, get, undefined, []); - this.propertyNormalized(set, get, null, []); + FramePropertyTests.propertyRoundTrip(set, get, [new EventTimeCode(EventType.Profanity, 123)]); + FramePropertyTests.propertyRoundTrip(set, get, []); + framePropertyTests.propertyNormalized(set, get, undefined, []); + FramePropertyTests.propertyNormalized(set, get, null, []); } @test @@ -228,10 +233,11 @@ class EventTimeCodeFramePropertyTests extends FramePropertiesTests { const frame = EventTimeCodeFrame.fromTimestampFormat(TimestampFormat.AbsoluteMilliseconds); // Act / Assert - this.propertyRoundTrip( + FramePropertyTests.propertyRoundTrip( (v) => { frame.timestampFormat = v; }, () => frame.timestampFormat, - TimestampFormat.AbsoluteMpegFrames); + TimestampFormat.AbsoluteMpegFrames + ); } } diff --git a/test/id3v2/framePropertiesTests.ts b/test/id3v2/framePropertyTests.ts similarity index 94% rename from test/id3v2/framePropertiesTests.ts rename to test/id3v2/framePropertyTests.ts index ab052d19..e583162c 100644 --- a/test/id3v2/framePropertiesTests.ts +++ b/test/id3v2/framePropertyTests.ts @@ -5,7 +5,7 @@ import * as ChaiAsPromised from "chai-as-promised"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -export default class FramePropertiesTests { +export default class FramePropertyTests { public static propertyRoundTrip(set: (v: T) => void, get: () => T, val: T) { // Act set(val); diff --git a/test/id3v2/musicCdIdentifierFrameTests.ts b/test/id3v2/musicCdIdentifierFrameTests.ts index 6f09ed69..85433b1a 100644 --- a/test/id3v2/musicCdIdentifierFrameTests.ts +++ b/test/id3v2/musicCdIdentifierFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertiesTests from "./framePropertiesTests"; +import FramePropertyTests from "./framePropertyTests"; import FrameTypes from "../../src/id3v2/frameTypes"; import MusicCdIdentifierFrame from "../../src/id3v2/frames/musicCdIdentifierFrame"; import {ByteVector, StringType} from "../../src/byteVector"; @@ -15,7 +15,7 @@ Chai.use(ChaiAsPromised); const assert = Chai.assert; @suite(timeout(3000), slow(1000)) -class Id3v2_MusicCdIdentifierFrame extends FrameConstructorTests { +class Id3v2_MusicCdIdentifierFrameTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { return MusicCdIdentifierFrame.fromOffsetRawData; } @@ -66,7 +66,7 @@ class Id3v2_MusicCdIdentifierFrame extends FrameConstructorTests { const data = ByteVector.fromString("fuxbuxqux", StringType.Latin1); // Act / Assert - FramePropertiesTests.propertyRoundTrip((v) => { frame.data = v; }, () => frame.data, data); + FramePropertyTests.propertyRoundTrip((v) => { frame.data = v; }, () => frame.data, data); } @test diff --git a/test/id3v2/PopularimeterFrameTests.ts b/test/id3v2/popularimeterFrameTests.ts similarity index 91% rename from test/id3v2/PopularimeterFrameTests.ts rename to test/id3v2/popularimeterFrameTests.ts index ef9f34cd..1819fa14 100644 --- a/test/id3v2/PopularimeterFrameTests.ts +++ b/test/id3v2/popularimeterFrameTests.ts @@ -1,12 +1,12 @@ +import * as BigInt from "big-integer"; import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; -import * as BigInt from "big-integer"; -import ConstructorTests from "./frameConstructorTests"; +import FrameConstructorTests from "./frameConstructorTests"; +import FramePropertyTests from "./framePropertyTests"; import FrameTypes from "../../src/id3v2/frameTypes"; import PopularimeterFrame from "../../src/id3v2/frames/popularimeterFrame"; -import PropertyTests from "./framePropertiesTests"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; @@ -16,7 +16,7 @@ Chai.use(ChaiAsPromised); const assert = Chai.assert; @suite(timeout(3000), slow(1000)) -class PopularimeterFrameConstructorTests extends ConstructorTests { +class Id3v2_PopularimeterFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { return PopularimeterFrame.fromOffsetRawData; } @@ -178,7 +178,7 @@ class PopularimeterFrameConstructorTests extends ConstructorTests { } @suite(timeout(3000), slow(1000)) -class PopularimeterFramePropertyTests extends PropertyTests { +class Id3v2_PopularimeterFrame_PropertyTests { @test public playCount() { // Arrange @@ -187,10 +187,10 @@ class PopularimeterFramePropertyTests extends PropertyTests { const get = () => frame.playCount; // Act - this.propertyRoundTrip(set, get, BigInt(1234)); - this.propertyRoundTrip(set, get, undefined); - this.propertyThrows(set, null); - this.propertyThrows(set, BigInt(-1)); + FramePropertyTests.propertyRoundTrip(set, get, BigInt(1234)); + FramePropertyTests.propertyRoundTrip(set, get, undefined); + FramePropertyTests.propertyThrows(set, null); + FramePropertyTests.propertyThrows(set, BigInt(-1)); } @test @@ -201,10 +201,10 @@ class PopularimeterFramePropertyTests extends PropertyTests { const get = () => frame.rating; // Act - this.propertyRoundTrip(set, get, 5); - this.propertyThrows(set, -1); - this.propertyThrows(set, 1.23); - this.propertyThrows(set, 0x100); + FramePropertyTests.propertyRoundTrip(set, get, 5); + FramePropertyTests.propertyThrows(set, -1); + FramePropertyTests.propertyThrows(set, 1.23); + FramePropertyTests.propertyThrows(set, 0x100); } @test @@ -215,14 +215,14 @@ class PopularimeterFramePropertyTests extends PropertyTests { const get = () => frame.user; // Act - this.propertyRoundTrip(set, get, "bux"); - this.propertyNormalized(set, get, undefined, ""); - this.propertyNormalized(set, get, null, ""); + FramePropertyTests.propertyRoundTrip(set, get, "bux"); + FramePropertyTests.propertyNormalized(set, get, undefined, ""); + FramePropertyTests.propertyNormalized(set, get, null, ""); } } @suite(timeout(3000), slow(1000)) -class PopularimeterFrameMethodTests { +class Id3v2_PopularimeterFrame_MethodTests { @test public find_falsyFrames() { // Act / Assert diff --git a/test/id3v2/privateFrameTests.ts b/test/id3v2/privateFrameTests.ts index 6d4eef8a..6c065bf6 100644 --- a/test/id3v2/privateFrameTests.ts +++ b/test/id3v2/privateFrameTests.ts @@ -2,10 +2,10 @@ import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; -import ConstructorTests from "./frameConstructorTests"; +import FrameConstructorTests from "./frameConstructorTests"; +import FramePropertyTests from "./framePropertyTests"; import FrameTypes from "../../src/id3v2/frameTypes"; import PrivateFrame from "../../src/id3v2/frames/privateFrame"; -import PropertyTests from "./framePropertiesTests"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; @@ -15,7 +15,7 @@ Chai.use(ChaiAsPromised); const assert = Chai.assert; @suite(timeout(3000), slow(1000)) -class PrivateFrameConstructorTests extends ConstructorTests { +class Id3v2_PrivateFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { return PrivateFrame.fromOffsetRawData; } @@ -113,14 +113,14 @@ class PrivateFrameConstructorTests extends ConstructorTests { } @suite(timeout(3000), slow(1000)) -class PrivateFramePropertyTests extends PropertyTests { +class Id3v2_PrivateFrame_PropertyTests { @test public privateData() { // Arrange const frame = PrivateFrame.fromOwner("fux"); // Act / Assert - this.propertyRoundTrip( + FramePropertyTests.propertyRoundTrip( (v) => { frame.privateData = v; }, () => frame.privateData, ByteVector.fromString("bux") @@ -129,7 +129,7 @@ class PrivateFramePropertyTests extends PropertyTests { } @suite(timeout(3000), slow(1000)) -class PrivateFrameMethodTests { +class Id3v2_PrivateFrame_MethodTests { @test public find_noFrames() { // Arrange diff --git a/test/id3v2/syncDataTests.ts b/test/id3v2/syncDataTests.ts index 510d30a4..5b579ae4 100644 --- a/test/id3v2/syncDataTests.ts +++ b/test/id3v2/syncDataTests.ts @@ -11,7 +11,7 @@ Chai.use(ChaiAsPromised); const assert = Chai.assert; @suite(timeout(3000), slow(1000)) -class SyncDataTests { +class Id3v2_SyncDataTests { @test public fromUint_InvalidValues() { // Act/Assert @@ -49,7 +49,7 @@ class SyncDataTests { @test public toUint_TooFewBytes() { // Arrange - const input = ByteVector.fromSize(2, 0x10) + const input = ByteVector.fromSize(2, 0x10); // Act const output = SyncData.toUint(input); diff --git a/test/id3v2/synchronizedLyricsFrameTests.ts b/test/id3v2/synchronizedLyricsFrameTests.ts index 6543b3ba..221b2c44 100644 --- a/test/id3v2/synchronizedLyricsFrameTests.ts +++ b/test/id3v2/synchronizedLyricsFrameTests.ts @@ -2,10 +2,10 @@ import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; -import ConstructorTests from "./frameConstructorTests"; +import FrameConstructorTests from "./frameConstructorTests"; +import FramePropertyTests from "./framePropertyTests"; import FrameTypes from "../../src/id3v2/frameTypes"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; -import PropertiesTests from "./framePropertiesTests"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; @@ -47,7 +47,7 @@ class SynchronizedTextTests { } @suite(timeout(3000), slow(1000)) -class SynchronizedLyricsFrameConstructorTests extends ConstructorTests { +class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { return SynchronizedLyricsFrame.fromOffsetRawData; } @@ -339,7 +339,7 @@ class SynchronizedLyricsFrameConstructorTests extends ConstructorTests { } @suite(timeout(3000), slow(1000)) -class SynchronizedLyricsFramePropertyTests extends PropertiesTests { +class Id3v2_SynchronizedLyricsFrame_PropertyTests { @test public description() { // Arrange @@ -348,8 +348,8 @@ class SynchronizedLyricsFramePropertyTests extends PropertiesTests { const get = () => frame.description; // Act / Assert - this.propertyRoundTrip(set, get, "fux" ); - this.propertyRoundTrip(set, get, undefined); + FramePropertyTests.propertyRoundTrip(set, get, "fux" ); + FramePropertyTests.propertyRoundTrip(set, get, undefined); } @test @@ -358,7 +358,11 @@ class SynchronizedLyricsFramePropertyTests extends PropertiesTests { const frame = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord); // Act / Assert - this.propertyRoundTrip((v) => { frame.format = v; }, () => frame.format, TimestampFormat.AbsoluteMilliseconds); + FramePropertyTests.propertyRoundTrip( + (v) => { frame.format = v; }, + () => frame.format, + TimestampFormat.AbsoluteMilliseconds + ); } @test @@ -369,9 +373,9 @@ class SynchronizedLyricsFramePropertyTests extends PropertiesTests { const get = () => frame.language; // Act / Assert - this.propertyRoundTrip(set, get, "fux"); - this.propertyRoundTrip(set, get, "shoe"); - this.propertyRoundTrip(set, get, "ab"); + FramePropertyTests.propertyRoundTrip(set, get, "fux"); + FramePropertyTests.propertyRoundTrip(set, get, "shoe"); + FramePropertyTests.propertyRoundTrip(set, get, "ab"); } @test @@ -383,9 +387,9 @@ class SynchronizedLyricsFramePropertyTests extends PropertiesTests { const value = [new SynchronizedText(123, "foo")]; // Act / Assert - this.propertyRoundTrip(set, get, value); - this.propertyNormalized(set, get, undefined, []); - this.propertyNormalized(set, get, null, []); + FramePropertyTests.propertyRoundTrip(set, get, value); + FramePropertyTests.propertyNormalized(set, get, undefined, []); + FramePropertyTests.propertyNormalized(set, get, null, []); } @test @@ -394,7 +398,11 @@ class SynchronizedLyricsFramePropertyTests extends PropertiesTests { const frame = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord); // Act / Assert - this.propertyRoundTrip((v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16BE); + FramePropertyTests.propertyRoundTrip( + (v) => { frame.textEncoding = v; }, + () => frame.textEncoding, + StringType.UTF16BE + ); } @test @@ -403,12 +411,16 @@ class SynchronizedLyricsFramePropertyTests extends PropertiesTests { const frame = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord); // Act / Assert - this.propertyRoundTrip((v) => { frame.textType = v; }, () => frame.textType, SynchronizedTextType.Trivia); + FramePropertyTests.propertyRoundTrip( + (v) => { frame.textType = v; }, + () => frame.textType, + SynchronizedTextType.Trivia + ); } } @suite(timeout(3000), slow(1000)) -class SynchronizedLyricsFrameMethodTests { +class Id3v2_SynchronizedLyricsFrame_MethodTests { @test public find_falsyFrames() { // Act / Assert diff --git a/test/id3v2/extendedHeaderTests.ts b/test/id3v2/tagExtendedHeaderTests.ts similarity index 98% rename from test/id3v2/extendedHeaderTests.ts rename to test/id3v2/tagExtendedHeaderTests.ts index d5d54f55..3740d80f 100644 --- a/test/id3v2/extendedHeaderTests.ts +++ b/test/id3v2/tagExtendedHeaderTests.ts @@ -10,7 +10,7 @@ Chai.use(ChaiAsPromised); const assert = Chai.assert; @suite(timeout(3000), slow(1000)) -class extendedHeaderTests { +class Id3v2_TagExtendedHeaderTests { @test public fromData_falsyData() { // Act/Assert diff --git a/test/id3v2/tagFooterTests.ts b/test/id3v2/tagFooterTests.ts index 94c120b2..6ec33693 100644 --- a/test/id3v2/tagFooterTests.ts +++ b/test/id3v2/tagFooterTests.ts @@ -24,12 +24,12 @@ const getTestFooter = (majorVersion: number, minorVersion: number, flags: Header }; @suite(timeout(3000), slow(1000)) -class TagFooterConstructorTests { +class Id3v2_TagFooterConstructorTests { @test public falsyData() { // Act/Assert - assert.throws(() => { new Footer(null); }); - assert.throws(() => { new Footer(undefined); }); + assert.throws(() => { const _ = new Footer(null); }); + assert.throws(() => { const _ = new Footer(undefined); }); } @test @@ -38,7 +38,7 @@ class TagFooterConstructorTests { const data = ByteVector.fromSize(1); // Act/Assert - assert.throws(() => { new Footer(data); }); + assert.throws(() => { const _ = new Footer(data); }); } @test @@ -47,7 +47,7 @@ class TagFooterConstructorTests { const data = ByteVector.fromSize(10); // Act/Assert - assert.throws(() => { new Footer(data); }); + assert.throws(() => { const _ = new Footer(data); }); } @test @@ -59,7 +59,7 @@ class TagFooterConstructorTests { ); // Act/Assert - assert.throws(() => { new Footer(data); }); + assert.throws(() => { const _ = new Footer(data); }); } @test @@ -75,10 +75,10 @@ class TagFooterConstructorTests { const testData4 = ByteVector.concatenate(testData, 0x00, 0x00, 0x00, 0x80); // Act/Assert - assert.throws(() => { const q = new Footer(testData1); }); - assert.throws(() => { const q = new Footer(testData2); }); - assert.throws(() => { const q = new Footer(testData3); }); - assert.throws(() => { const q = new Footer(testData4); }); + assert.throws(() => { const _ = new Footer(testData1); }); + assert.throws(() => { const _ = new Footer(testData2); }); + assert.throws(() => { const _ = new Footer(testData3); }); + assert.throws(() => { const _ = new Footer(testData4); }); } @test @@ -204,7 +204,7 @@ class TagFooterPropertyTests { } @test - publicsetTagSize_validValue() { + public publicsetTagSize_validValue() { // Arrange const footer = getTestFooter(4, 0, HeaderFlags.None); diff --git a/test/id3v2/tagHeaderTests.ts b/test/id3v2/tagHeaderTests.ts index be787186..80249fe0 100644 --- a/test/id3v2/tagHeaderTests.ts +++ b/test/id3v2/tagHeaderTests.ts @@ -24,12 +24,12 @@ const getTestHeader = (majorVersion: number, minorVersion: number, flags: Header }; @suite(timeout(3000), slow(1000)) -class TagHeaderConstructorTests { +class Id3v2_TagHeaderConstructorTests { @test public falsyData() { // Act/Assert - assert.throws(() => { const q = new Header(null); }); - assert.throws(() => { const q = new Header(undefined); }); + assert.throws(() => { const _ = new Header(null); }); + assert.throws(() => { const _ = new Header(undefined); }); } @test @@ -39,14 +39,14 @@ class TagHeaderConstructorTests { const data1 = ByteVector.fromSize(1); // Act/Assert - assert.throws(() => { const q = new Header(data0); }); - assert.throws(() => { const q = new Header(data1); }); + assert.throws(() => { const _ = new Header(data0); }); + assert.throws(() => { const _ = new Header(data1); }); } @test public invalidStartOfData() { // Act/Assert - assert.throws(() => { const q = new Header(TestConstants.testByteVector); }); + assert.throws(() => { const _ = new Header(TestConstants.testByteVector); }); } @test @@ -60,7 +60,7 @@ class TagHeaderConstructorTests { ); // Act/Assert - assert.throws(() => { const q = new Header(testData); }); + assert.throws(() => { const _ = new Header(testData); }); } @test @@ -74,7 +74,7 @@ class TagHeaderConstructorTests { ); // Act/Assert - assert.throws(() => { const q = new Header(testData); }); + assert.throws(() => { const _ = new Header(testData); }); } @test @@ -88,7 +88,7 @@ class TagHeaderConstructorTests { ); // Act/Assert - assert.throws(() => { const q = new Header(testData); }); + assert.throws(() => { const _ = new Header(testData); }); } @test @@ -105,10 +105,10 @@ class TagHeaderConstructorTests { const testData4 = ByteVector.concatenate(testData, 0x00, 0x00, 0x00, 0x80); // Act/Assert - assert.throws(() => {const q = new Header(testData1); }); - assert.throws(() => {const q = new Header(testData2); }); - assert.throws(() => {const q = new Header(testData3); }); - assert.throws(() => {const q = new Header(testData4); }); + assert.throws(() => {const _ = new Header(testData1); }); + assert.throws(() => {const _ = new Header(testData2); }); + assert.throws(() => {const _ = new Header(testData3); }); + assert.throws(() => {const _ = new Header(testData4); }); } @test @@ -260,7 +260,7 @@ class TagHeaderPropertyTests { header.majorVersion = 2; // Assert - assert.equal(header.majorVersion, 2) + assert.equal(header.majorVersion, 2); assert.equal(header.flags & flags, 0); } @@ -313,7 +313,7 @@ class TagHeaderPropertyTests { } @test - publicsetTagSize_validValue() { + public setTagSize_validValue() { // Arrange const header = getTestHeader(4, 0, HeaderFlags.None); diff --git a/test/id3v2/termsOfUseFrameTests.ts b/test/id3v2/termsOfUseFrameTests.ts index 1e1a659e..0598a15f 100644 --- a/test/id3v2/termsOfUseFrameTests.ts +++ b/test/id3v2/termsOfUseFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertiesTests from "./framePropertiesTests"; +import FramePropertyTests from "./framePropertyTests"; import FrameTypes from "../../src/id3v2/frameTypes"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import TermsOfUseFrame from "../../src/id3v2/frames/termsOfUseFrame"; @@ -17,7 +17,7 @@ Chai.use(ChaiAsPromised); const assert = Chai.assert; @suite(timeout(3000), slow(1000)) -class TermsOfUseFrameConstructorsTests extends FrameConstructorTests { +class Id3v2_TermsOfUseFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { return TermsOfUseFrame.fromOffsetRawData; } @@ -124,7 +124,7 @@ class TermsOfUseFrameConstructorsTests extends FrameConstructorTests { } @suite(timeout(3000), slow(1000)) -class TermsOfUseFramePropertyTests extends FramePropertiesTests { +class Id3v2_TermsOfUseFrame_PropertyTests { @test public language() { // Arrange @@ -133,37 +133,41 @@ class TermsOfUseFramePropertyTests extends FramePropertiesTests { // Act/Assert const frame = TermsOfUseFrame.fromFields("eng"); - this.propertyNormalized(set, get, "fu", "XXX"); - this.propertyNormalized(set, get, "fuxx", "fux"); - this.propertyNormalized(set, get, undefined, "XXX"); - this.propertyNormalized(set, get, null, "XXX"); - this.propertyRoundTrip(set, get, "fux"); + FramePropertyTests.propertyNormalized(set, get, "fu", "XXX"); + FramePropertyTests.propertyNormalized(set, get, "fuxx", "fux"); + FramePropertyTests.propertyNormalized(set, get, undefined, "XXX"); + FramePropertyTests.propertyNormalized(set, get, null, "XXX"); + FramePropertyTests.propertyRoundTrip(set, get, "fux"); } @test public text() { const frame = TermsOfUseFrame.fromFields("eng"); - this.propertyRoundTrip((v) => { frame.text = v; }, () => frame.text, "fux"); - this.propertyRoundTrip((v) => { frame.text = v; }, () => frame.text, undefined); + FramePropertyTests.propertyRoundTrip((v) => { frame.text = v; }, () => frame.text, "fux"); + FramePropertyTests.propertyRoundTrip((v) => { frame.text = v; }, () => frame.text, undefined); } @test public textEncoding() { const frame = TermsOfUseFrame.fromFields("eng", StringType.Latin1); - this.propertyRoundTrip((v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16); + FramePropertyTests.propertyRoundTrip( + (v) => { frame.textEncoding = v; }, + () => frame.textEncoding, + StringType.UTF16 + ); } } @suite(timeout(3000), slow(1000)) -class TermsOfUseFrameMethodTests { +class Id3v2_TermsOfUseFrame_MethodTests { @test - public get_falsyFrames() { + public find_falsyFrames() { // Act/Assert assert.throws(() => { TermsOfUseFrame.find(undefined); }); } @test - public get_noFrames() { + public find_noFrames() { // Act const output = TermsOfUseFrame.find([]); @@ -172,7 +176,7 @@ class TermsOfUseFrameMethodTests { } @test - public get_matchWithFrames_returnsFirst() { + public find_matchWithFrames_returnsFirst() { // Arrange const frames = [ TermsOfUseFrame.fromFields("eng"), @@ -188,7 +192,7 @@ class TermsOfUseFrameMethodTests { } @test - public get_matchWithFramesWithLanguage() { + public find_matchWithFramesWithLanguage() { // Arrange const frames = [ TermsOfUseFrame.fromFields("eng"), @@ -204,13 +208,13 @@ class TermsOfUseFrameMethodTests { } @test - public getPreferred_falsyFrames() { + public findPreferred_falsyFrames() { // Act/Assert assert.throws(() => TermsOfUseFrame.findPreferred(undefined, "eng")); } @test - public getPreferred_noFrames() { + public findPreferred_noFrames() { // Act const output = TermsOfUseFrame.findPreferred([], "eng"); @@ -219,7 +223,7 @@ class TermsOfUseFrameMethodTests { } @test - public getPrefeerred_nonExactMatch() { + public findPreferred_nonExactMatch() { // Arrange const frames = [ TermsOfUseFrame.fromFields("eng"), @@ -235,7 +239,7 @@ class TermsOfUseFrameMethodTests { } @test - public getPreferred_exactMatch() { + public findPreferred_exactMatch() { // Arrange const frames = [ TermsOfUseFrame.fromFields("eng"), diff --git a/test/id3v2/textInformationFrameTests.ts b/test/id3v2/textInformationFrameTests.ts index 317a2504..abaa9cae 100644 --- a/test/id3v2/textInformationFrameTests.ts +++ b/test/id3v2/textInformationFrameTests.ts @@ -2,7 +2,8 @@ import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; -import ConstructorTests from "./frameConstructorTests"; +import FrameConstructorTests from "./frameConstructorTests"; +import FramePropertyTests from "./framePropertyTests"; import FrameTypes from "../../src/id3v2/frameTypes"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import {TextInformationFrame} from "../../src/id3v2/frames/textInformationFrame"; @@ -29,7 +30,7 @@ function getTestFrame(): TextInformationFrame { } @suite(timeout(3000), slow(1000)) -class TextInformationFrameConstructorTests extends ConstructorTests { +class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { return TextInformationFrame.fromOffsetRawData; } @@ -60,7 +61,7 @@ class TextInformationFrameConstructorTests extends ConstructorTests { const frame = TextInformationFrame.fromIdentifier(FrameTypes.TCOP, StringType.Latin1); // Assert - TextInformationFrameConstructorTests.assertFrame(frame, FrameTypes.TCOP, []); + this.assertFrame(frame, FrameTypes.TCOP, []); } @test @@ -77,7 +78,7 @@ class TextInformationFrameConstructorTests extends ConstructorTests { const frame = TextInformationFrame.fromRawData(data, 3); // Assert - TextInformationFrameConstructorTests.assertFrame(frame, FrameTypes.TCOP, []); + this.assertFrame(frame, FrameTypes.TCOP, []); } @test @@ -95,7 +96,7 @@ class TextInformationFrameConstructorTests extends ConstructorTests { const frame = TextInformationFrame.fromRawData(data, 3); // Assert - TextInformationFrameConstructorTests.assertFrame(frame, FrameTypes.TCOP, []); + this.assertFrame(frame, FrameTypes.TCOP, []); } @test @@ -116,7 +117,7 @@ class TextInformationFrameConstructorTests extends ConstructorTests { const frame = TextInformationFrame.fromRawData(data, 4); // Assert - TextInformationFrameConstructorTests.assertFrame(frame, FrameTypes.TCOP, ["fux", "bux"]); + this.assertFrame(frame, FrameTypes.TCOP, ["fux", "bux"]); } @test @@ -137,7 +138,7 @@ class TextInformationFrameConstructorTests extends ConstructorTests { const frame = TextInformationFrame.fromRawData(data, 4); // Assert - TextInformationFrameConstructorTests.assertFrame(frame, FrameTypes.TXXX, ["fux", "bux"]); + this.assertFrame(frame, FrameTypes.TXXX, ["fux", "bux"]); } @test @@ -155,7 +156,7 @@ class TextInformationFrameConstructorTests extends ConstructorTests { const frame = TextInformationFrame.fromRawData(data, 3); // Assert - TextInformationFrameConstructorTests.assertFrame(frame, FrameTypes.TCOM, ["fux", "bux"]); + this.assertFrame(frame, FrameTypes.TCOM, ["fux", "bux"]); } @test @@ -174,7 +175,7 @@ class TextInformationFrameConstructorTests extends ConstructorTests { const frame = TextInformationFrame.fromRawData(data, 3); // Assert - TextInformationFrameConstructorTests.assertFrame(frame, FrameTypes.TCON, [ + this.assertFrame(frame, FrameTypes.TCON, [ "SomeGenre", "32", "32", @@ -203,10 +204,10 @@ class TextInformationFrameConstructorTests extends ConstructorTests { const frame = TextInformationFrame.fromOffsetRawData(data, 2, header); // Assert - TextInformationFrameConstructorTests.assertFrame(frame, FrameTypes.TCOP, ["fux", "bux"]); + this.assertFrame(frame, FrameTypes.TCOP, ["fux", "bux"]); } - private static assertFrame(frame: TextInformationFrame, frameId: ByteVector, text: string[]): void { + private assertFrame(frame: TextInformationFrame, frameId: ByteVector, text: string[]): void { assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.TextInformationFrame); assert.isTrue(ByteVector.equal(frame.frameId, frameId)); @@ -218,7 +219,7 @@ class TextInformationFrameConstructorTests extends ConstructorTests { } @suite(timeout(3000), slow(1000)) -class TextInformationFramePropertyTests { +class Id3v2_TextInformationFrame_PropertyTests { @test public getText() { // Arrange @@ -234,15 +235,12 @@ class TextInformationFramePropertyTests { } @test - public setText() { + public settext() { // Arrange const frame = getTestFrame(); - // Act - frame.text = ["bux", "fux"]; - - // Assert - assert.deepStrictEqual(frame.text, ["bux", "fux"]); + // Act / Assert + FramePropertyTests.propertyRoundTrip((v) => { frame.text = v; }, () => frame.text, ["bux", "fux"]); } @test @@ -250,11 +248,12 @@ class TextInformationFramePropertyTests { // Arrange const frame = getTestFrame(); - // Act - frame.textEncoding = StringType.UTF16BE; - - // Assert - assert.strictEqual(frame.textEncoding, StringType.UTF16BE); + // Act / Assert + FramePropertyTests.propertyRoundTrip( + (v) => { frame.textEncoding = v; }, + () => frame.textEncoding, + StringType.UTF16BE + ); } @test @@ -263,16 +262,17 @@ class TextInformationFramePropertyTests { const frame = getTestFrame(); const _ = frame.text; // Force a read - // Act - frame.textEncoding = StringType.UTF16BE; - - // Assert - assert.strictEqual(frame.textEncoding, StringType.UTF16BE); + // Act / Assert + FramePropertyTests.propertyRoundTrip( + (v) => { frame.textEncoding = v; }, + () => frame.textEncoding, + StringType.UTF16BE + ); } } @suite(timeout(3000), slow(1000)) -class TextInformationFrameMethodTests { +class Id3v2_TextInformationFrame_MethodTests { @test public clone_returnsCopy() { // Arrange diff --git a/test/id3v2/uniqueFileIdentifierFrameTests.ts b/test/id3v2/uniqueFileIdentifierFrameTests.ts index 335392e9..c91a67f4 100644 --- a/test/id3v2/uniqueFileIdentifierFrameTests.ts +++ b/test/id3v2/uniqueFileIdentifierFrameTests.ts @@ -2,7 +2,8 @@ import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; -import ConstructorTests from "./frameConstructorTests"; +import FrameConstructorTests from "./frameConstructorTests"; +import FramePropertyTests from "./framePropertyTests"; import FrameTypes from "../../src/id3v2/frameTypes"; import UniqueFileIdentifierFrame from "../../src/id3v2/frames/uniqueFileIdentifierFrame"; import {ByteVector, StringType} from "../../src/byteVector"; @@ -18,7 +19,7 @@ const testIdentifier = ByteVector.fromString("foobarbaz"); const testOwner = "http://github.com/benrr101/node-taglib-sharp"; @suite(timeout(3000), slow(1000)) -class UniqueFileIdentifierFrameConstructorTests extends ConstructorTests { +class Id3v2_UniqueFileIdentifierFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { return UniqueFileIdentifierFrame.fromOffsetRawData; } @@ -56,18 +57,13 @@ class UniqueFileIdentifierFrameConstructorTests extends ConstructorTests { const frame = UniqueFileIdentifierFrame.fromData(owner, identifier); // Assert - assert.isOk(frame); - assert.equal(frame.frameClassType, FrameClassType.UniqueFileIdentifierFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.UFID)); - - assert.isTrue(ByteVector.equal(frame.identifier, identifier)); - assert.strictEqual(frame.owner, owner); + this.assertFrame(frame, owner, identifier); } @test public fromOffsetRawData_tooFewFields_returnsEmptyFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); + const header = new Id3v2FrameHeader(FrameTypes.UFID, 4); header.frameSize = 9; const data = ByteVector.concatenate( header.render(4), @@ -79,18 +75,13 @@ class UniqueFileIdentifierFrameConstructorTests extends ConstructorTests { const frame = UniqueFileIdentifierFrame.fromOffsetRawData(data, 2, header); // Assert - assert.isOk(frame); - assert.strictEqual(frame.frameClassType, FrameClassType.UniqueFileIdentifierFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.WXXX)); - - assert.isUndefined(frame.identifier); - assert.isUndefined(frame.owner); + this.assertFrame(frame, undefined, undefined); } @test public fromOffsetRawData_tooManyFields_returnsEmptyFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); + const header = new Id3v2FrameHeader(FrameTypes.UFID, 4); header.frameSize = 29; const data = ByteVector.concatenate( header.render(4), @@ -107,18 +98,13 @@ class UniqueFileIdentifierFrameConstructorTests extends ConstructorTests { const frame = UniqueFileIdentifierFrame.fromOffsetRawData(data, 2, header); // Assert - assert.isOk(frame); - assert.strictEqual(frame.frameClassType, FrameClassType.UniqueFileIdentifierFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.WXXX)); - - assert.isUndefined(frame.identifier); - assert.isUndefined(frame.owner); + this.assertFrame(frame, undefined, undefined); } @test public fromOffsetRawData_validData_returnsFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); + const header = new Id3v2FrameHeader(FrameTypes.UFID, 4); header.frameSize = 54; const data = ByteVector.concatenate( header.render(4), @@ -132,18 +118,13 @@ class UniqueFileIdentifierFrameConstructorTests extends ConstructorTests { const frame = UniqueFileIdentifierFrame.fromOffsetRawData(data, 2, header); // Assert - assert.isOk(frame); - assert.strictEqual(frame.frameClassType, FrameClassType.UniqueFileIdentifierFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.WXXX)); - - assert.strictEqual(frame.owner, testOwner); - assert.isTrue(ByteVector.equal(frame.identifier, testIdentifier)); + this.assertFrame(frame, testOwner, testIdentifier); } @test public fromRawData_tooFewFields_returnsEmptyFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); + const header = new Id3v2FrameHeader(FrameTypes.UFID, 4); header.frameSize = 9; const data = ByteVector.concatenate( header.render(4), @@ -154,18 +135,13 @@ class UniqueFileIdentifierFrameConstructorTests extends ConstructorTests { const frame = UniqueFileIdentifierFrame.fromRawData(data, 4); // Assert - assert.isOk(frame); - assert.strictEqual(frame.frameClassType, FrameClassType.UniqueFileIdentifierFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.WXXX)); - - assert.isUndefined(frame.identifier); - assert.isUndefined(frame.owner); + this.assertFrame(frame, undefined, undefined); } @test public fromRawData_tooManyFields_returnsEmptyFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); + const header = new Id3v2FrameHeader(FrameTypes.UFID, 4); header.frameSize = 29; const data = ByteVector.concatenate( header.render(4), @@ -181,18 +157,13 @@ class UniqueFileIdentifierFrameConstructorTests extends ConstructorTests { const frame = UniqueFileIdentifierFrame.fromRawData(data, 4); // Assert - assert.isOk(frame); - assert.strictEqual(frame.frameClassType, FrameClassType.UniqueFileIdentifierFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.WXXX)); - - assert.isUndefined(frame.identifier); - assert.isUndefined(frame.owner); + this.assertFrame(frame, undefined, undefined); } @test public fromRawData_validData_returnsFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); + const header = new Id3v2FrameHeader(FrameTypes.UFID, 4); header.frameSize = 54; const data = ByteVector.concatenate( header.render(4), @@ -205,24 +176,33 @@ class UniqueFileIdentifierFrameConstructorTests extends ConstructorTests { const frame = UniqueFileIdentifierFrame.fromRawData(data, 4); // Assert + this.assertFrame(frame, testOwner, testIdentifier); + } + + private assertFrame(frame: UniqueFileIdentifierFrame, o: string, i: ByteVector) { assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.UniqueFileIdentifierFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.WXXX)); + assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.UFID)); + + assert.strictEqual(frame.owner, o); - assert.strictEqual(frame.owner, testOwner); - assert.isTrue(ByteVector.equal(frame.identifier, testIdentifier)); + if (i !== undefined) { + assert.isTrue(ByteVector.equal(frame.identifier, i)); + } else { + assert.isUndefined(frame.identifier); + } } } @suite(timeout(3000), slow(1000)) -class UniqueFileIdentifierFramePropertiesTests { +class Id3v2_UniqueFileIdentifierFrame_PropertyTests { @test public setIdentifier_tooLong_throws() { // Arrange const frame = UniqueFileIdentifierFrame.fromData("fuxqux", ByteVector.fromSize(1)); // Act/Assert - assert.throws(() => { frame.identifier = ByteVector.fromSize(65); }); + FramePropertyTests.propertyThrows((v) => { frame.identifier = v; }, ByteVector.fromSize(65)); } @test @@ -231,16 +211,13 @@ class UniqueFileIdentifierFramePropertiesTests { const frame = UniqueFileIdentifierFrame.fromData("fuxqux", ByteVector.fromSize(1)); const identifier = ByteVector.fromString("quxx"); - // Act - frame.identifier = identifier; - - // Assert - assert.isTrue(ByteVector.equal(frame.identifier, identifier)); + // Act / Assert + FramePropertyTests.propertyRoundTrip((v) => { frame.identifier = v; }, () => frame.identifier, identifier); } } @suite(timeout(3000), slow(1000)) -class UniqueFileIdentifierFrameMethodTests { +class Id3v2_UniqueFileIdentifierFrame_MethodTests { @test public find_falsyFrames_throws() { // Act/Assert @@ -310,7 +287,7 @@ class UniqueFileIdentifierFrameMethodTests { @test public render_returnsByteVector() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); + const header = new Id3v2FrameHeader(FrameTypes.UFID, 4); header.frameSize = 54; const data = ByteVector.concatenate( header.render(4), diff --git a/test/id3v2/unknownFrameTests.ts b/test/id3v2/unknownFrameTests.ts index 83dedaaf..318e756c 100644 --- a/test/id3v2/unknownFrameTests.ts +++ b/test/id3v2/unknownFrameTests.ts @@ -14,7 +14,7 @@ Chai.use(ChaiAsPromised); const assert = Chai.assert; @suite(timeout(3000), slow(1000)) -class UnknownFrameConstructorTests extends FrameConstructorTests { +class Id3v2_UnknownFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { return UnknownFrame.fromOffsetRawData; } @@ -36,13 +36,10 @@ class UnknownFrameConstructorTests extends FrameConstructorTests { const frameType = FrameTypes.WXXX; // Act - const result = UnknownFrame.fromData(frameType, undefined); + const frame = UnknownFrame.fromData(frameType, undefined); // Assert - assert.ok(result); - assert.strictEqual(result.frameClassType, FrameClassType.UnknownFrame); - assert.isTrue(ByteVector.equal(result.frameId, frameType)); - assert.isUndefined(result.data); + this.assertFrame(frame, FrameTypes.WXXX, undefined); } @test @@ -51,13 +48,10 @@ class UnknownFrameConstructorTests extends FrameConstructorTests { const frameType = FrameTypes.WXXX; // Act - const result = UnknownFrame.fromData(frameType, null); + const frame = UnknownFrame.fromData(frameType, null); // Assert - assert.ok(result); - assert.strictEqual(result.frameClassType, FrameClassType.UnknownFrame); - assert.isTrue(ByteVector.equal(result.frameId, frameType)); - assert.isUndefined(result.data); + this.assertFrame(frame, FrameTypes.WXXX, undefined); } @test @@ -67,13 +61,10 @@ class UnknownFrameConstructorTests extends FrameConstructorTests { const data = ByteVector.fromString("fux qux quxx"); // Act - const result = UnknownFrame.fromData(frameType, data); + const frame = UnknownFrame.fromData(frameType, data); // Assert - assert.ok(result); - assert.strictEqual(result.frameClassType, FrameClassType.UnknownFrame); - assert.isTrue(ByteVector.equal(result.frameId, frameType)); - assert.isTrue(ByteVector.equal(result.data, data)); + this.assertFrame(frame, FrameTypes.WXXX, data); } @test @@ -91,11 +82,7 @@ class UnknownFrameConstructorTests extends FrameConstructorTests { const frame = UnknownFrame.fromOffsetRawData(data, 2, header); // Assert - assert.ok(frame); - assert.strictEqual(frame.frameClassType, FrameClassType.UnknownFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.WXXX)); - - assert.isTrue(ByteVector.equal(frame.data, ByteVector.fromString("foo bar baz"))); + this.assertFrame(frame, FrameTypes.WXXX, ByteVector.fromString("foo bar baz")); } @test @@ -112,16 +99,24 @@ class UnknownFrameConstructorTests extends FrameConstructorTests { const frame = UnknownFrame.fromRawData(data, 4); // Assert + this.assertFrame(frame, FrameTypes.WXXX, ByteVector.fromString("foo bar baz")); + } + + private assertFrame(frame: UnknownFrame, ft: ByteVector, d: ByteVector) { assert.ok(frame); assert.strictEqual(frame.frameClassType, FrameClassType.UnknownFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.WXXX)); + assert.isTrue(ByteVector.equal(frame.frameId, ft)); - assert.isTrue(ByteVector.equal(frame.data, ByteVector.fromString("foo bar baz"))); + if (d !== undefined) { + assert.isTrue(ByteVector.equal(frame.data, d)); + } else { + assert.isUndefined(frame.data); + } } } @suite(timeout(3000), slow(1000)) -class UnknownFrameMethodTests { +class Id3v2_UnknownFrame_MethodTests { @test public clone_returnsCopy() { // Arrange diff --git a/test/id3v2/unsynchronizedLyricsFrameTests.ts b/test/id3v2/unsynchronizedLyricsFrameTests.ts index 358cfa03..c9397309 100644 --- a/test/id3v2/unsynchronizedLyricsFrameTests.ts +++ b/test/id3v2/unsynchronizedLyricsFrameTests.ts @@ -8,6 +8,7 @@ import UnsynchronizedLyricsFrame from "../../src/id3v2/frames/unsynchronizedLyri import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import FramePropertyTests from "./framePropertyTests"; // Setup chai Chai.use(ChaiAsPromised); @@ -33,7 +34,7 @@ const getTestUnsynchronizedLyricsFrame = (): UnsynchronizedLyricsFrame => { }; @suite(timeout(3000), slow(1000)) -class UnsynchronizedLyricsFrameConstructorsTests extends FrameConstructorTests { +class Id3v2_UnsynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { return UnsynchronizedLyricsFrame.fromOffsetRawData; } @@ -50,17 +51,10 @@ class UnsynchronizedLyricsFrameConstructorsTests extends FrameConstructorTests { const description = "foo"; // Act - const result = UnsynchronizedLyricsFrame.fromData(description, language, encoding); + const frame = UnsynchronizedLyricsFrame.fromData(description, language, encoding); // Assert - assert.isOk(result); - assert.equal(result.frameClassType, FrameClassType.UnsynchronizedLyricsFrame); - assert.isTrue(ByteVector.equal(result.frameId, FrameTypes.USLT)); - - assert.strictEqual(result.description, description); - assert.strictEqual(result.language, language); - assert.strictEqual(result.text, ""); - assert.strictEqual(result.textEncoding, encoding); + this.assertFrame(frame, description, language, "", encoding); } @test @@ -80,14 +74,7 @@ class UnsynchronizedLyricsFrameConstructorsTests extends FrameConstructorTests { const frame = UnsynchronizedLyricsFrame.fromOffsetRawData(data, 2, header); // Assert - assert.isOk(frame); - assert.equal(frame.frameClassType, FrameClassType.UnsynchronizedLyricsFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.USLT)); - - assert.strictEqual(frame.description, ""); - assert.strictEqual(frame.language, "eng"); - assert.strictEqual(frame.text, "foo"); - assert.strictEqual(frame.textEncoding, StringType.Latin1); + this.assertFrame(frame, "", "eng", "foo", StringType.Latin1); } @test @@ -109,14 +96,7 @@ class UnsynchronizedLyricsFrameConstructorsTests extends FrameConstructorTests { const frame = UnsynchronizedLyricsFrame.fromOffsetRawData(data, 2, header); // Assert - assert.isOk(frame); - assert.equal(frame.frameClassType, FrameClassType.UnsynchronizedLyricsFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.USLT)); - - assert.strictEqual(frame.description, "foo"); - assert.strictEqual(frame.language, "eng"); - assert.strictEqual(frame.text, "bar"); - assert.strictEqual(frame.textEncoding, StringType.Latin1); + this.assertFrame(frame, "foo", "eng", "bar", StringType.Latin1); } @test @@ -135,14 +115,7 @@ class UnsynchronizedLyricsFrameConstructorsTests extends FrameConstructorTests { const frame = UnsynchronizedLyricsFrame.fromRawData(data, 4); // Assert - assert.isOk(frame); - assert.equal(frame.frameClassType, FrameClassType.UnsynchronizedLyricsFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.USLT)); - - assert.strictEqual(frame.description, ""); - assert.strictEqual(frame.language, "eng"); - assert.strictEqual(frame.text, "foo"); - assert.strictEqual(frame.textEncoding, StringType.Latin1); + this.assertFrame(frame, "", "eng", "foo", StringType.Latin1); } @test @@ -163,166 +136,80 @@ class UnsynchronizedLyricsFrameConstructorsTests extends FrameConstructorTests { const frame = UnsynchronizedLyricsFrame.fromRawData(data, 4); // Assert + this.assertFrame(frame, "foo", "eng", "bar", StringType.Latin1); + } + + private assertFrame(frame: UnsynchronizedLyricsFrame, d: string, l: string, t: string, te: StringType) { assert.isOk(frame); assert.equal(frame.frameClassType, FrameClassType.UnsynchronizedLyricsFrame); assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.USLT)); - assert.strictEqual(frame.description, "foo"); - assert.strictEqual(frame.language, "eng"); - assert.strictEqual(frame.text, "bar"); - assert.strictEqual(frame.textEncoding, StringType.Latin1); + assert.strictEqual(frame.description, d); + assert.strictEqual(frame.language, l); + assert.strictEqual(frame.text, t); + assert.strictEqual(frame.textEncoding, te); } } @suite(timeout(3000), slow(1000)) -class UnsynchronizedLyricsFramePropertiesTests { - @test - public setDescription_undefined() { - // Arrange - const frame = getTestUnsynchronizedLyricsFrame(); - - // Act - frame.description = undefined; - - // Assert - assert.strictEqual(frame.description, ""); - } - +class Id3v2_UnsynchronizedLyricsFrame_PropertyTests { @test - public setDescription_null() { + public description() { // Arrange const frame = getTestUnsynchronizedLyricsFrame(); + const set = (v: string) => { frame.description = v; }; + const get = () => frame.description; - // Act - frame.description = null; - - // Assert - assert.strictEqual(frame.description, ""); - } - - @test - public setDescription_value() { - // Arrange - const frame = getTestUnsynchronizedLyricsFrame(); - - // Act - frame.description = "fux"; - - // Assert - assert.strictEqual(frame.description, "fux"); + // Act / Assert + FramePropertyTests.propertyRoundTrip(set, get, "fux"); + FramePropertyTests.propertyNormalized(set, get, undefined, ""); + FramePropertyTests.propertyNormalized(set, get, null, ""); } @test - public setLanguage_undefined_returnsXXX() { + public language() { // Arrange const frame = getTestUnsynchronizedLyricsFrame(); + const set = (v: string) => { frame.language = v; }; + const get = () => frame.language; - // Act - frame.language = undefined; - - // Assert - assert.strictEqual(frame.language, "XXX"); + // Act / assert + FramePropertyTests.propertyRoundTrip(set, get, "ABC"); + FramePropertyTests.propertyNormalized(set, get, undefined, "XXX"); + FramePropertyTests.propertyNormalized(set, get, null, "XXX"); + FramePropertyTests.propertyNormalized(set, get, "AB", "XXX"); + FramePropertyTests.propertyNormalized(set, get, "ABCD", "XXX"); } @test - public setLanguage_null_returnsXXX() { + public text() { // Arrange const frame = getTestUnsynchronizedLyricsFrame(); + const set = (v: string) => { frame.text = v; }; + const get = () => frame.text; - // Act - frame.language = null; - - // Assert - assert.strictEqual(frame.language, "XXX"); + // Act / Assert + FramePropertyTests.propertyRoundTrip(set, get, "fux qux quxx"); + FramePropertyTests.propertyNormalized(set, get, undefined, ""); + FramePropertyTests.propertyNormalized(set, get, null, ""); } @test - public setLanguage_tooShort_returnsXXX() { + public textEncoding() { // Arrange const frame = getTestUnsynchronizedLyricsFrame(); - // Act - frame.language = "AB"; - - // Assert - assert.strictEqual(frame.language, "XXX"); - } - - @test - public setLanguage_tooLong_returnsXXX() { - // Arrange - const frame = getTestUnsynchronizedLyricsFrame(); - - // Act - frame.language = "ABCD"; - - // Assert - assert.strictEqual(frame.language, "XXX"); - } - - @test - public setLanguage_justRight_returnsValue() { - // Arrange - const frame = getTestUnsynchronizedLyricsFrame(); - - // Act - frame.language = "ABC"; - - // Assert - assert.strictEqual(frame.language, "ABC"); - } - - @test - public setText_undefined() { - // Arrange - const frame = getTestUnsynchronizedLyricsFrame(); - - // Act - frame.text = undefined; - - // Assert - assert.strictEqual(frame.text, ""); - } - - @test - public setText_null() { - // Arrange - const frame = getTestUnsynchronizedLyricsFrame(); - - // Act - frame.text = null; - - // Assert - assert.strictEqual(frame.text, ""); - } - - @test - public setText_values() { - // Arrange - const frame = getTestUnsynchronizedLyricsFrame(); - - // Act - frame.text = "fux qux quxx"; - - // Assert - assert.strictEqual(frame.text, "fux qux quxx"); - } - - @test - public setTextEncoding() { - // Arrange - const frame = getTestUnsynchronizedLyricsFrame(); - - // Act - frame.textEncoding = StringType.UTF16; - - // Assert - assert.strictEqual(StringType.UTF16, frame.textEncoding); + // Act / Assert + FramePropertyTests.propertyRoundTrip( + (v) => { frame.textEncoding = v; }, + () => frame.textEncoding, + StringType.UTF16BE + ); } } @suite(timeout(3000), slow(1000)) -class UnsynchronizedLyricsFrameMethodTests { +class Id3v2_UnsynchronizedLyricsFrame_MethodTests { @test public find_falsyFrames_throws() { // Act/Assert diff --git a/test/id3v2/urlLinkFrameTests.ts b/test/id3v2/urlLinkFrameTests.ts index bbf96bf5..51e946f3 100644 --- a/test/id3v2/urlLinkFrameTests.ts +++ b/test/id3v2/urlLinkFrameTests.ts @@ -7,15 +7,16 @@ import FrameTypes from "../../src/id3v2/frameTypes"; import TestConstants from "../testConstants"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; -import {Id3v2FrameFlags, Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; import {UrlLinkFrame} from "../../src/id3v2/frames/urlLinkFrame"; +import FramePropertyTests from "./framePropertyTests"; // Setup chai Chai.use(ChaiAsPromised); const assert = Chai.assert; const getTestFrameData = (): ByteVector => { - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); + const header = new Id3v2FrameHeader(FrameTypes.WCOM, 4); header.frameSize = 7; return ByteVector.concatenate( @@ -32,7 +33,7 @@ const getTestUrlLinkFrame = (): UrlLinkFrame => { }; @suite(timeout(3000), slow(1000)) -class UrlLinkFrameConstructorTests extends FrameConstructorTests { +class Id3v2_UrlLinkFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { return UrlLinkFrame.fromOffsetRawData; } @@ -54,17 +55,7 @@ class UrlLinkFrameConstructorTests extends FrameConstructorTests { const output = UrlLinkFrame.fromIdentity(FrameTypes.WCOM); // Assert - assert.ok(output); - assert.equal(output.frameClassType, FrameClassType.UrlLinkFrame); - - assert.equal(output.encryptionId, -1); - assert.equal(output.flags, Id3v2FrameFlags.None); - assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.WCOM)); - assert.equal(output.groupId, -1); - assert.equal(output.size, 0); - - assert.deepEqual(output.text, []); - assert.equal(output.textEncoding, StringType.Latin1); + this.assertFrame(output, FrameTypes.WCOM, [], StringType.Latin1); } @test @@ -72,47 +63,28 @@ class UrlLinkFrameConstructorTests extends FrameConstructorTests { // Arrange const header = new Id3v2FrameHeader(FrameTypes.WCOM, 4); header.frameSize = TestConstants.syncedUint; - - // Offset bytes - // Header - // Data - // Some null bytes to trigger null cleanup const data = ByteVector.concatenate( - 0x00, 0x00, header.render(4), + 0x00, 0x00, ByteVector.fromString("foobar", StringType.Latin1, undefined, true), - 0x00, 0x00 + 0x00, 0x00 // Some null bytes to trigger null cleanup ); // Act const output = UrlLinkFrame.fromOffsetRawData(data, 2, header); // Assert - assert.ok(output); - assert.equal(output.frameClassType, FrameClassType.UrlLinkFrame); - - assert.equal(output.encryptionId, -1); - assert.equal(output.flags, Id3v2FrameFlags.None); - assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.WCOM)); - assert.equal(output.groupId, -1); - assert.equal(output.size, TestConstants.syncedUint); - - assert.deepEqual(output.text, ["foobar"]); - assert.equal(output.textEncoding, StringType.Latin1); + this.assertFrame(output, FrameTypes.WCOM, ["foobar"], StringType.Latin1); } @test public fromOffsetRawData_userFrame() { // Arrange const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); - header.frameSize = TestConstants.syncedUint; - - // Offset bytes - // Header - // Data (2x strings w/divider) + header.frameSize = 7; const data = ByteVector.concatenate( - 0x00, 0x00, header.render(4), + 0x00, 0x00, ByteVector.fromString("foo", StringType.Latin1, undefined, true), ByteVector.getTextDelimiter(StringType.Latin1), ByteVector.fromString("bar", StringType.Latin1, undefined, true) @@ -122,26 +94,16 @@ class UrlLinkFrameConstructorTests extends FrameConstructorTests { const output = UrlLinkFrame.fromOffsetRawData(data, 2, header); // Assert - assert.ok(output); - assert.equal(output.frameClassType, FrameClassType.UrlLinkFrame); - - assert.equal(output.encryptionId, -1); - assert.equal(output.flags, Id3v2FrameFlags.None); - assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.WXXX)); - assert.equal(output.groupId, -1); - assert.equal(output.size, TestConstants.syncedUint); - - assert.deepEqual(output.text, ["foo", "bar"]); - assert.equal(output.textEncoding, StringType.Latin1); + this.assertFrame(output, FrameTypes.WXXX, ["foo", "bar"], StringType.Latin1); } @test public fromRawData_notUserFrame() { // Arrange + const header = new Id3v2FrameHeader(FrameTypes.WCOM, 4); + header.frameSize = 8; const data = ByteVector.concatenate( - FrameTypes.WCOM, // - Frame ID - TestConstants.syncedUintBytes, // - Frame size - 0x00, 0x00, // - Frame Flags + header.render(4), ByteVector.fromString("foobar", StringType.Latin1, undefined, true), // - Data 0x00, 0x00 // - null bytes to test end byte // cleanup @@ -151,51 +113,40 @@ class UrlLinkFrameConstructorTests extends FrameConstructorTests { const output = UrlLinkFrame.fromRawData(data, 4); // Assert - assert.ok(output); - assert.equal(output.frameClassType, FrameClassType.UrlLinkFrame); - - assert.equal(output.encryptionId, -1); - assert.equal(output.flags, Id3v2FrameFlags.None); - assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.WCOM)); - assert.equal(output.groupId, -1); - assert.equal(output.size, TestConstants.syncedUint); - - assert.deepEqual(output.text, ["foobar"]); - assert.equal(output.textEncoding, StringType.Latin1); + this.assertFrame(output, FrameTypes.WCOM, ["foobar"], StringType.Latin1); } @test public fromRawData_userFrame() { // Arrange + const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); + header.frameSize = 7; const data = ByteVector.concatenate( - FrameTypes.WXXX, // - Frame ID - TestConstants.syncedUintBytes, // - Frame size - 0x00, 0x00, // - Frame flags - ByteVector.fromString("foo", StringType.Latin1, undefined, true), // - String 1 - ByteVector.getTextDelimiter(StringType.Latin1), // - String separator - ByteVector.fromString("bar", StringType.Latin1, undefined, true) // - String 2 + header.render(4), + ByteVector.fromString("foo", StringType.Latin1, undefined, true), + ByteVector.getTextDelimiter(StringType.Latin1), + ByteVector.fromString("bar", StringType.Latin1, undefined, true) ); // Act const output = UrlLinkFrame.fromRawData(data, 4); // Assert - assert.ok(output); - assert.equal(output.frameClassType, FrameClassType.UrlLinkFrame); + this.assertFrame(output, FrameTypes.WXXX, ["foo", "bar"], StringType.Latin1); + } - assert.equal(output.encryptionId, -1); - assert.equal(output.flags, Id3v2FrameFlags.None); - assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.WXXX)); - assert.equal(output.groupId, -1); - assert.equal(output.size, TestConstants.syncedUint); + private assertFrame(frame: UrlLinkFrame, ft: ByteVector, t: string[], te: StringType) { + assert.ok(frame); + assert.equal(frame.frameClassType, FrameClassType.UrlLinkFrame); + assert.isTrue(ByteVector.equal(frame.frameId, ft)); - assert.deepEqual(output.text, ["foo", "bar"]); - assert.equal(output.textEncoding, StringType.Latin1); + assert.deepEqual(frame.text, t); + assert.equal(frame.textEncoding, te); } } @suite(timeout(3000), slow(1000)) -class UrlLinkFramePropertyTests { +class Id3v2_UrlLinkFrame_PropertyTests { @test public getText_outputIsClone() { // Arrange @@ -210,27 +161,15 @@ class UrlLinkFramePropertyTests { } @test - public setText_undefined() { + public setText_falsyValues() { // Arrange const frame = getTestUrlLinkFrame(); + const set = (v: string[]) => { frame.text = v; }; + const get = () => frame.text; - // Act - frame.text = undefined; - - // Assert - assert.deepEqual(frame.text, []); - } - - @test - public setText_null() { - // Arrange - const frame = getTestUrlLinkFrame(); - - // Act - frame.text = null; - - // Assert - assert.deepEqual(frame.text, []); + // Act / Assert + FramePropertyTests.propertyNormalized(set, get, undefined, []); + FramePropertyTests.propertyNormalized(set, get, null, []); } @test @@ -267,7 +206,7 @@ class UrlLinkFramePropertyTests { } @suite(timeout(3000), slow(1000)) -class UrlLinkMethodTests { +class Id3v2_UrlLinkFrame_MethodTests { @test public findUrlLinkFrame_falsyFrames_throws(): void { // Act/Assert @@ -310,10 +249,10 @@ class UrlLinkMethodTests { @test public findUrlLinkFrame_noMatch_returnsUndefined() { // Arrange - const frames = [getTestUrlLinkFrame(), getTestUrlLinkFrame()]; // Type is WXXX + const frames = [getTestUrlLinkFrame(), getTestUrlLinkFrame()]; // Type is WCOM // Act - const result = UrlLinkFrame.findUrlLinkFrame(frames, FrameTypes.WCOM); + const result = UrlLinkFrame.findUrlLinkFrame(frames, FrameTypes.WXXX); // Assert assert.isUndefined(result); @@ -327,7 +266,7 @@ class UrlLinkMethodTests { const frames = [frame1, frame2]; // Act - const result = UrlLinkFrame.findUrlLinkFrame(frames, FrameTypes.WXXX); + const result = UrlLinkFrame.findUrlLinkFrame(frames, FrameTypes.WCOM); // Assert assert.equal(result, frame1); diff --git a/test/id3v2/userTextInformationFrameTests.ts b/test/id3v2/userTextInformationFrameTests.ts index 1e748504..958abf1b 100644 --- a/test/id3v2/userTextInformationFrameTests.ts +++ b/test/id3v2/userTextInformationFrameTests.ts @@ -2,7 +2,7 @@ import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; -import ConstructorTests from "./frameConstructorTests"; +import FrameConstructorTests from "./frameConstructorTests"; import FrameTypes from "../../src/id3v2/frameTypes"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import {UserTextInformationFrame} from "../../src/id3v2/frames/textInformationFrame"; @@ -29,7 +29,7 @@ function getTestFrame(): UserTextInformationFrame { } @suite(timeout(3000), slow(1000)) -class UserInformationFrameConstructorTests extends ConstructorTests { +class Id3v2_UserInformationFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { return UserTextInformationFrame.fromOffsetRawData; } @@ -44,7 +44,7 @@ class UserInformationFrameConstructorTests extends ConstructorTests { const frame = UserTextInformationFrame.fromDescription("foo"); // Assert - UserInformationFrameConstructorTests.assertFrame(frame, "foo", [], Id3v2TagSettings.defaultEncoding); + this.assertFrame(frame, "foo", [], Id3v2TagSettings.defaultEncoding); } @test @@ -53,7 +53,7 @@ class UserInformationFrameConstructorTests extends ConstructorTests { const frame = UserTextInformationFrame.fromDescription("foo", StringType.UTF16); // Assert - UserInformationFrameConstructorTests.assertFrame(frame, "foo", [], StringType.UTF16); + this.assertFrame(frame, "foo", [], StringType.UTF16); } @test @@ -74,7 +74,7 @@ class UserInformationFrameConstructorTests extends ConstructorTests { const frame = UserTextInformationFrame.fromOffsetRawData(data, 2, header); // Assert - UserInformationFrameConstructorTests.assertFrame(frame, "foo", ["bar"], StringType.Latin1); + this.assertFrame(frame, "foo", ["bar"], StringType.Latin1); } @test @@ -94,10 +94,10 @@ class UserInformationFrameConstructorTests extends ConstructorTests { const frame = UserTextInformationFrame.fromRawData(data, 4); // Assert - UserInformationFrameConstructorTests.assertFrame(frame, "foo", ["bar"], StringType.Latin1); + this.assertFrame(frame, "foo", ["bar"], StringType.Latin1); } - private static assertFrame( + private assertFrame( frame: UserTextInformationFrame, description: string, text: string[], @@ -115,7 +115,7 @@ class UserInformationFrameConstructorTests extends ConstructorTests { } @suite(timeout(3000), slow(1000)) -class UserInformationFramePropertiesTests { +class Id3v2_UserInformationFrame_PropertyTests { @test public setDescription() { // Arrange @@ -158,7 +158,7 @@ class UserInformationFramePropertiesTests { } @suite(timeout(3000), slow(1000)) -class UserTextInformationFrameMethodTests { +class Id3v2_UserTextInformationFrame_MethodTests { @test public findUserTextInformationFrame_falsyFrames() { // Act/Assert diff --git a/test/id3v2/userUrlLinkFrameTests.ts b/test/id3v2/userUrlLinkFrameTests.ts index 5431f2af..b3da76cd 100644 --- a/test/id3v2/userUrlLinkFrameTests.ts +++ b/test/id3v2/userUrlLinkFrameTests.ts @@ -32,7 +32,7 @@ const getTestUserUrlLinkFrame = (): UserUrlLinkFrame => { }; @suite(timeout(3000), slow(1000)) -class UserUrlLinkFrameConstructorTests extends FrameConstructorTests { +class Id3v2_UserUrlLinkFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { return UserUrlLinkFrame.fromOffsetRawData; } @@ -108,15 +108,11 @@ class UserUrlLinkFrameConstructorTests extends FrameConstructorTests { } @suite(timeout(3000), slow(1000)) -class UserUrlLinkFramePropertyTests { +class Id3v2_UserUrlLinkFrame_PropertyTests { @test public getDescription_emptyText_returnsUndefined() { // Arrange - const data = ByteVector.concatenate( - FrameTypes.WXXX, // - Frame ID - 0x00, 0x00, 0x00, 0x00, // - Frame size - 0x00, 0x00, // - Frame flags - ); + const data = new Id3v2FrameHeader(FrameTypes.WXXX, 4).render(4); const frame = UserUrlLinkFrame.fromRawData(data, 4); // Act @@ -259,7 +255,7 @@ class UserUrlLinkFramePropertyTests { } @suite(timeout(3000), slow(1000)) -class UserUrlLinkMethodTests { +class Id3v2_UserUrlLink_MethodTests { @test public findUserUrlLinkFrame_falsyFrames_throws(): void { // Act/Assert From 3c6028f7600df7db533c07788f4bc40c5c80137a Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 7 Jan 2020 19:45:08 -0500 Subject: [PATCH 26/71] Fixing stuff I broke at some point --- src/id3v2/frames/frameFactory.ts | 4 ++-- test/id3v2/PopularimeterFrameTests.ts | 22 +++++++++--------- test/id3v2/commentsFrameTests.ts | 26 +++++++++++----------- test/id3v2/eventTimeCodeFrameTests.ts | 16 ++++++------- test/id3v2/privateFrameTests.ts | 4 ++-- test/id3v2/synchronizedLyricsFrameTests.ts | 24 ++++++++++---------- test/id3v2/termsOfUseFrameTests.ts | 22 ++++++++++-------- 7 files changed, 61 insertions(+), 57 deletions(-) diff --git a/src/id3v2/frames/frameFactory.ts b/src/id3v2/frames/frameFactory.ts index 343e31fc..6a40874d 100644 --- a/src/id3v2/frames/frameFactory.ts +++ b/src/id3v2/frames/frameFactory.ts @@ -104,7 +104,7 @@ export default { } // TODO: Support compression - if ((header.flags & Id3v2FrameFlags.Compression) != 0) { + if ((header.flags & Id3v2FrameFlags.Compression) !== 0) { throw new NotImplementedError("Compression is not supported"); } @@ -164,7 +164,7 @@ export default { func = UniqueFileIdentifierFrame.fromOffsetRawData; } else if (ByteVector.equal(header.frameId, FrameTypes.MCDI)) { // Music CD identifier (frames 4.5) - func = MusicCdIdentifierFrame.fromOffsetRawHeader; + func = MusicCdIdentifierFrame.fromOffsetRawData; } else if (ByteVector.equal(header.frameId, FrameTypes.USLT)) { // Unsynchronized lyrics (frames 4.8) func = UnsynchronizedLyricsFrame.fromOffsetRawData; diff --git a/test/id3v2/PopularimeterFrameTests.ts b/test/id3v2/PopularimeterFrameTests.ts index ef9f34cd..570643d6 100644 --- a/test/id3v2/PopularimeterFrameTests.ts +++ b/test/id3v2/PopularimeterFrameTests.ts @@ -187,10 +187,10 @@ class PopularimeterFramePropertyTests extends PropertyTests { const get = () => frame.playCount; // Act - this.propertyRoundTrip(set, get, BigInt(1234)); - this.propertyRoundTrip(set, get, undefined); - this.propertyThrows(set, null); - this.propertyThrows(set, BigInt(-1)); + PropertyTests.propertyRoundTrip(set, get, BigInt(1234)); + PropertyTests.propertyRoundTrip(set, get, undefined); + PropertyTests.propertyThrows(set, null); + PropertyTests.propertyThrows(set, BigInt(-1)); } @test @@ -201,10 +201,10 @@ class PopularimeterFramePropertyTests extends PropertyTests { const get = () => frame.rating; // Act - this.propertyRoundTrip(set, get, 5); - this.propertyThrows(set, -1); - this.propertyThrows(set, 1.23); - this.propertyThrows(set, 0x100); + PropertyTests.propertyRoundTrip(set, get, 5); + PropertyTests.propertyThrows(set, -1); + PropertyTests.propertyThrows(set, 1.23); + PropertyTests.propertyThrows(set, 0x100); } @test @@ -215,9 +215,9 @@ class PopularimeterFramePropertyTests extends PropertyTests { const get = () => frame.user; // Act - this.propertyRoundTrip(set, get, "bux"); - this.propertyNormalized(set, get, undefined, ""); - this.propertyNormalized(set, get, null, ""); + PropertyTests.propertyRoundTrip(set, get, "bux"); + PropertyTests.propertyNormalized(set, get, undefined, ""); + PropertyTests.propertyNormalized(set, get, null, ""); } } diff --git a/test/id3v2/commentsFrameTests.ts b/test/id3v2/commentsFrameTests.ts index 90c4b604..032bd682 100644 --- a/test/id3v2/commentsFrameTests.ts +++ b/test/id3v2/commentsFrameTests.ts @@ -295,16 +295,16 @@ class CommentsFrameConstructorTests extends FrameConstructorTests { } @suite(timeout(3000), slow(1000)) -class CommentsFramePropertiesTests extends FramePropertiesTests { +class CommentsFramePropertiesTests { @test public description() { const frame = getTestFrame(); const set = (v: string) => { frame.description = v; }; const get = () => frame.description; - this.propertyRoundTrip(set, get, "fux"); - this.propertyNormalized(set, get, undefined, ""); - this.propertyNormalized(set, get, null, ""); + FramePropertiesTests.propertyRoundTrip(set, get, "fux"); + FramePropertiesTests.propertyNormalized(set, get, undefined, ""); + FramePropertiesTests.propertyNormalized(set, get, null, ""); } @test @@ -313,11 +313,11 @@ class CommentsFramePropertiesTests extends FramePropertiesTests { const set = (v: string) => { frame.language = v; }; const get = () => frame.language; - this.propertyRoundTrip(set, get, "jpn"); - this.propertyNormalized(set, get, undefined, "XXX"); - this.propertyNormalized(set, get, null, "XXX"); - this.propertyNormalized(set, get, "ab", "XXX"); - this.propertyNormalized(set, get, "abcd", "abc"); + FramePropertiesTests.propertyRoundTrip(set, get, "jpn"); + FramePropertiesTests.propertyNormalized(set, get, undefined, "XXX"); + FramePropertiesTests.propertyNormalized(set, get, null, "XXX"); + FramePropertiesTests.propertyNormalized(set, get, "ab", "XXX"); + FramePropertiesTests.propertyNormalized(set, get, "abcd", "abc"); } @test @@ -326,16 +326,16 @@ class CommentsFramePropertiesTests extends FramePropertiesTests { const set = (v: string) => { frame.text = v; }; const get = () => frame.text; - this.propertyRoundTrip(set, get, "fux"); - this.propertyNormalized(set, get, undefined, ""); - this.propertyNormalized(set, get, null, ""); + FramePropertiesTests.propertyRoundTrip(set, get, "fux"); + FramePropertiesTests.propertyNormalized(set, get, undefined, ""); + FramePropertiesTests.propertyNormalized(set, get, null, ""); } @test public textEncoding() { const frame = getTestFrame(); - this.propertyRoundTrip((v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16); + FramePropertiesTests.propertyRoundTrip((v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16); } } diff --git a/test/id3v2/eventTimeCodeFrameTests.ts b/test/id3v2/eventTimeCodeFrameTests.ts index 0623b260..7bdba3d6 100644 --- a/test/id3v2/eventTimeCodeFrameTests.ts +++ b/test/id3v2/eventTimeCodeFrameTests.ts @@ -16,7 +16,7 @@ Chai.use(ChaiAsPromised); const assert = Chai.assert; @suite(timeout(3000), slow(1000)) -class EventTimeCodeTests extends FramePropertiesTests { +class EventTimeCodeTests { @test public constructor_invalidTime() { // Act/Assert @@ -60,7 +60,7 @@ class EventTimeCodeTests extends FramePropertiesTests { const output = EventTimeCode.fromEmpty(); // Act/Assert - this.propertyRoundTrip((v) => { output.time = v; }, () => output.time, 123); + FramePropertiesTests.propertyRoundTrip((v) => { output.time = v; }, () => output.time, 123); } @test @@ -69,7 +69,7 @@ class EventTimeCodeTests extends FramePropertiesTests { const output = EventTimeCode.fromEmpty(); // Act/Assert - this.propertyRoundTrip((v) => { output.eventType = v; }, () => output.eventType, EventType.IntroEnd); + FramePropertiesTests.propertyRoundTrip((v) => { output.eventType = v; }, () => output.eventType, EventType.IntroEnd); } @test @@ -216,10 +216,10 @@ class EventTimeCodeFramePropertyTests extends FramePropertiesTests { const get = () => frame.events; // Act / Assert - this.propertyRoundTrip(set, get, [new EventTimeCode(EventType.Profanity, 123)]); - this.propertyRoundTrip(set, get, []); - this.propertyNormalized(set, get, undefined, []); - this.propertyNormalized(set, get, null, []); + FramePropertiesTests.propertyRoundTrip(set, get, [new EventTimeCode(EventType.Profanity, 123)]); + FramePropertiesTests.propertyRoundTrip(set, get, []); + FramePropertiesTests.propertyNormalized(set, get, undefined, []); + FramePropertiesTests.propertyNormalized(set, get, null, []); } @test @@ -228,7 +228,7 @@ class EventTimeCodeFramePropertyTests extends FramePropertiesTests { const frame = EventTimeCodeFrame.fromTimestampFormat(TimestampFormat.AbsoluteMilliseconds); // Act / Assert - this.propertyRoundTrip( + FramePropertiesTests.propertyRoundTrip( (v) => { frame.timestampFormat = v; }, () => frame.timestampFormat, TimestampFormat.AbsoluteMpegFrames); diff --git a/test/id3v2/privateFrameTests.ts b/test/id3v2/privateFrameTests.ts index 6d4eef8a..4cfd3abd 100644 --- a/test/id3v2/privateFrameTests.ts +++ b/test/id3v2/privateFrameTests.ts @@ -113,14 +113,14 @@ class PrivateFrameConstructorTests extends ConstructorTests { } @suite(timeout(3000), slow(1000)) -class PrivateFramePropertyTests extends PropertyTests { +class PrivateFramePropertyTests { @test public privateData() { // Arrange const frame = PrivateFrame.fromOwner("fux"); // Act / Assert - this.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.privateData = v; }, () => frame.privateData, ByteVector.fromString("bux") diff --git a/test/id3v2/synchronizedLyricsFrameTests.ts b/test/id3v2/synchronizedLyricsFrameTests.ts index 6543b3ba..d202fd8d 100644 --- a/test/id3v2/synchronizedLyricsFrameTests.ts +++ b/test/id3v2/synchronizedLyricsFrameTests.ts @@ -339,7 +339,7 @@ class SynchronizedLyricsFrameConstructorTests extends ConstructorTests { } @suite(timeout(3000), slow(1000)) -class SynchronizedLyricsFramePropertyTests extends PropertiesTests { +class SynchronizedLyricsFramePropertyTests { @test public description() { // Arrange @@ -348,8 +348,8 @@ class SynchronizedLyricsFramePropertyTests extends PropertiesTests { const get = () => frame.description; // Act / Assert - this.propertyRoundTrip(set, get, "fux" ); - this.propertyRoundTrip(set, get, undefined); + PropertiesTests.propertyRoundTrip(set, get, "fux" ); + PropertiesTests.propertyRoundTrip(set, get, undefined); } @test @@ -358,7 +358,7 @@ class SynchronizedLyricsFramePropertyTests extends PropertiesTests { const frame = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord); // Act / Assert - this.propertyRoundTrip((v) => { frame.format = v; }, () => frame.format, TimestampFormat.AbsoluteMilliseconds); + PropertiesTests.propertyRoundTrip((v) => { frame.format = v; }, () => frame.format, TimestampFormat.AbsoluteMilliseconds); } @test @@ -369,9 +369,9 @@ class SynchronizedLyricsFramePropertyTests extends PropertiesTests { const get = () => frame.language; // Act / Assert - this.propertyRoundTrip(set, get, "fux"); - this.propertyRoundTrip(set, get, "shoe"); - this.propertyRoundTrip(set, get, "ab"); + PropertiesTests.propertyRoundTrip(set, get, "fux"); + PropertiesTests.propertyRoundTrip(set, get, "shoe"); + PropertiesTests.propertyRoundTrip(set, get, "ab"); } @test @@ -383,9 +383,9 @@ class SynchronizedLyricsFramePropertyTests extends PropertiesTests { const value = [new SynchronizedText(123, "foo")]; // Act / Assert - this.propertyRoundTrip(set, get, value); - this.propertyNormalized(set, get, undefined, []); - this.propertyNormalized(set, get, null, []); + PropertiesTests.propertyRoundTrip(set, get, value); + PropertiesTests.propertyNormalized(set, get, undefined, []); + PropertiesTests.propertyNormalized(set, get, null, []); } @test @@ -394,7 +394,7 @@ class SynchronizedLyricsFramePropertyTests extends PropertiesTests { const frame = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord); // Act / Assert - this.propertyRoundTrip((v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16BE); + PropertiesTests.propertyRoundTrip((v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16BE); } @test @@ -403,7 +403,7 @@ class SynchronizedLyricsFramePropertyTests extends PropertiesTests { const frame = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord); // Act / Assert - this.propertyRoundTrip((v) => { frame.textType = v; }, () => frame.textType, SynchronizedTextType.Trivia); + PropertiesTests.propertyRoundTrip((v) => { frame.textType = v; }, () => frame.textType, SynchronizedTextType.Trivia); } } diff --git a/test/id3v2/termsOfUseFrameTests.ts b/test/id3v2/termsOfUseFrameTests.ts index 1e1a659e..631e84e8 100644 --- a/test/id3v2/termsOfUseFrameTests.ts +++ b/test/id3v2/termsOfUseFrameTests.ts @@ -124,7 +124,7 @@ class TermsOfUseFrameConstructorsTests extends FrameConstructorTests { } @suite(timeout(3000), slow(1000)) -class TermsOfUseFramePropertyTests extends FramePropertiesTests { +class TermsOfUseFramePropertyTests { @test public language() { // Arrange @@ -133,24 +133,28 @@ class TermsOfUseFramePropertyTests extends FramePropertiesTests { // Act/Assert const frame = TermsOfUseFrame.fromFields("eng"); - this.propertyNormalized(set, get, "fu", "XXX"); - this.propertyNormalized(set, get, "fuxx", "fux"); - this.propertyNormalized(set, get, undefined, "XXX"); - this.propertyNormalized(set, get, null, "XXX"); - this.propertyRoundTrip(set, get, "fux"); + FramePropertiesTests.propertyNormalized(set, get, "fu", "XXX"); + FramePropertiesTests.propertyNormalized(set, get, "fuxx", "fux"); + FramePropertiesTests.propertyNormalized(set, get, undefined, "XXX"); + FramePropertiesTests.propertyNormalized(set, get, null, "XXX"); + FramePropertiesTests.propertyRoundTrip(set, get, "fux"); } @test public text() { const frame = TermsOfUseFrame.fromFields("eng"); - this.propertyRoundTrip((v) => { frame.text = v; }, () => frame.text, "fux"); - this.propertyRoundTrip((v) => { frame.text = v; }, () => frame.text, undefined); + FramePropertiesTests.propertyRoundTrip((v) => { frame.text = v; }, () => frame.text, "fux"); + FramePropertiesTests.propertyRoundTrip((v) => { frame.text = v; }, () => frame.text, undefined); } @test public textEncoding() { const frame = TermsOfUseFrame.fromFields("eng", StringType.Latin1); - this.propertyRoundTrip((v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16); + FramePropertiesTests.propertyRoundTrip( + (v) => { frame.textEncoding = v; }, + () => frame.textEncoding, + StringType.UTF16 + ); } } From bd69680dc631cbede6f40b12d70ea577814f6692 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 18 Jan 2020 15:10:35 -0500 Subject: [PATCH 27/71] Relative volume frame tests --- src/byteVector.ts | 6 +- src/combinedTag.ts | 2 +- src/errors.ts | 2 +- src/file.ts | 2 +- src/id3v1/id3v1Tag.ts | 4 +- src/id3v2/frames/frame.ts | 4 +- src/id3v2/frames/relativeVolumeFrame.ts | 221 ++++---- src/id3v2/frames/textInformationFrame.ts | 4 +- src/id3v2/header.ts | 4 +- src/id3v2/id3v2Tag.ts | 2 +- src/id3v2/id3v2TagSettings.ts | 2 +- src/utils.ts | 10 +- ...ameTests.ts => popularimeterFrameTests.ts} | 0 test/id3v2/relativeVolumeFrameTests.ts | 506 ++++++++++++++++++ 14 files changed, 654 insertions(+), 115 deletions(-) rename test/id3v2/{PopularimeterFrameTests.ts => popularimeterFrameTests.ts} (100%) create mode 100644 test/id3v2/relativeVolumeFrameTests.ts diff --git a/src/byteVector.ts b/src/byteVector.ts index 520bfe95..d9b1288a 100644 --- a/src/byteVector.ts +++ b/src/byteVector.ts @@ -25,7 +25,7 @@ class IConvEncoding { } /** - * @summary Specifies the text encoding used when converting between a {@link string} and a + * @summary Specifies the text encoding used when converting betweenInclusive a {@link string} and a * {@link ByteVector}. * @remarks This enumeration is used by {@link ByteVector.FromString(string,StringType)} and * {@link ByteVector.ToString(StringType)} @@ -193,7 +193,7 @@ export class ByteVector { // @TODO Remove usages of .addX when this can be substituted public static concatenate(... vectors: Array): ByteVector { // Get the length of the vector we need to create - var totalLength = 0; + let totalLength = 0; for (const vector of vectors) { if (typeof(vector) === "number") { totalLength++; @@ -1062,7 +1062,7 @@ export class ByteVector { /** * Sets the value at a specified index * @param index Index to set the value of - * @param value Value to set at the index. Must be a valid integer between 0x0 and 0xff + * @param value Value to set at the index. Must be a valid integer betweenInclusive 0x0 and 0xff */ public set(index: number, value: number): void { if (this._isReadOnly) { diff --git a/src/combinedTag.ts b/src/combinedTag.ts index 9045b251..4e4a9a4c 100644 --- a/src/combinedTag.ts +++ b/src/combinedTag.ts @@ -120,7 +120,7 @@ export default class CombinedTag extends Tag { * @description This is typically useful for movies, although the instrument played by each * artist in a music file may be of relevance. * It is highly important to match each role to the performers. This means that a role may - * be `null\undefined` to keep a match between performers[i] and performersRole[i]. + * be `null\undefined` to keep a match betweenInclusive performers[i] and performersRole[i]. */ public get performersRole(): string[] { return this.getFirstArray((t) => t.performersRole); } /** diff --git a/src/errors.ts b/src/errors.ts index 78c3d33b..cbf15e6a 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -10,6 +10,6 @@ export class NotImplementedError extends Error { public static isNotImplementedError: boolean = true; public constructor(message?: string) { - super(`Not implemented${message ? ": message" : ""}`); + super(`Not implemented${message ? `: ${message}` : ""}`); } } diff --git a/src/file.ts b/src/file.ts index fbc91c22..f357f342 100644 --- a/src/file.ts +++ b/src/file.ts @@ -388,7 +388,7 @@ export abstract class File { } // Ensure that we always rewind the stream a little so we never have a partial - // match where our data exists between the end of read A and the start of read B. + // match where our data exists betweenInclusive the end of read A and the start of read B. bufferOffset += File._bufferSize - pattern.length; if (before != null && before.length > pattern.length) { bufferOffset -= before.length - pattern.length; diff --git a/src/id3v1/id3v1Tag.ts b/src/id3v1/id3v1Tag.ts index 1bac0333..f3fd6172 100644 --- a/src/id3v1/id3v1Tag.ts +++ b/src/id3v1/id3v1Tag.ts @@ -163,7 +163,7 @@ export default class Id3v1Tag extends Tag { } /** * @inheritDoc - * @description Only values between 1 and 9999 will be stored. All other values will result in + * @description Only values betweenInclusive 1 and 9999 will be stored. All other values will result in * the property being zeroed. */ public set year(value: number) { @@ -174,7 +174,7 @@ export default class Id3v1Tag extends Tag { public get track(): number { return this._track; } /** * @inheritDoc - * @description Only values between 1 and 255 will be stored. All other values will result in + * @description Only values betweenInclusive 1 and 255 will be stored. All other values will result in * the property being zeroed. */ public set track(value: number) { diff --git a/src/id3v2/frames/frame.ts b/src/id3v2/frames/frame.ts index ae89850a..2de96250 100644 --- a/src/id3v2/frames/frame.ts +++ b/src/id3v2/frames/frame.ts @@ -61,7 +61,7 @@ export abstract class Frame { * Sets the encryption ID applied to the current instance. * @param value Value containing the encryption identifier for the current instance. Must be a * safe 16-bit integer. - * Encryption values can be between 0 and 255. Setting any other value will unset the + * Encryption values can be betweenInclusive 0 and 255. Setting any other value will unset the * encryption ID and set the value to -1; */ public set encryptionId(value: number) { @@ -106,7 +106,7 @@ export abstract class Frame { /** * Sets the grouping ID applied to the current instance. * @param value Grouping identifier for the current instance. Must be a 16-bit integer. - * Grouping identifiers can be between 0 and 255. Setting any other value will unset the + * Grouping identifiers can be betweenInclusive 0 and 255. Setting any other value will unset the * grouping identify and set the value to -1. */ public set groupId(value: number) { diff --git a/src/id3v2/frames/relativeVolumeFrame.ts b/src/id3v2/frames/relativeVolumeFrame.ts index 4b1cb24a..4ef461fd 100644 --- a/src/id3v2/frames/relativeVolumeFrame.ts +++ b/src/id3v2/frames/relativeVolumeFrame.ts @@ -55,41 +55,111 @@ export enum ChannelType { Subwoofer = 0x08 } -class ChannelData { - private _peakVolume: number; - private _peakVolumeIndex: BigInt.BigInteger; +export class ChannelData { + private _channel: ChannelType; + private _peakBits: number; + private _peakVolume: BigInt.BigInteger; private _volumeAdjustment: number; - private _volumeAdjustmentIndex: number; + + public constructor(channel: ChannelType) { + this._channel = channel; + } + + public static fromData(bytes: ByteVector): ChannelData { + Guards.truthy(bytes, "bytes"); + + const channelType = bytes.get(0); + const channelData = new ChannelData(channelType); + channelData._volumeAdjustment = bytes.mid(1, 2).toShort(); + channelData._peakBits = bytes.get(3); + + const peakByteCount = Math.ceil(channelData._peakBits / 8); + const peakBytes = bytes.mid(4); + const zeroes = ByteVector.fromSize(peakByteCount - peakBytes.length, 0x00); + peakBytes.insertByteVector(0, zeroes); + channelData._peakVolume = peakBytes.toULong(); + + return channelData; + } + + public get channelType(): ChannelType { return this._channel; } public get isSet(): boolean { - return this._volumeAdjustmentIndex !== 0 || !this._peakVolumeIndex.isZero(); + const volumeAdjustSet = !!this._volumeAdjustment; + const peakSet = !!this._peakVolume && !this._peakVolume.isZero(); + return volumeAdjustSet || peakSet; } - public get peakVolume(): number { return this._peakVolume; } - public set peakVolume(value: number) { - this._peakVolume = value; - this._peakVolumeIndex = BigInt(value).multiply(512); + /** + * Number of bits used to express the peak volume. + */ + public get peakBits(): number { return this._peakBits; } + /** + * Number of bits used to express the peak volume. + * @param value Bits used to express the peak volume. Must be an integer betweenInclusive 1 and 64 + */ + public set peakBits(value: number) { + Guards.byte(value, "value"); + Guards.betweenInclusive(value, 1, 64, "value"); + this._peakBits = value; } - public get peakVolumeIndex(): BigInt.BigInteger { return this._peakVolumeIndex; } - public set peakVolumeIndex(value: BigInt.BigInteger) { - Guards.ulong(value, "value"); - this._peakVolumeIndex = value; - this._peakVolume = value / 512; // @TODO: Not sure this works as expected due to bigint division + /** + * Value of the peak sample in the file. It's unclear exactly how this works, but the ID3v2.4 + * documentation explains this value as betweenInclusive 0 and 255 - but can be expressed using any + * number of bits ({@see peakBits}). + */ + public get peakVolume(): BigInt.BigInteger { return this._peakVolume; } + /** + * Value of the peak sample in the file. It's unclear exactly how this works, but the ID3v2.4 + * documentation explains this value as betweenInclusive 0 and 255 - but can be expressed using any + * number of bits ({@see peakBits}). + * @param value Peak volume value. Must fit in the number of bits set in {@see peakBits} + */ + public set peakVolume(value: BigInt.BigInteger) { + if (!this.peakBits) { + throw new Error("Peak bits must be set before setting peak volume"); + } + if (value.isNegative()) { + throw new Error("Argument out of range: value must be positive"); + } + if (value.gt(BigInt(2).pow(this.peakBits).minus(1))) { + throw new Error("Argument out of range: value must fit within number of bits defined by peakBits"); + } + this._peakVolume = value; } - public get volumeAdjustment(): number { return this._volumeAdjustment; } + /** + * Volume adjustment of the track in dB. + */ + public get volumeAdjustment(): number { return this._volumeAdjustment / 512; } + /** + * Volume adjustment of the track in dB. This value is expressed as a fixed-precision value + * betweenInclusive -64 and 64. Don't worry about the math, we'll do it for you. + * @param value Volume adjustment. Must be betweenInclusive -64 and 64. + */ public set volumeAdjustment(value: number) { - Guards.between(value, -64, 64, "value"); - this._volumeAdjustment = value; - this._volumeAdjustmentIndex = Math.floor(value * 512); + Guards.int(value, "value"); + Guards.betweenExclusive(value, -64, 64, "value"); + this._volumeAdjustment = Math.floor(value * 512); } - public get volumeAdjustmentIndex(): number { return this._volumeAdjustmentIndex; } - public set volumeAdjustmentIndex(value: number) { - Guards.short(value, "value"); - this._volumeAdjustmentIndex = value; - this._volumeAdjustment = value / 512.0; + public render(): ByteVector { + if (!this.isSet) { + return ByteVector.empty(); + } + + // NOTE: According to the docs, peak volume is to be stored in as few bytes as possible for + // the number of bits used to encode it. For instance, 1-8 bits peak volume must be stored + // in 1 byte, 8-16 in 2 bytes, etc. + + const peakByteCount = Math.ceil(this._peakBits / 8); + return ByteVector.concatenate( + this._channel, + ByteVector.fromShort(this._volumeAdjustment), + this._peakBits, + ByteVector.fromULong(this._peakVolume).mid(8 - peakByteCount) + ); } } @@ -101,6 +171,9 @@ export class RelativeVolumeFrame extends Frame { private constructor(header: Id3v2FrameHeader) { super(header); + for (let i = 0; i < 9; i++) { + this._channels[i] = new ChannelData(i); + } } /** @@ -193,38 +266,37 @@ export class RelativeVolumeFrame extends Frame { } /** - * Gets the peak volume for a specified channel + * Gets the number of bits used to encode the peak volume * @param type Which channel to get the value for */ - public getPeakVolume(type: ChannelType): number { - return this._channels[type].peakVolume; + public getPeakBits(type: ChannelType): number { + return this._channels[type].peakBits; } /** - * Gets the peak volume index for a specified channel. The peak volume index is simply the peak - * volume multiplied by 512. + * Gets the peak volume for a specified channel * @param type Which channel to get the value for */ - public getPeakVolumeIndex(type: ChannelType): BigInt.BigInteger { - return this._channels[type].peakVolumeIndex; + public getPeakVolume(type: ChannelType): BigInt.BigInteger { + return this._channels[type].peakVolume; } /** * Gets the volume adjustment for the specified channel. * @param type Which channel to get the value for - * @returns number Volume adjustment for the channel, can be between -64 and +64 decibels + * @returns number Volume adjustment for the channel, can be betweenInclusive -64 and +64 decibels */ public getVolumeAdjustment(type: ChannelType): number { return this._channels[type].volumeAdjustment; } /** - * Gets the volume adjustment index for a specified channel. - * The volume adjustment index is simply the volume adjustment multiplied by 512. - * @param type Which channel to get the value for + * Sets the number of bits used to encode peak volume for a specified channel. + * @param type Which channel to set the value for + * @param value Peak volume */ - public getVolumeAdjustmentIndex(type: ChannelType): number { - return this._channels[type].volumeAdjustmentIndex; + public setPeakBits(type: ChannelType, value: number) { + this._channels[type].peakBits = value; } /** @@ -232,40 +304,19 @@ export class RelativeVolumeFrame extends Frame { * @param type Which channel to set the value for * @param value Peak volume */ - public setPeakVolume(type: ChannelType, value: number): void { + public setPeakVolume(type: ChannelType, value: BigInt.BigInteger): void { this._channels[type].peakVolume = value; } - /** - * Sets the peak volume index for a specified channel. The peak volume index is simply the peak - * volume multiplied by 512. - * @param type Which channel to set the value for - * @param index Peak volume index - */ - public setPeakVolumeIndex(type: ChannelType, index: BigInt.BigInteger): void { - this._channels[type].peakVolumeIndex = index; - } - /** * Sets the volume adjustment in decibels for the specified channel. * @param type Which channel to set the value for - * @param value Volume adjustment in decibels. Must be between -64 and +64 + * @param value Volume adjustment in decibels. Must be betweenInclusive -64 and +64 */ public setVolumeAdjustment(type: ChannelType, value: number): void { this._channels[type].volumeAdjustment = value; } - /** - * Sets the volume adjustment index for a specified channel. - * The volume adjustment index is simply the volume adjustment multiplied by 512. - * @param type Which channel to set the value of - * @param index Volume adjustment index (volume adjustment multiplied by 512), must be a 16-bit - * integer. - */ - public setVolumeAdjustmentIndex(type: ChannelType, index: number): void { - this._channels[type].volumeAdjustmentIndex = index; - } - /** * Creates a text description of the current instance */ @@ -279,29 +330,26 @@ export class RelativeVolumeFrame extends Frame { /** @inheritDoc */ protected parseFields(data: ByteVector, version: number): void { - Guards.byte(version, "version"); - - let pos = data.find(ByteVector.getTextDelimiter(StringType.Latin1)); - if (pos < 0) { + const identifierEndIndex = data.find(ByteVector.getTextDelimiter(StringType.Latin1)); + if (identifierEndIndex < 0) { return; } - this._identification = data.toString(StringType.Latin1, 0, pos++); - - // Each channel is at least 4 bytes - while (pos <= data.length - 4) { - const type = data.get(pos++); - this._channels[type].volumeAdjustmentIndex = data.mid(pos, 2).toUShort(); - pos += 2; + this._identification = data.toString(identifierEndIndex, StringType.Latin1); - const bytes = RelativeVolumeFrame.bitsToBytes(data.get(pos++)); + let pos = identifierEndIndex + 1; + while (pos < data.length) { + const dataLength = 4 + Math.ceil(data.get(pos + 3) / 8); + const dataBytes = data.mid(pos, dataLength); - if (data.length < pos + bytes) { + // If we're at the end of the vector, we'll just end processing + if (dataBytes.length !== dataLength) { break; } - this._channels[type].peakVolumeIndex = data.mid(pos, bytes).toULong(); - pos += bytes; + const channelData = ChannelData.fromData(dataBytes); + this._channels[channelData.channelType] = channelData; + pos += dataLength; } } @@ -311,36 +359,15 @@ export class RelativeVolumeFrame extends Frame { data.addByteVector(ByteVector.getTextDelimiter(StringType.Latin1)); for (let i = 0; i < 9; i++) { - if (!this._channels[i].isSet) { + if (!this._channels[i] || !this._channels[i].isSet) { continue; } - data.addByte(i); - data.addByteVector(ByteVector.fromUShort(this._channels[i].volumeAdjustmentIndex)); - - let bits = 0; - for (let j = 0; j < 64; j++) { - if (!this._channels[i].peakVolumeIndex.and(1 << j).isZero()) { - bits = j + 1; - } - } - - data.addByte(bits); - - if (bits > 0) { - const ulongBytes = ByteVector.fromULong(this._channels[i].peakVolumeIndex); - data.addByteVector(ulongBytes.mid(8 - RelativeVolumeFrame.bitsToBytes(bits))); - } + data.addByteVector(this._channels[i].render()); } return data; } - private static bitsToBytes(i: number) { - return i % 8 === 0 - ? i / 8 - : (i - i % 8) / 8 + 1; - } - // #endregion } diff --git a/src/id3v2/frames/textInformationFrame.ts b/src/id3v2/frames/textInformationFrame.ts index e8d86c19..ffbf5049 100644 --- a/src/id3v2/frames/textInformationFrame.ts +++ b/src/id3v2/frames/textInformationFrame.ts @@ -52,11 +52,11 @@ import {Guards, StringComparison} from "../../utils"; * original recording, if for example the music in the file should be a cover of a previously * released song. * * TCOM - The "composer" frame is intended for the name of the composer. - * * TMCL - The "musician credits list" frame is intended as a mapping between instruments and the + * * TMCL - The "musician credits list" frame is intended as a mapping betweenInclusive instruments and the * musician who played it. Every odd field is an instrument and every even is an artst of a comma * delimited list of artists. * * TIPL - The "Involved people list" frame is very similar to the musician credits list, but maps - * between functions, like producer, and names. + * betweenInclusive functions, like producer, and names. * * TENC - The "Encoded by" frame contains the name of the person or organization that encoded the * audio file. This field may contain a copyright message, if the audio file is also copyrighted * by the encoder. diff --git a/src/id3v2/header.ts b/src/id3v2/header.ts index 513a19fb..a26c35fb 100644 --- a/src/id3v2/header.ts +++ b/src/id3v2/header.ts @@ -101,11 +101,11 @@ export default class Header { * Sets the major version of the tag described by the current instance. * When the version is set, unsupported header flags will automatically be removed from the * tag. - * @param value ID3v2 version of tag. Must be a positive 8-bit integer between 2 and 4. + * @param value ID3v2 version of tag. Must be a positive 8-bit integer betweenInclusive 2 and 4. */ public set majorVersion(value: number) { Guards.byte(value, "value"); - Guards.between(value, 2, 4, "value"); + Guards.betweenInclusive(value, 2, 4, "value"); // @TODO: do we need to support setting to versions <4? if (value < 3) { diff --git a/src/id3v2/id3v2Tag.ts b/src/id3v2/id3v2Tag.ts index ee60d634..29ac3c73 100644 --- a/src/id3v2/id3v2Tag.ts +++ b/src/id3v2/id3v2Tag.ts @@ -104,7 +104,7 @@ export default class Id3v2Tag extends Tag { */ public set version(value: number) { Guards.byte(value, "value"); - Guards.between(value, 2, 4, "value"); + Guards.betweenInclusive(value, 2, 4, "value"); this._header.majorVersion = value; } diff --git a/src/id3v2/id3v2TagSettings.ts b/src/id3v2/id3v2TagSettings.ts index 23a72f17..e905b439 100644 --- a/src/id3v2/id3v2TagSettings.ts +++ b/src/id3v2/id3v2TagSettings.ts @@ -35,7 +35,7 @@ export default class Id3v2TagSettings { */ public static set defaultVersion(value: number) { Guards.byte(value, "value"); - Guards.between(value, 2, 4, "value"); + Guards.betweenInclusive(value, 2, 4, "value"); Id3v2TagSettings._defaultVersion = value; } diff --git a/src/utils.ts b/src/utils.ts index 69b39a50..4f0720b5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,15 @@ import * as BigInt from "big-integer"; export class Guards { - public static between(value: number, minValue: number, maxValue: number, name: string): void { + public static betweenExclusive(value: number, minValue: number, maxValue: number, name: string): void { + if (value <= minValue || value >= maxValue) { + throw new Error(`Argument out of range: ${name} must satisfy ${maxValue} <= ${name} <= ${minValue}`); + } + } + + public static betweenInclusive(value: number, minValue: number, maxValue: number, name: string): void { if (value < minValue || value > maxValue) { - throw new Error(`Argument out of range: ${name} must be between ${minValue} and ${maxValue}`); + throw new Error(`Argument out of range: ${name} must satisfy ${maxValue} < ${name} < ${minValue}`); } } diff --git a/test/id3v2/PopularimeterFrameTests.ts b/test/id3v2/popularimeterFrameTests.ts similarity index 100% rename from test/id3v2/PopularimeterFrameTests.ts rename to test/id3v2/popularimeterFrameTests.ts diff --git a/test/id3v2/relativeVolumeFrameTests.ts b/test/id3v2/relativeVolumeFrameTests.ts new file mode 100644 index 00000000..86f06ac0 --- /dev/null +++ b/test/id3v2/relativeVolumeFrameTests.ts @@ -0,0 +1,506 @@ +import * as BigInt from "big-integer"; +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import ConstructorTests from "./frameConstructorTests"; +import FrameTypes from "../../src/id3v2/frameTypes"; +import {ChannelData, ChannelType, RelativeVolumeFrame} from "../../src/id3v2/frames/relativeVolumeFrame"; +import {ByteVector, StringType} from "../../src/byteVector"; +import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; +import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import FramePropertiesTests from "./framePropertiesTests"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(timeout(3000), slow(1000)) +class Id3v2_RelativeVolumeChannelData { + @test + public peakBits_setInvalidValues() { + // Arrange + const channel = new ChannelData(0); + + // Act / Assert + assert.throws(() => { channel.peakBits = -1; }); + assert.throws(() => { channel.peakBits = 1.23; }); + assert.throws(() => { channel.peakBits = 0x100; }); + assert.throws(() => { channel.peakBits = 0; }); + assert.throws(() => { channel.peakBits = 65; }); + } + + @test + public peakBits_setValidValues() { + // Arrange + const channel = new ChannelData(0); + + // Act + channel.peakBits = 7; + + // Assert + assert.strictEqual(channel.peakBits, 7); + } + + @test + public peakVolume_peakBitsNotSet() { + // Arrange + const channel = new ChannelData(0); + + // Act / Assert + assert.throws(() => { + channel.peakBits = undefined; + channel.peakVolume = BigInt(123); + }); + assert.throws(() => { + channel.peakBits = null; + channel.peakVolume = BigInt(123); + }); + assert.throws(() => { + channel.peakBits = 0; + channel.peakVolume = BigInt(123); + }); + } + + @test + public peakVolume_setOutsideBitRange() { + // Arrange + const channel = new ChannelData(0); + channel.peakBits = 8; + + // Act / Assert + assert.throws(() => { channel.peakVolume = BigInt(0x100); }); + assert.throws(() => { channel.peakVolume = BigInt(-100); }); + } + + @test + public peakVolume_setWithinRange() { + // Arrange + const channel = new ChannelData(0); + channel.peakBits = 16; + + // Act + channel.peakVolume = BigInt(0x1FF); + + // Assert + assert.isTrue(channel.peakVolume.equals(0x1FF)); + } + + @test + public volumeAdjustment_invalidValues() { + // Arrange + const channel = new ChannelData(0); + + // Act / Assert + assert.throws(() => { channel.volumeAdjustment = -64; }); + assert.throws(() => { channel.volumeAdjustment = -1.23; }); + assert.throws(() => { channel.volumeAdjustment = 1.23; }); + assert.throws(() => { channel.volumeAdjustment = 64; }); + } + + @test + public volumeAdjustment_withinRange() { + // Arrange + const channel = new ChannelData(0); + + // Act + channel.volumeAdjustment = 32; + + // Assert + assert.strictEqual(channel.volumeAdjustment, 32); + } + + @test + public isSet_trueViaVolumeAdjustment() { + // Arrange + const channel = new ChannelData(0); + channel.volumeAdjustment = 32; + + // Act / Assert + assert.isTrue(channel.isSet); + } + + @test + public isSet_trueViaPeakVolume() { + // Arrange + const channel = new ChannelData(0); + channel.peakBits = 8; + channel.peakVolume = BigInt(123); + + // Act / Assert + assert.isTrue(channel.isSet); + } + + @test + public isSet_false() { + // Arrange + const channel = new ChannelData(0); + channel.peakBits = 8; + + // Act / Assert + assert.isFalse(channel.isSet); + } + + @test + public render_notSet() { + // Arrange + const channel = new ChannelData(0); + channel.peakBits = 4; + + // Act + const output = channel.render(); + + // Assert + assert.strictEqual(0, output.length); + } + + @test + public render_lessThan8BitsStoresAsByte() { + // Arrange + const channel = new ChannelData(ChannelType.Subwoofer); + channel.peakBits = 7; + channel.peakVolume = BigInt(0x1F); + channel.volumeAdjustment = 32; + + // Act + const output = channel.render(); + + // Assert + const expected = ByteVector.concatenate( + ChannelType.Subwoofer, + 0x40, 0x00, + 0x07, + 0x1F + ); + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_15BitsStoresAsShort() { + // Arrange + const channel = new ChannelData(ChannelType.Subwoofer); + channel.peakBits = 15; + channel.peakVolume = BigInt(0x1FFF); + channel.volumeAdjustment = 32; + + // Act + const output = channel.render(); + + // Assert + const expected = ByteVector.concatenate( + ChannelType.Subwoofer, + 0x40, 0x00, + 0x0F, + 0x1F, 0xFF + ); + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_23BitsStoresAs3Bytes() { + // Arrange + const channel = new ChannelData(ChannelType.Subwoofer); + channel.peakBits = 23; + channel.peakVolume = BigInt(0x1FFFFF); + channel.volumeAdjustment = 32; + + // Act + const output = channel.render(); + + // Assert + const expected = ByteVector.concatenate( + ChannelType.Subwoofer, + 0x40, 0x00, + 0x17, + 0x1F, 0xFF, 0xFF + ); + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_31BitsStoresAs4Bytes() { + // Arrange + const channel = new ChannelData(ChannelType.Subwoofer); + channel.peakBits = 31; + channel.peakVolume = BigInt(0x1FFFFFFF); + channel.volumeAdjustment = 32; + + // Act + const output = channel.render(); + + // Assert + const expected = ByteVector.concatenate( + ChannelType.Subwoofer, + 0x40, 0x00, + 0x1F, + 0x1F, 0xFF, 0xFF, 0xFF + ); + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_39BitsStoresAs5Bytes() { + // Arrange + const channel = new ChannelData(ChannelType.Subwoofer); + channel.peakBits = 39; + channel.peakVolume = BigInt("1FFFFFFFFF", 16); + channel.volumeAdjustment = 32; + + // Act + const output = channel.render(); + + // Assert + const expected = ByteVector.concatenate( + ChannelType.Subwoofer, + 0x40, 0x00, + 0x27, + 0x1F, 0xFF, 0xFF, 0xFF, 0xFF + ); + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_64BitsStoresAs8Bytes() { + // Arrange + const channel = new ChannelData(ChannelType.Subwoofer); + channel.peakBits = 64; + channel.peakVolume = BigInt("1FFFFFFFFFFFFFFF", 16); + channel.volumeAdjustment = 32; + + // Act + const output = channel.render(); + + // Assert + const expected = ByteVector.concatenate( + ChannelType.Subwoofer, + 0x40, 0x00, + 0x40, + 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF + ); + assert.isTrue(ByteVector.equal(output, expected)); + } +} + +@suite(timeout(3000), slow(1000)) +class Id3v2_RelativeVolumeFrame_ConstructorTests extends ConstructorTests { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + return RelativeVolumeFrame.fromOffsetRawData; + } + + public get fromRawData(): (d: ByteVector, v: number) => Frame { + return RelativeVolumeFrame.fromRawData; + } + + @test + public fromIdentification() { + // Act + const frame = RelativeVolumeFrame.fromIdentification("foo"); + + // Assert + this.assertFrame(frame, [], "foo"); + } + + @test + public fromRawData_noDelimiter_emptyFrame() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.RVA2, 4); + header.frameSize = 3; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("foo", StringType.Latin1) + ); + + // Act + const frame = RelativeVolumeFrame.fromRawData(data, 4); + + // Assert + this.assertFrame(frame, [], undefined); + } + + @test + public fromRawData_withChannelData_hasChannelData() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.RVA2, 4); + header.frameSize = 15; + + const channel1 = new ChannelData(ChannelType.Subwoofer); + channel1.volumeAdjustment = 32; + channel1.peakBits = 8; + channel1.peakVolume = BigInt(12); + + const channel2 = new ChannelData(ChannelType.BackCenter); + channel2.volumeAdjustment = 62; + channel2.peakBits = 16; + channel2.peakVolume = BigInt(123); + + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("foo", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + channel1.render(), + channel2.render() + ); + + // Act + const frame = RelativeVolumeFrame.fromRawData(data, 4); + + // Assert + this.assertFrame(frame, [channel2, channel1], "foo"); + } + + @test + public fromOffsetRawData_withChannelData_hasChannelData() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.RVA2, 4); + header.frameSize = 15; + + const channel1 = new ChannelData(ChannelType.Subwoofer); + channel1.volumeAdjustment = 32; + channel1.peakBits = 8; + channel1.peakVolume = BigInt(12); + + const channel2 = new ChannelData(ChannelType.BackCenter); + channel2.volumeAdjustment = 62; + channel2.peakBits = 16; + channel2.peakVolume = BigInt(123); + + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, + ByteVector.fromString("foo", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + channel1.render(), + channel2.render() + ); + + // Act + const frame = RelativeVolumeFrame.fromOffsetRawData(data, 2, header); + + // Assert + this.assertFrame(frame, [channel2, channel1], "foo"); + } + + private assertFrame(frame: RelativeVolumeFrame, c: ChannelData[], i: string) { + assert.isOk(frame); + assert.strictEqual(frame.frameClassType, FrameClassType.RelativeVolumeFrame); + assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.RVA2)); + + assert.deepStrictEqual(frame.channels, c); + assert.strictEqual(frame.identification, i); + } +} + +@suite(timeout(3000), slow(1000)) +class Id3v2_RelativeVolumeFrameMethodTests { + @test + public find_falsyFrames() { + // Act / Assert + assert.throws(() => { RelativeVolumeFrame.find(undefined, "foo"); }); + assert.throws(() => { RelativeVolumeFrame.find(null, "foo"); }); + } + + @test + public find_noMatches() { + // Arrange + const frames = [ + RelativeVolumeFrame.fromIdentification("fux"), + RelativeVolumeFrame.fromIdentification("bux") + ]; + + // Act + const result = RelativeVolumeFrame.find(frames, "qux"); + + // Assert + assert.isUndefined(result); + } + + @test + public find_multipleMatches() { + // Arrange + const frames = [ + RelativeVolumeFrame.fromIdentification("fux"), + RelativeVolumeFrame.fromIdentification("bux"), + RelativeVolumeFrame.fromIdentification("qux"), + RelativeVolumeFrame.fromIdentification("qux") + ]; + + // Act + const result = RelativeVolumeFrame.find(frames, "qux"); + + // Assert + assert.strictEqual(result, frames[2]); + } + + @test + public peakBits() { + // Arrange + const frame = RelativeVolumeFrame.fromIdentification("foo"); + + // Act / Assert + FramePropertiesTests.propertyRoundTrip( + (v) => { frame.setPeakBits(ChannelType.Subwoofer, v); }, + () => frame.getPeakBits(ChannelType.Subwoofer), + 8 + ); + } + + @test + public peakVolume() { + // Arrange + const frame = RelativeVolumeFrame.fromIdentification("foo"); + frame.setPeakBits(ChannelType.Subwoofer, 16); + + // Act / Assert + FramePropertiesTests.propertyRoundTrip( + (v) => { frame.setPeakVolume(ChannelType.Subwoofer, v); }, + () => frame.getPeakVolume(ChannelType.Subwoofer), + BigInt(123) + ); + } + + @test + public volumeAdjustment() { + // Arrange + const frame = RelativeVolumeFrame.fromIdentification("foo"); + + // Act / Assert + FramePropertiesTests.propertyRoundTrip( + (v) => { frame.setVolumeAdjustment(ChannelType.Subwoofer, v); }, + () => frame.getVolumeAdjustment(ChannelType.Subwoofer), + 8 + ); + } + + @test + public render() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.RVA2, 4); + header.frameSize = 15; + + const channel1 = new ChannelData(ChannelType.Subwoofer); + channel1.volumeAdjustment = 32; + channel1.peakBits = 8; + channel1.peakVolume = BigInt(12); + + const channel2 = new ChannelData(ChannelType.BackCenter); + channel2.volumeAdjustment = 62; + channel2.peakBits = 16; + channel2.peakVolume = BigInt(123); + + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("foo", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + channel2.render(), + channel1.render() + ); + const frame = RelativeVolumeFrame.fromRawData(data, 4); + + // Act + const output = frame.render(4); + + // Assert + assert.ok(output); + assert.isTrue(ByteVector.equal(output, data)); + } +} From 364c86dcf8ded9ba6efce98825d6fa23b3aa523c Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Mon, 20 Jan 2020 12:44:40 -0500 Subject: [PATCH 28/71] Last batch of cleanup stuff and things --- test/id3v2/eventTimeCodeFrameTests.ts | 8 ++++---- test/id3v2/synchronizedLyricsFrameTests.ts | 2 +- test/id3v2/tagFooterTests.ts | 6 +++--- test/id3v2/tagHeaderTests.ts | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/test/id3v2/eventTimeCodeFrameTests.ts b/test/id3v2/eventTimeCodeFrameTests.ts index 86cf897a..daa8c6d5 100644 --- a/test/id3v2/eventTimeCodeFrameTests.ts +++ b/test/id3v2/eventTimeCodeFrameTests.ts @@ -17,7 +17,7 @@ Chai.use(ChaiAsPromised); const assert = Chai.assert; @suite(timeout(3000), slow(1000)) -class EventTimeCodeTests { +class Id3v2_EventTimeCodeTests { @test public constructor_invalidTime() { // Act/Assert @@ -95,7 +95,7 @@ class EventTimeCodeTests { } @suite(timeout(3000), slow(1000)) -class EventTimeCodeFrameConstructorTests extends FrameConstructorTests { +class Id3v2_EventTimeCodeFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { return EventTimeCodeFrame.fromOffsetRawData; } @@ -212,7 +212,7 @@ class EventTimeCodeFrameConstructorTests extends FrameConstructorTests { } @suite(timeout(3000), slow(1000)) -class EventTimeCodeFramePropertyTests { +class Id3v2_EventTimeCodeFrame_PropertyTests { @test public events() { // Arrange @@ -242,7 +242,7 @@ class EventTimeCodeFramePropertyTests { } @suite(timeout(3000), slow(1000)) -class EventTimeCodeFrameMethodTests { +class Id3v2_EventTimeCodeFrame_MethodTests { @test public clone() { // Arrange diff --git a/test/id3v2/synchronizedLyricsFrameTests.ts b/test/id3v2/synchronizedLyricsFrameTests.ts index 221b2c44..ec90b738 100644 --- a/test/id3v2/synchronizedLyricsFrameTests.ts +++ b/test/id3v2/synchronizedLyricsFrameTests.ts @@ -17,7 +17,7 @@ Chai.use(ChaiAsPromised); const assert = Chai.assert; @suite(timeout(3000), slow(1000)) -class SynchronizedTextTests { +class Id3v2_SynchronizedTextTests { @test public synchronizedText_construct() { // Act diff --git a/test/id3v2/tagFooterTests.ts b/test/id3v2/tagFooterTests.ts index 6ec33693..8fcf8147 100644 --- a/test/id3v2/tagFooterTests.ts +++ b/test/id3v2/tagFooterTests.ts @@ -24,7 +24,7 @@ const getTestFooter = (majorVersion: number, minorVersion: number, flags: Header }; @suite(timeout(3000), slow(1000)) -class Id3v2_TagFooterConstructorTests { +class Id3v2_TagFooter_ConstructorTests { @test public falsyData() { // Act/Assert @@ -107,7 +107,7 @@ class Id3v2_TagFooterConstructorTests { } @suite(timeout(3000), slow(1000)) -class TagFooterPropertyTests { +class Id3v2_TagFooter_PropertyTests { @test public getCompleteTagSize() { // Arrange @@ -217,7 +217,7 @@ class TagFooterPropertyTests { } @suite(timeout(3000), slow(1000)) -class TagFooterRenderTests { +class Id3v2_TagFooter_RenderTests { @test public render() { // Arrange diff --git a/test/id3v2/tagHeaderTests.ts b/test/id3v2/tagHeaderTests.ts index 80249fe0..b21e1b9c 100644 --- a/test/id3v2/tagHeaderTests.ts +++ b/test/id3v2/tagHeaderTests.ts @@ -24,7 +24,7 @@ const getTestHeader = (majorVersion: number, minorVersion: number, flags: Header }; @suite(timeout(3000), slow(1000)) -class Id3v2_TagHeaderConstructorTests { +class Id3v2_TagHeader_ConstructorTests { @test public falsyData() { // Act/Assert @@ -137,7 +137,7 @@ class Id3v2_TagHeaderConstructorTests { } @suite(timeout(3000), slow(1000)) -class TagHeaderPropertyTests { +class Id3v2_TagHeader_PropertyTests { @test public getFileIdentifier() { // Act @@ -326,7 +326,7 @@ class TagHeaderPropertyTests { } @suite(timeout(3000), slow(1000)) -class TagHeaderRenderTests { +class Id3v2_TagHeader_RenderTests { @test public render() { // Arrange From cce8b734f08ee4912168d403b4c23837f121d85c Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 21 Jan 2020 20:32:47 -0500 Subject: [PATCH 29/71] Rest of the comments frame and playcount frame tests --- src/id3v2/frames/commentsFrame.ts | 3 +- src/id3v2/frames/frameFactory.ts | 2 +- src/id3v2/frames/playCountFrame.ts | 5 +- test/id3v2/commentsFrameTests.ts | 122 +++++++++++++++++ test/id3v2/playCountFrameTests.ts | 206 +++++++++++++++++++++++++++++ 5 files changed, 333 insertions(+), 5 deletions(-) create mode 100644 test/id3v2/playCountFrameTests.ts diff --git a/src/id3v2/frames/commentsFrame.ts b/src/id3v2/frames/commentsFrame.ts index 8627d040..d490cee0 100644 --- a/src/id3v2/frames/commentsFrame.ts +++ b/src/id3v2/frames/commentsFrame.ts @@ -270,10 +270,9 @@ export default class CommentsFrame extends Frame { } protected renderFields(version: number): ByteVector { - Guards.byte(version, "version"); const encoding = Frame.correctEncoding(this.textEncoding, version); const v = ByteVector.empty(); - v.addByte(version); + v.addByte(encoding); v.addByteVector(ByteVector.fromString(this.language, StringType.Latin1)); v.addByteVector(ByteVector.fromString(this.description, encoding)); v.addByteVector(ByteVector.getTextDelimiter(encoding)); diff --git a/src/id3v2/frames/frameFactory.ts b/src/id3v2/frames/frameFactory.ts index 6a40874d..229ed6b2 100644 --- a/src/id3v2/frames/frameFactory.ts +++ b/src/id3v2/frames/frameFactory.ts @@ -185,7 +185,7 @@ export default { func = AttachmentFrame.fromOffsetRawData; } else if (ByteVector.equal(header.frameId, FrameTypes.PCNT)) { // Play count (frames 4.16) - func = PlayCountFrame.fromOffsetRawHeader; + func = PlayCountFrame.fromOffsetRawData; } else if (ByteVector.equal(header.frameId, FrameTypes.POPM)) { // Popularimeter (frames 4.17) func = PopularimeterFrame.fromOffsetRawData; diff --git a/src/id3v2/frames/playCountFrame.ts b/src/id3v2/frames/playCountFrame.ts index 72e1e3ad..25cc79db 100644 --- a/src/id3v2/frames/playCountFrame.ts +++ b/src/id3v2/frames/playCountFrame.ts @@ -13,6 +13,7 @@ export default class PlayCountFrame extends Frame { private constructor(header: Id3v2FrameHeader) { super(header); + this._playCount = BigInt.zero; } // #region Constructors @@ -24,7 +25,7 @@ export default class PlayCountFrame extends Frame { return new PlayCountFrame(new Id3v2FrameHeader(FrameTypes.PCNT, 4)); } - public static fromOffsetRawHeader( + public static fromOffsetRawData( data: ByteVector, offset: number, header: Id3v2FrameHeader @@ -44,7 +45,7 @@ export default class PlayCountFrame extends Frame { * @param data ByteVector starting with the raw representation of the new frame * @param version ID3v2 veersion the raw frame is encoded in, must be a positive 8-bit integer */ - public static fromRawHeader(data: ByteVector, version: number): PlayCountFrame { + public static fromRawData(data: ByteVector, version: number): PlayCountFrame { Guards.truthy(data, "data"); Guards.byte(version, "version"); diff --git a/test/id3v2/commentsFrameTests.ts b/test/id3v2/commentsFrameTests.ts index 2ddd3982..97a37cfb 100644 --- a/test/id3v2/commentsFrameTests.ts +++ b/test/id3v2/commentsFrameTests.ts @@ -462,4 +462,126 @@ class Id3v2_CommentsFrame_MethodTests { assert.isArray(output); assert.sameMembers(output, [frames[2]]); } + + @test + public findPreferred_noFrames_returnsUndefined() { + // Arrange + const frames: CommentsFrame[] = []; + + // Act + const output = CommentsFrame.findPreferred(frames, "fux", "eng"); + + // Assert + assert.isUndefined(output); + } + + @test + public findPreferred_firstFrameMatch() { + // Arrange + const frames = [ + CommentsFrame.fromDescription("fux", "eng") + ]; + + // Act + const output = CommentsFrame.findPreferred(frames, "bux", "jpn"); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[0]); + } + + @test + public findPreferred_languageMatch() { + // Arrange + const frames = [ + CommentsFrame.fromDescription("fux", "eng"), + CommentsFrame.fromDescription("bux", "eng"), + CommentsFrame.fromDescription("fux", "jpn"), + ]; + + // Act + const output = CommentsFrame.findPreferred(frames, "bux", "jpn"); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[2]); + } + + @test + public findPreferred_descriptionMatch() { + // Arrange + const frames = [ + CommentsFrame.fromDescription("fux", "eng"), + CommentsFrame.fromDescription("bux", "eng"), + ]; + + // Act + const output = CommentsFrame.findPreferred(frames, "bux", "jpn"); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[1]); + } + + @test + public findPreferred_perfectMatch() { + // Arrange + const frames = [ + CommentsFrame.fromDescription("fux", "eng"), + CommentsFrame.fromDescription("fux", "jpn"), + CommentsFrame.fromDescription("bux", "eng"), + CommentsFrame.fromDescription("bux", "jpn"), + ]; + + // Act + const output = CommentsFrame.findPreferred(frames, "bux", "jpn"); + + // Assert + assert.isOk(output); + assert.strictEqual(output, frames[3]); + } + + @test + public clone() { + // Arrange + const frame = CommentsFrame.fromDescription("fux", "bux", StringType.UTF16BE); + frame.text = "qux"; + + // Act + const output = frame.clone(); + + // Assert + assert.isOk(output); + assert.equal(output.frameClassType, FrameClassType.CommentsFrame); + assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.COMM)); + + assert.strictEqual(output.description, frame.description); + assert.strictEqual(output.language, frame.language); + assert.strictEqual(output.textEncoding, frame.textEncoding); + assert.strictEqual(output.text, frame.text); + } + + @test + public render() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + header.frameSize = 11; + const data = ByteVector.concatenate( + header.render(4), + StringType.Latin1, + ByteVector.fromString("eng", StringType.Latin1), + ByteVector.fromString("fux", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + ByteVector.fromString("bux", StringType.Latin1) + ); + const frame = CommentsFrame.fromRawData(data, 4); + + // Act + const output = frame.render(4); + + // Assert + assert.isOk(output); + assert.isTrue(ByteVector.equal(output, data)); + + } } diff --git a/test/id3v2/playCountFrameTests.ts b/test/id3v2/playCountFrameTests.ts new file mode 100644 index 00000000..1324acdf --- /dev/null +++ b/test/id3v2/playCountFrameTests.ts @@ -0,0 +1,206 @@ +import * as BigInt from "big-integer"; +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import FrameConstructorTests from "./frameConstructorTests"; +import FramePropertyTests from "./framePropertyTests"; +import FrameTypes from "../../src/id3v2/frameTypes"; +import PlayCountFrame from "../../src/id3v2/frames/playCountFrame"; +import {ByteVector, StringType} from "../../src/byteVector"; +import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; +import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(timeout(3000), slow(1000)) +class Id3v2_PlayCountFrame_ConstructorTests extends FrameConstructorTests { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + return PlayCountFrame.fromOffsetRawData; + } + + public get fromRawData(): (d: ByteVector, v: number) => Frame { + return PlayCountFrame.fromRawData; + } + + @test + public fromEmpty() { + // Act + const frame = PlayCountFrame.fromEmpty(); + + // Assert + this.assertFrame(frame, 0); + } + + @test + public fromRawData_fourBytePlayCount() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.PCNT, 4); + header.frameSize = 4; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromUInt(1234) + ); + + // Act + const frame = PlayCountFrame.fromRawData(data, 4); + + // Assert + this.assertFrame(frame, 1234); + } + + public fromRawData_sixBytePlayCount() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.PCNT, 4); + header.frameSize = 6; + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, ByteVector.fromUInt(1234) + ); + + // Act + const frame = PlayCountFrame.fromRawData(data, 4); + + // Assert + this.assertFrame(frame, 1234); + } + + @test + public fromRawData_eightBytePlayCount() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.PCNT, 4); + header.frameSize = 8; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromULong(BigInt("4294967296")) + ); + + // Act + const frame = PlayCountFrame.fromRawData(data, 4); + + // Assert + this.assertFrame(frame, BigInt("4294967296")); + } + + @test + public fromOffsetRawData_twoBytePlayCount() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.PCNT, 4); + header.frameSize = 8; + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, + ByteVector.fromULong(BigInt("4294967296")) + ); + + // Act + const frame = PlayCountFrame.fromOffsetRawData(data, 2, header); + + // Assert + this.assertFrame(frame, BigInt("4294967296")); + } + + private assertFrame(frame: PlayCountFrame, p: BigInt.BigNumber) { + assert.isOk(frame); + assert.strictEqual(frame.frameClassType, FrameClassType.PlayCountFrame); + assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.PCNT)); + + assert.isTrue(frame.playCount.eq(p)); + } +} + +@suite(timeout(3000), slow(1000)) +class Id3v2_PlayCountFrame_PropertyTests { + @test + public playCount() { + // Arrange + const frame = PlayCountFrame.fromEmpty(); + const set = (v: BigInt.BigInteger) => { frame.playCount = v; }; + const get = () => frame.playCount; + + // Act / Assert + FramePropertyTests.propertyThrows(set, BigInt(-1)); + FramePropertyTests.propertyThrows(set, BigInt("18446744073709551616", 10)); + FramePropertyTests.propertyRoundTrip(set, get, BigInt(100)); + FramePropertyTests.propertyRoundTrip(set, get, BigInt("68719476721", 10)); + } +} + +@suite(timeout(3000), slow(1000)) +class Id3v2_PlayCountFrame_MethodTests { + @test + public clone() { + // Arrange + const frame = PlayCountFrame.fromEmpty(); + frame.playCount = BigInt(123); + + // Act + const output = frame.clone(); + + // Assert + assert.isOk(output); + assert.strictEqual(output.frameClassType, FrameClassType.PlayCountFrame); + assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.PCNT)); + + assert.isTrue(output.playCount.eq(frame.playCount)); + } + + @test + public render_intPlayCount() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.PCNT, 4); + header.frameSize = 4; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromUInt(1234) + ); + const frame = PlayCountFrame.fromRawData(data, 4); + + // Act + const output = frame.render(4); + + // Assert + assert.isOk(output); + assert.isTrue(ByteVector.equal(output, data)); + } + + @test + public render_sixBytePlayCount() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.PCNT, 4); + header.frameSize = 6; + const data = ByteVector.concatenate( + header.render(4), + 0x12, 0x34, 0x45, 0x56, 0x78, 0x90 + ); + const frame = PlayCountFrame.fromRawData(data, 4); + + // Act + const output = frame.render(4); + + // Assert + assert.isOk(output); + assert.isTrue(ByteVector.equal(output, data)); + } + + @test + public render_longBytePlayCount() { + // Arrange + const header = new Id3v2FrameHeader(FrameTypes.PCNT, 4); + header.frameSize = 8; + const data = ByteVector.concatenate( + header.render(4), + 0x12, 0x34, 0x45, 0x56, 0x78, 0x90, 0xAB, 0xCD + ); + const frame = PlayCountFrame.fromRawData(data, 4); + + // Act + const output = frame.render(4); + + // Assert + assert.isOk(output); + assert.isTrue(ByteVector.equal(output, data)); + } +} From 19dda61a29feb5dccc8fd155cfde9dc4025bbeb9 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Fri, 24 Jan 2020 18:01:56 -0500 Subject: [PATCH 30/71] Implementation of lazy picture --- src/picture.ts | 15 +-- src/pictureLazy.ts | 213 +++++++++++++++++++++++++++++++++++++++++++ test/pictureTests.ts | 17 ++++ 3 files changed, 232 insertions(+), 13 deletions(-) create mode 100644 src/pictureLazy.ts create mode 100644 test/pictureTests.ts diff --git a/src/picture.ts b/src/picture.ts index 7e3ac558..9c46c181 100644 --- a/src/picture.ts +++ b/src/picture.ts @@ -280,17 +280,6 @@ export class Picture implements IPicture { return picture; } - public static fromPicture(original: IPicture): Picture { - const picture = new Picture(); - picture.mimeType = original.mimeType; - picture.type = original.type; - picture.filename = original.filename; - picture.description = original.description; - picture.data = original.data; - - return picture; - } - // #endregion // #region Public Properties @@ -309,8 +298,8 @@ export class Picture implements IPicture { // #region Public Static Methods - public static getExtensionFromData(data: ByteVector): string|null { - let ext = null; + public static getExtensionFromData(data: ByteVector): string { + let ext; // No picture unless it is corrupted, can fit in a file of less than 4 bytes if (data.length >= 4) { diff --git a/src/pictureLazy.ts b/src/pictureLazy.ts new file mode 100644 index 00000000..180fdce8 --- /dev/null +++ b/src/pictureLazy.ts @@ -0,0 +1,213 @@ +import * as Path from "path"; + +import ILazy from "./iLazy"; +import {IPicture, Picture, PictureType} from "./picture"; +import {ByteVector} from "./byteVector"; +import {IFileAbstraction, LocalFileAbstraction} from "./fileAbstraction"; +import {Guards} from "./utils"; +import {SeekOrigin} from "./stream"; + +/** + * This class implements {@see IPicture} and provides mechanisms for loading pictures from files. + * Contrary to {@see Picture}, a reference to a file where the picture is located can be given and + * the picture is lazily loaded from the file, meaning that it will be read from the file only when + * needed. This saves time and memory if the picture loading is not required. + */ +export default class PictureLazy implements IPicture, ILazy { + private _data: ByteVector; + private _description: string; + private _file: IFileAbstraction; + private _filename: string; + private _mimeType: string; + private _streamOffset: number; + private _streamSize: number; + private _type: PictureType; + + // #region Constructors + + private constructor() {} + + /** + * Constructs a new picture using data that's already been read into memory. The content + * will not be lazily loaded. + * @param data ByteVector Object containing picture data + */ + public static fromData(data: ByteVector): PictureLazy { + Guards.truthy(data, "data"); + + const picture = new PictureLazy(); + picture._data = ByteVector.fromByteVector(data); + + const extension = Picture.getExtensionFromData(data); + if (extension) { + picture._type = PictureType.FrontCover; + picture._filename = `cover${extension}`; + picture._description = picture._filename; + } else { + picture._type = PictureType.NotAPicture; + picture._filename = "UnknownType"; + } + picture._mimeType = Picture.getMimeTypeFromExtension(extension); + + return picture; + } + + /** + * Constructs a new instance from a file abstraction. The content will be lazily loaded. + * @param file File abstraction containing the file to read + * @param offset Index into the file where the picture is located, must be a 32-bit integer + * @param size Optionally, size of the picture in bytes. If omitted, all bytes will be read + * when lazily loaded. Must be a 32-bit integer or `undefined` + */ + public static fromFile(file: IFileAbstraction, offset: number, size?: number): PictureLazy { + Guards.truthy(file, "file"); + Guards.int(offset, "offset"); + if (size !== undefined) { + Guards.int(offset, "size"); + } + + const picture = new PictureLazy(); + picture._file = file; + picture._streamOffset = offset; + picture._streamSize = size; + picture._filename = file.name; + picture._description = file.name; + + if (picture._filename && picture._filename.indexOf(".") > -1) { + picture._mimeType = Picture.getMimeTypeFromExtension(picture._filename); + picture._type = picture._mimeType.startsWith("image/") ? PictureType.FrontCover : PictureType.NotAPicture; + } + + return picture; + } + + /** + * Constructs a new instance that will be lazily loaded from the path provided. + * @param path Path to the file to read + */ + public static fromPath(path: string): PictureLazy { + Guards.truthy(path, "path"); + + const picture = new PictureLazy(); + picture._file = new LocalFileAbstraction(path); + picture._filename = Path.basename(path); + picture._description = picture._filename; + picture._mimeType = Picture.getMimeTypeFromExtension(picture._filename); + picture._type = picture._mimeType.startsWith("image/") ? PictureType.FrontCover : PictureType.NotAPicture; + + return picture; + } + + // #endregion + + // #region Properties + + /** @inheritDoc */ + public get data(): ByteVector { + if (!this._data) { this.load(); } + return this._data; + } + /** @inheritDoc */ + public set data(value: ByteVector) { this._data = value; } + + /** @inheritDoc */ + public get description(): string { return this._description; } + /** @inheritDoc */ + public set description(value: string) { this._description = value; } + + /** @inheritDoc */ + public get filename(): string { + if (!this._filename) { this.load(); } + return this._filename; + } + /** @inheritDoc */ + public set filename(value: string) { this._filename = value; } + + /** @inheritDoc */ + public get isLoaded(): boolean { return !!this._data; } + + /** @inheritDoc */ + public get mimeType(): string { + if (!this._mimeType) { this.load(); } + return this._mimeType; + } + /** @inheritDoc */ + public set mimeType(value: string) { this._mimeType = value; } + + /** @inheritDoc */ + public get type(): PictureType { + if (this._type === PictureType.Other && !this._mimeType) { this.load(); } + return this._type; + } + /** @inheritDoc */ + public set type(value: PictureType) { this._type = value; } + + // #endregion + + // #region Methods + + /** @inheritDoc */ + public load(): void { + // Already loaded? + if (this._data) { + return; + } + + // Load the picture from the stream for the file + let stream; + try { + if (this._streamSize === 0) { + // Picture is 0 bytes, just use an empty byte vector for the data + this._data = ByteVector.empty(); + } else if (this._streamSize !== undefined) { + // Picture length was defined, read those bytes as the data + stream = this._file.readStream; + stream.seek(this._streamOffset, SeekOrigin.Begin); + + let count = 0; + let read = 0; + let needed = this._streamSize; + const buffer = new Uint8Array(needed); + + do { + count = stream.read(buffer, read, needed); + read += count; + needed -= count; + } while (needed > 0 && count !== 0); + + this._data = ByteVector.fromByteArray(buffer, read); + } else { + // Picture length was not defined, read entire stream as the data + stream = this._file.readStream; + stream.seek(this._streamOffset, SeekOrigin.Begin); + this._data = ByteVector.fromInternalStream(stream); + } + } finally { + // Free any open resources + if (stream && this._file) { + this._file.closeStream(stream); + } + this._file = undefined; + } + + // Retrieve remaining properties from data (if required) + if (!this._mimeType) { + const ext = Picture.getExtensionFromData(this._data); + this._mimeType = Picture.getMimeTypeFromExtension(ext); + if (ext) { + this._type = PictureType.FrontCover; + if (!this._filename) { + this._filename = `cover${ext}`; + this._description = this._filename; + } + } else { + this._type = PictureType.NotAPicture; + if (!this._filename) { + this._filename = "UnknownType"; + } + } + } + } + + // #endregion +} diff --git a/test/pictureTests.ts b/test/pictureTests.ts new file mode 100644 index 00000000..062120bd --- /dev/null +++ b/test/pictureTests.ts @@ -0,0 +1,17 @@ +import * as BigInt from "big-integer"; +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import * as StreamBuffers from "stream-buffers"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import TestConstants from "./testConstants"; +import {ByteVector, StringType} from "../src/byteVector"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(timeout(3000), slow(1000)) +class Picture_StaticMethodTests { + public +} \ No newline at end of file From eaee18eecf3b41406ec7e2f1d4004552b5c56135 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Fri, 24 Jan 2020 18:58:15 -0500 Subject: [PATCH 31/71] Unit tests for picture utility methods --- src/picture.ts | 55 ++++++++++------ test/pictureTests.ts | 153 +++++++++++++++++++++++++++++++++++++++++-- test/tslint.json | 6 ++ 3 files changed, 190 insertions(+), 24 deletions(-) create mode 100644 test/tslint.json diff --git a/src/picture.ts b/src/picture.ts index 9c46c181..69eb5664 100644 --- a/src/picture.ts +++ b/src/picture.ts @@ -2,6 +2,7 @@ import * as path from "path"; import {ByteVector} from "./byteVector"; import {IFileAbstraction} from "./fileAbstraction"; +import {Guards} from "./utils"; export enum PictureType { /** @@ -131,12 +132,12 @@ export class Picture implements IPicture { // #region Constants private static readonly _lutExtensionMime: string[] = [ + "bin", "application/octet-stream", // Any kind of binary data - placed at top to act as default "aac", "audio/aac", // AAC audio file "abw", "application/x-abiword", // AbiWord document "arc", "application/octet-stream", // Archive document (multiple files embedded) "avi", "video/x-msvideo", // AVI: Audio Video Interleave "azw", "application/vnd.amazon.ebook", // Amazon Kindle eBook format - "bin", "application/octet-stream", // Any kind of binary data "bmp", "image/bmp", // BMP image data "bmp", "image/x-windows-bmp", // BMP image data "bm", "image/bmp", // BMP image data @@ -160,7 +161,7 @@ export class Picture implements IPicture { "json", "application/json", // JSON format "mid", "audio/midi", // Musical Instrument Digital Interface (MIDI) "midi", "audio/midi", // Musical Instrument Digital Interface (MIDI) - "mp3", "audio/mpeg", + "mp3", "audio/mpeg", // MPEG audio, mp3 is first since it's most likely "mp1", "audio/mpeg", "mp2", "audio/mpeg", "mpg", "video/mpeg", @@ -298,21 +299,25 @@ export class Picture implements IPicture { // #region Public Static Methods + /** + * Retrieve a mimetype from raw file data by reading the first few bytes of the file. Less + * accurate than {@see getExtensionFromMimeType} since this is limited to image file types. + * @param data Bytes of the file to read to identify the extension + * @returns string Extension of the file with dot at the beginning based on the first few bytes + * of the data. If the extension cannot be determined, `undefined` is returned + */ public static getExtensionFromData(data: ByteVector): string { - let ext; + let ext: string; // No picture unless it is corrupted, can fit in a file of less than 4 bytes - if (data.length >= 4) { + if (!data || data.length >= 4) { if (data.get(1) === 0x50 && data.get(2) === 0x4E && data.get(3) === 0x47) { - // PNG ext = ".png"; - } else if (data.get(0) === 0x47 && data.get(1) === 0x49 && data.get(2) === 0x47) { - // GIF + } else if (data.get(0) === 0x47 && data.get(1) === 0x49 && data.get(2) === 0x46) { ext = ".gif"; } else if (data.get(0) === 0x42 && data.get(1) === 0x4D) { - // BM ext = ".bmp"; - } else if (data.get(0) === 0xFF && data.get(1) === 0xD8 && data.get(2) === 0xFF && data.get(3) === 0xE0) { + } else if (data.get(0) === 0xFF && data.get(1) === 0xD8) { ext = ".jpg"; } } @@ -320,33 +325,45 @@ export class Picture implements IPicture { return ext; } - public static getExtensionFromMimeType(mime: string): string|null { - let ext: string = null; + /** + * Gets the file extension for a specific mimetype. + * @param mime Mimetype to lookup the extension for + * @returns string Extension of the file based on the mimetype with a dot at the beginning. If + * the extension cannot be determined, `undefined` is returned + */ + public static getExtensionFromMimeType(mime: string): string { + let ext: string; - for (let i = 1; i < this._lutExtensionMime.length; i += 2) { - if (this._lutExtensionMime[i] === mime) { - ext = this._lutExtensionMime[i - 1]; + for (let i = 0; i < this._lutExtensionMime.length; i += 2) { + if (this._lutExtensionMime[i + 1] === mime) { + ext = this._lutExtensionMime[i]; break; } } - return ext; + return ext ? `.${ext}` : ext; } - public static getMimeTypeFromExtension(name: string): string|null { + /** + * Gets the mimetype of a file based on its extension. If the mimetype cannot be determined, it + * is assumed to be a basic binary file. + * @param name Filename with extension or just the extension of the file + * @returns string Mimetype of the file based on the extension. If mimetype cannot be + * determined, application/octet-stream is returned. + */ + public static getMimeTypeFromExtension(name: string): string { let mimeType: string = "application/octet-stream"; - if (!name || name.length === 0) { + if (!name) { return mimeType; } let ext = path.extname(name); - if (!ext || ext.length === 0) { + if (!ext) { ext = name; } else { ext = ext.substring(1); } - ext = ext.substring(1); for (let i = 0; i < this._lutExtensionMime.length; i += 2) { if (this._lutExtensionMime[i] === ext) { diff --git a/test/pictureTests.ts b/test/pictureTests.ts index 062120bd..547439aa 100644 --- a/test/pictureTests.ts +++ b/test/pictureTests.ts @@ -1,11 +1,9 @@ -import * as BigInt from "big-integer"; import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; -import * as StreamBuffers from "stream-buffers"; import {slow, suite, test, timeout} from "mocha-typescript"; -import TestConstants from "./testConstants"; import {ByteVector, StringType} from "../src/byteVector"; +import {Picture} from "../src/picture"; // Setup chai Chai.use(ChaiAsPromised); @@ -13,5 +11,150 @@ const assert = Chai.assert; @suite(timeout(3000), slow(1000)) class Picture_StaticMethodTests { - public -} \ No newline at end of file + @test + public getExtensionFromData_noMatch() { + // Arrange + const data = ByteVector.fromString("FOObarBaZ"); + + // Act + const output = Picture.getExtensionFromData(data); + + // Assert + assert.isUndefined(output); + } + + @test + public getExtensionFromData_dataTooShort() { + // Arrange + const data = ByteVector.fromSize(2); + + // Act + const output = Picture.getExtensionFromData(data); + + // Assert + assert.isUndefined(output); + } + + @test + public getExtensionFromData_jpeg() { + // Arrange + const data = ByteVector.concatenate( + 0xFF, 0xD8, 0xFF, + ByteVector.fromString("foobarbaz") + ); + + // Act + const output = Picture.getExtensionFromData(data); + + // Assert + assert.strictEqual(output, ".jpg"); + } + + @test + public getExtensionFromData_png() { + // Arrange + const data = ByteVector.concatenate(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A); + + // Act + const output = Picture.getExtensionFromData(data); + + // Assert + assert.strictEqual(output, ".png"); + } + + @test + public getExtensionFromData_bmp() { + // Arrange + const data = ByteVector.fromString("BMfoobarbaz", StringType.Latin1); + + // Act + const output = Picture.getExtensionFromData(data); + + // Assert + assert.strictEqual(output, ".bmp"); + } + + @test + public getExtensionFromData_gif() { + // Arrange + const data = ByteVector.fromString("GIF8foobarbaz", StringType.Latin1); + + // Act + const output = Picture.getExtensionFromData(data); + + // Assert + assert.strictEqual(output, ".gif"); + } + + @test + public getExtensionFromMimeType_noMatch() { + // Act + const output = Picture.getExtensionFromMimeType("audio/foobarbaz"); + + // Assert + assert.isUndefined(output); + } + + @test + public getExtensionFromMimeType_mp3() { + // Act + const output = Picture.getExtensionFromMimeType("audio/mpeg"); + + // Assert + assert.strictEqual(output, ".mp3"); + } + + @test + public getExtensionFromMimeType_bin() { + // Act + const output = Picture.getExtensionFromMimeType("application/octet-stream"); + + // Assert + assert.strictEqual(output, ".bin"); + } + + @test + public getExtensionFromMimeType_matchFound() { + // Act + const output = Picture.getExtensionFromMimeType("application/rtf"); + + // Assert + assert.strictEqual(output, ".rtf"); + } + + @test + public getMimeTypeFromExtension_extensionNotGiven() { + // Act + const output = Picture.getMimeTypeFromExtension(""); + + // Assert + assert.strictEqual(output, "application/octet-stream"); + } + + @test + public getMimeTypeFromExtension_noExtension() { + // Act + const output = Picture.getMimeTypeFromExtension("gif"); + + // Assert + assert.strictEqual(output, "image/gif"); + } + + @test + public getMimeTypeFromExtension_matchFound() { + // Act + const output = Picture.getMimeTypeFromExtension("/foo/bar/baz.jpg"); + + // Assert + assert.strictEqual(output, "image/jpeg"); + } + + @test + public getMimeTypeFromExtension_noMatchFound() { + // Act + const output = Picture.getMimeTypeFromExtension("/foo/bar/baz.qqq"); + + // Assert + assert.strictEqual(output, "application/octet-stream"); + } +} diff --git a/test/tslint.json b/test/tslint.json new file mode 100644 index 00000000..d8f5946f --- /dev/null +++ b/test/tslint.json @@ -0,0 +1,6 @@ +{ + "extends": "../tslint.json", + "rules": { + "class-name": false + } +} \ No newline at end of file From ecf458511abe9b58bf8add1a13701b0de59192e9 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 25 Jan 2020 15:00:48 -0500 Subject: [PATCH 32/71] Unit tests for lazy picture --- src/id3v2/frames/attachmentFrame.ts | 2 +- src/picture.ts | 12 +- src/pictureLazy.ts | 8 +- test/pictureLazyTests.ts | 275 ++++++++++++++++++++++++++++ test/pictureTests.ts | 25 ++- test/utilities/testStream.ts | 2 +- 6 files changed, 304 insertions(+), 20 deletions(-) create mode 100644 test/pictureLazyTests.ts diff --git a/src/id3v2/frames/attachmentFrame.ts b/src/id3v2/frames/attachmentFrame.ts index 55093b55..15d1aeba 100644 --- a/src/id3v2/frames/attachmentFrame.ts +++ b/src/id3v2/frames/attachmentFrame.ts @@ -451,7 +451,7 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { pos = offset = 1; } else { const ext = this._data.mid(pos, 3); - this._mimeType = Picture.getMimeTypeFromExtension(ext.toString(StringType.UTF8)); + this._mimeType = Picture.getMimeTypeFromFilename(ext.toString(StringType.UTF8)); pos += 3; } diff --git a/src/picture.ts b/src/picture.ts index 69eb5664..c9580d9f 100644 --- a/src/picture.ts +++ b/src/picture.ts @@ -225,7 +225,7 @@ export class Picture implements IPicture { picture.data = ByteVector.fromPath(filePath); picture.filename = path.basename(filePath); picture.description = picture.filename; - picture.mimeType = Picture.getMimeTypeFromExtension(picture.filename); + picture.mimeType = Picture.getMimeTypeFromFilename(picture.filename); picture.type = picture.mimeType.startsWith("image/") ? PictureType.FrontCover : PictureType.NotAPicture; return picture; @@ -241,7 +241,7 @@ export class Picture implements IPicture { picture.data = ByteVector.fromByteVector(data); const ext = Picture.getExtensionFromData(data); - picture.mimeType = Picture.getMimeTypeFromExtension(ext); + picture.mimeType = Picture.getMimeTypeFromFilename(ext); if (ext) { picture.type = PictureType.FrontCover; picture.filename = picture.description = "cover" + ext; @@ -264,11 +264,11 @@ export class Picture implements IPicture { picture.description = abstraction.name; if (picture.filename && picture.filename.indexOf(".") >= 0) { - picture.mimeType = Picture.getMimeTypeFromExtension(picture.filename); + picture.mimeType = Picture.getMimeTypeFromFilename(picture.filename); picture.type = picture.mimeType.startsWith("image/") ? PictureType.FrontCover : PictureType.NotAPicture; } else { const ext = Picture.getExtensionFromData(picture.data); - picture.mimeType = Picture.getMimeTypeFromExtension(ext); + picture.mimeType = Picture.getMimeTypeFromFilename(ext); if (ext) { picture.type = PictureType.FrontCover; picture.filename = picture.description = "cover" + ext; @@ -351,7 +351,7 @@ export class Picture implements IPicture { * @returns string Mimetype of the file based on the extension. If mimetype cannot be * determined, application/octet-stream is returned. */ - public static getMimeTypeFromExtension(name: string): string { + public static getMimeTypeFromFilename(name: string): string { let mimeType: string = "application/octet-stream"; if (!name) { @@ -360,7 +360,7 @@ export class Picture implements IPicture { let ext = path.extname(name); if (!ext) { - ext = name; + ext = name.startsWith(".") ? name.substring(1) : name; } else { ext = ext.substring(1); } diff --git a/src/pictureLazy.ts b/src/pictureLazy.ts index 180fdce8..7af4c968 100644 --- a/src/pictureLazy.ts +++ b/src/pictureLazy.ts @@ -47,7 +47,7 @@ export default class PictureLazy implements IPicture, ILazy { picture._type = PictureType.NotAPicture; picture._filename = "UnknownType"; } - picture._mimeType = Picture.getMimeTypeFromExtension(extension); + picture._mimeType = Picture.getMimeTypeFromFilename(picture._filename); return picture; } @@ -74,7 +74,7 @@ export default class PictureLazy implements IPicture, ILazy { picture._description = file.name; if (picture._filename && picture._filename.indexOf(".") > -1) { - picture._mimeType = Picture.getMimeTypeFromExtension(picture._filename); + picture._mimeType = Picture.getMimeTypeFromFilename(picture._filename); picture._type = picture._mimeType.startsWith("image/") ? PictureType.FrontCover : PictureType.NotAPicture; } @@ -92,7 +92,7 @@ export default class PictureLazy implements IPicture, ILazy { picture._file = new LocalFileAbstraction(path); picture._filename = Path.basename(path); picture._description = picture._filename; - picture._mimeType = Picture.getMimeTypeFromExtension(picture._filename); + picture._mimeType = Picture.getMimeTypeFromFilename(picture._filename); picture._type = picture._mimeType.startsWith("image/") ? PictureType.FrontCover : PictureType.NotAPicture; return picture; @@ -193,7 +193,7 @@ export default class PictureLazy implements IPicture, ILazy { // Retrieve remaining properties from data (if required) if (!this._mimeType) { const ext = Picture.getExtensionFromData(this._data); - this._mimeType = Picture.getMimeTypeFromExtension(ext); + this._mimeType = Picture.getMimeTypeFromFilename(ext); if (ext) { this._type = PictureType.FrontCover; if (!this._filename) { diff --git a/test/pictureLazyTests.ts b/test/pictureLazyTests.ts new file mode 100644 index 00000000..feb5baf0 --- /dev/null +++ b/test/pictureLazyTests.ts @@ -0,0 +1,275 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import * as TypeMoq from "typemoq"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import PictureLazy from "../src/pictureLazy"; +import TestStream from "./utilities/testStream"; +import {IFileAbstraction} from "../src/fileAbstraction"; +import {ByteVector, StringType} from "../src/byteVector"; +import {PictureType} from "../src/picture"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(timeout(3000), slow(1000)) +class PictureLazy_Tests { + @test + public fromData_falsyData() { + // Act / Assert + assert.throws(() => { PictureLazy.fromData(undefined); }); + assert.throws(() => { PictureLazy.fromData(null); }); + } + + @test + public fromData_canGetExtension() { + // Arrange + const data = ByteVector.concatenate( + 0xFF, 0xD8, 0xFF, + ByteVector.fromString("foobarbaz", StringType.Latin1) + ); + + // Act + const picture = PictureLazy.fromData(data); + + // Assert + assert.isOk(picture); + assert.isTrue(ByteVector.equal(picture.data, data)); + assert.strictEqual(picture.description, "cover.jpg"); + assert.strictEqual(picture.filename, "cover.jpg"); + assert.strictEqual(picture.mimeType, "image/jpeg"); + assert.isTrue(picture.isLoaded); + assert.strictEqual(picture.type, PictureType.FrontCover); + } + + @test + public fromData_cannotGetExtension() { + // Arrange + const data = ByteVector.fromString("foobarbaz", StringType.Latin1); + + // Act + const picture = PictureLazy.fromData(data); + + // Assert + assert.isOk(picture); + assert.isTrue(ByteVector.equal(picture.data, data)); + assert.isUndefined(picture.description); + assert.strictEqual(picture.filename, "UnknownType"); + assert.strictEqual(picture.mimeType, "application/octet-stream"); + assert.isTrue(picture.isLoaded); + assert.strictEqual(picture.type, PictureType.NotAPicture); + } + + @test + public fromFile_falsyFile() { + // Act / Assert + assert.throws(() => { PictureLazy.fromFile(undefined, 0 , 0); }); + assert.throws(() => { PictureLazy.fromFile(null, 0, 0); }); + } + + @test + public fromFile_hasImageFilenameNoOffsetReadAll() { + // Arrange + const data = ByteVector.fromString("foobarbaz"); + const mockStream = new TestStream(data, false); + + const mockFile = TypeMoq.Mock.ofType(); + mockFile.setup((f) => f.name).returns(() => "foobarbaz.jpg"); + mockFile.setup((f) => f.readStream).returns(() => mockStream); + mockFile.setup((f) => f.closeStream(mockStream)); + + // Act + const picture = PictureLazy.fromFile(mockFile.object, 0); + + // Assert + this.assertFileLazyPicture( + picture, + data, + "foobarbaz.jpg", + "foobarbaz.jpg", + "image/jpeg", + PictureType.FrontCover + ); + } + + @test + public fromFile_hasNonImageFilenameNoOffsetReadAll() { + // Arrange + const data = ByteVector.fromString("foobarbaz"); + const mockStream = new TestStream(data, false); + + const mockFile = TypeMoq.Mock.ofType(); + mockFile.setup((f) => f.name).returns(() => "foobarbaz.bin"); + mockFile.setup((f) => f.readStream).returns(() => mockStream); + mockFile.setup((f) => f.closeStream(mockStream)); + + // Act + const picture = PictureLazy.fromFile(mockFile.object, 0); + + // Assert + this.assertFileLazyPicture( + picture, + data, + "foobarbaz.bin", + "foobarbaz.bin", + "application/octet-stream", + PictureType.NotAPicture + ); + } + + @test + public fromFile_noExtensionNoOffsetReadAll() { + // Arrange + const data = ByteVector.concatenate( + 0xFF, 0xD8, 0xFF, + ByteVector.fromString("foobarbaz") + ); + const mockStream = new TestStream(data, false); + + const mockFile = TypeMoq.Mock.ofType(); + mockFile.setup((f) => f.name).returns(() => "fuxbuxqux"); + mockFile.setup((f) => f.readStream).returns(() => mockStream); + mockFile.setup((f) => f.closeStream(mockStream)); + + // Act + const picture = PictureLazy.fromFile(mockFile.object, 0); + + // Assert + this.assertFileLazyPicture( + picture, + data, + "fuxbuxqux", + "fuxbuxqux", + "image/jpeg", + PictureType.FrontCover + ); + } + + @test + public fromFile_noExtensionNotImageNoOffsetReadAll() { + // Arrange + const data = ByteVector.fromString("foobarbaz"); + const mockStream = new TestStream(data, false); + + const mockFile = TypeMoq.Mock.ofType(); + mockFile.setup((f) => f.name).returns(() => "fuxbuxqux"); + mockFile.setup((f) => f.readStream).returns(() => mockStream); + mockFile.setup((f) => f.closeStream(mockStream)); + + // Act + const picture = PictureLazy.fromFile(mockFile.object, 0); + + // Assert + this.assertFileLazyPicture( + picture, + data, + "fuxbuxqux", + "fuxbuxqux", + "application/octet-stream", + PictureType.NotAPicture + ); + } + + @test + public fromFile_noFilenameNotImageFileNoOffsetReadAll() { + // Arrange + const data = ByteVector.fromString("foobarbaz"); + const mockStream = new TestStream(data, false); + + const mockFile = TypeMoq.Mock.ofType(); + mockFile.setup((f) => f.name).returns(() => undefined); + mockFile.setup((f) => f.readStream).returns(() => mockStream); + mockFile.setup((f) => f.closeStream(mockStream)); + + // Act + const picture = PictureLazy.fromFile(mockFile.object, 0); + + // Assert + this.assertFileLazyPicture( + picture, + data, + undefined, + "UnknownType", + "application/octet-stream", + PictureType.NotAPicture + ); + } + + @test + public fromFile_hasOffsetReadAll() { + const data = ByteVector.fromString("foobarbaz"); + const allData = ByteVector.concatenate( + 0x00, 0x00, + data + ); + const mockStream = new TestStream(allData, false); + + const mockFile = TypeMoq.Mock.ofType(); + mockFile.setup((f) => f.name).returns(() => "foobarbaz.jpg"); + mockFile.setup((f) => f.readStream).returns(() => mockStream); + mockFile.setup((f) => f.closeStream(mockStream)); + + // Act + const picture = PictureLazy.fromFile(mockFile.object, 2); + + // Assert + this.assertFileLazyPicture( + picture, + data, + "foobarbaz.jpg", + "foobarbaz.jpg", + "image/jpeg", + PictureType.FrontCover + ); + } + + @test + public fromFile_hasOffsetReadPartial() { + const data = ByteVector.fromString("foobarbaz", StringType.Latin1); + const allData = ByteVector.concatenate( + 0x00, 0x00, + data, + 0x00, 0x00 + ); + const mockStream = new TestStream(allData, false); + + const mockFile = TypeMoq.Mock.ofType(); + mockFile.setup((f) => f.name).returns(() => "foobarbaz.jpg"); + mockFile.setup((f) => f.readStream).returns(() => mockStream); + mockFile.setup((f) => f.closeStream(mockStream)); + + // Act + const picture = PictureLazy.fromFile(mockFile.object, 2, 9); + + // Assert + this.assertFileLazyPicture( + picture, + data, + "foobarbaz.jpg", + "foobarbaz.jpg", + "image/jpeg", + PictureType.FrontCover + ); + } + + private assertFileLazyPicture( + picture: PictureLazy, + d: ByteVector, + desc: string, + f: string, + mt: string, + t: PictureType + ) { + assert.isOk(picture); + assert.isFalse(picture.isLoaded); + + assert.isTrue(ByteVector.equal(picture.data, d)); + assert.strictEqual(picture.description, desc); + assert.strictEqual(picture.filename, f); + assert.strictEqual(picture.mimeType, mt); + assert.strictEqual(picture.type, t); + + assert.isTrue(picture.isLoaded); + } +} diff --git a/test/pictureTests.ts b/test/pictureTests.ts index 547439aa..4dc2ef91 100644 --- a/test/pictureTests.ts +++ b/test/pictureTests.ts @@ -123,36 +123,45 @@ class Picture_StaticMethodTests { } @test - public getMimeTypeFromExtension_extensionNotGiven() { + public getMimeTypeFromFilename_extensionNotGiven() { // Act - const output = Picture.getMimeTypeFromExtension(""); + const output = Picture.getMimeTypeFromFilename(""); // Assert assert.strictEqual(output, "application/octet-stream"); } @test - public getMimeTypeFromExtension_noExtension() { + public getMimeTypeFromFilename_justExtensionWithoutDot() { // Act - const output = Picture.getMimeTypeFromExtension("gif"); + const output = Picture.getMimeTypeFromFilename("gif"); // Assert assert.strictEqual(output, "image/gif"); } @test - public getMimeTypeFromExtension_matchFound() { + public getMimeTypeFromFilename_justExtensionWithDot() { // Act - const output = Picture.getMimeTypeFromExtension("/foo/bar/baz.jpg"); + const output = Picture.getMimeTypeFromFilename(".gif"); + + // Assert + assert.strictEqual(output, "image/gif"); + } + + @test + public getMimeTypeFromFilename_matchFound() { + // Act + const output = Picture.getMimeTypeFromFilename("/foo/bar/baz.jpg"); // Assert assert.strictEqual(output, "image/jpeg"); } @test - public getMimeTypeFromExtension_noMatchFound() { + public getMimeTypeFromFilename_noMatchFound() { // Act - const output = Picture.getMimeTypeFromExtension("/foo/bar/baz.qqq"); + const output = Picture.getMimeTypeFromFilename("/foo/bar/baz.qqq"); // Assert assert.strictEqual(output, "application/octet-stream"); diff --git a/test/utilities/testStream.ts b/test/utilities/testStream.ts index 0473dda1..cc6319d1 100644 --- a/test/utilities/testStream.ts +++ b/test/utilities/testStream.ts @@ -30,7 +30,7 @@ export default class TestStream implements IStream { public read(buffer: Uint8Array, bufferOffset: number, length: number): number { let bytesRead = 0; while (bytesRead < length && this._position + bytesRead < this._data.length) { - buffer[bufferOffset + bytesRead] = this._data.get(this._position); + buffer[bufferOffset + bytesRead] = this._data.get(this._position + bytesRead); bytesRead++; } From b71e06cbcb0f2dbee6fb760054ea3e49f03a40c0 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 25 Jan 2020 16:24:07 -0500 Subject: [PATCH 33/71] Rewrite of attachment frame to not do file loading --- src/id3v2/frames/attachmentFrame.ts | 297 +++++++++++----------------- src/id3v2/frames/frameFactory.ts | 9 +- test/id3v2/attachmentsFrameTests.ts | 179 ----------------- 3 files changed, 117 insertions(+), 368 deletions(-) diff --git a/src/id3v2/frames/attachmentFrame.ts b/src/id3v2/frames/attachmentFrame.ts index 15d1aeba..df348894 100644 --- a/src/id3v2/frames/attachmentFrame.ts +++ b/src/id3v2/frames/attachmentFrame.ts @@ -1,68 +1,29 @@ import FrameType from "../frameTypes"; import Id3v2TagSettings from "../id3v2TagSettings"; -import ILazy from "../../iLazy"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; -import {IFileAbstraction} from "../../fileAbstraction"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; import {IPicture, Picture, PictureType} from "../../picture"; -import {IStream, SeekOrigin} from "../../stream"; import {Guards} from "../../utils"; -export default class AttachmentFrame extends Frame implements ILazy, IPicture { - // #region Member Variables - +export default class AttachmentFrame extends Frame { private _data: ByteVector; private _description: string; private _encoding: StringType = Id3v2TagSettings.defaultEncoding; - private _file: IFileAbstraction; private _filename: string; private _mimeType: string; + private _rawPicture: IPicture; private _rawData: ByteVector; private _rawVersion: number; - private _streamOffset: number; - private _streamSize: number = -1; private _type: PictureType; - // #endregion - // #region Constructors private constructor(frameHeader: Id3v2FrameHeader) { super(frameHeader); } - /** - * Constructs a new attachment frame from a file. The content will be lazily loaded. - * @param abstraction Abstraction of the file to read. - * @param offset Position into the file where the picture is located - * @param size Size in bytes of the picture. Use -1 to read all bytes of the file - * @param header Header of the frame found at {@paramref offset} in the data - */ - public static fromFile( - abstraction: IFileAbstraction, - offset: number, - size: number, - header: Id3v2FrameHeader - ): AttachmentFrame { - Guards.truthy(abstraction, "abstraction"); - Guards.uint(offset, "offset"); - Guards.int(size, "size"); - Guards.truthy(header, "header"); - - if (size < 0 && size !== -1) { - throw new Error("Argument out of range: size must be positive 32-bit integer or -1"); - } - - const frame = new AttachmentFrame(header); - frame._file = abstraction; - frame._streamOffset = offset; - frame._streamSize = size; - frame._rawVersion = header.version; - return frame; - } - /** * Constructs and initializes a new attachment frame by reading its raw data in a specified * ID3v2 version. @@ -96,12 +57,9 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { public static fromPicture(picture: IPicture): AttachmentFrame { Guards.truthy(picture, "picture"); + // In this case we will assume the frame is an APIC until the picture is parsed const frame = new AttachmentFrame(new Id3v2FrameHeader(FrameType.APIC, 4)); - frame.type = picture.type; - frame._mimeType = picture.mimeType; - frame._filename = picture.filename; - frame._description = picture.description; - frame._data = picture.data; + frame._rawPicture = picture; return frame; } @@ -124,19 +82,21 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { // #region Properties + /** @inheritDoc */ + public get frameClassType(): FrameClassType { return FrameClassType.AttachmentFrame; } + /** * Gets the image data stored in the current instance. */ public get data(): ByteVector { - this.loadIfPossible(); - this.parseRawData(); + this.parseFromRaw(); return this._data ? this._data : ByteVector.empty(); } /** * Sets the image data stored in the current instance. */ public set data(value: ByteVector) { - this.loadIfPossible(); + this.parseFromRaw(); this._data = value; } @@ -144,8 +104,7 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { * Gets the description stored in the current instance. */ public get description(): string { - this.loadIfPossible(); - this.parseRawData(); + this.parseFromRaw(); return this._description ? this._description : ""; } /** @@ -153,7 +112,7 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { * There should only be one frame with a matching description and type per tag. */ public set description(value: string) { - this.loadIfPossible(); + this.parseFromRaw(); this._description = value; } @@ -161,31 +120,22 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { * Gets a filename of the picture stored in the current instance. */ public get filename(): string { - this.loadIfPossible(); + this.parseFromRaw(); return this._filename; } /** * Sets a filename of the picture stored in the current instance. */ public set filename(value: string) { - this.loadIfPossible(); + this.parseFromRaw(); this._filename = value; } - /** @inheritDoc */ - public get frameClassType(): FrameClassType { return FrameClassType.AttachmentFrame; } - - /** @inheritDoc */ - public get isLoaded(): boolean { - return !!(this._data || this._rawData); - } - /** * Gets the MimeType of the picture stored in the current instance. */ public get mimeType(): string { - this.loadIfPossible(); - this.parseRawData(); + this.parseFromRaw(); return this._mimeType ? this._mimeType : ""; } @@ -194,7 +144,7 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { * @param value MimeType of the picture stored in the current instance. */ public set mimeType(value: string) { - this.loadIfPossible(); + this.parseFromRaw(); this._mimeType = value; } @@ -203,8 +153,7 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { * @value Text encoding to use when storing the current instance. */ public get textEncoding(): StringType { - this.loadIfPossible(); - this.parseRawData(); + this.parseFromRaw(); return this._encoding; } /** @@ -214,7 +163,7 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { * `true` or the render version does not support it. */ public set textEncoding(value: StringType) { - this.loadIfPossible(); + this.parseFromRaw(); this._encoding = value; } @@ -222,8 +171,7 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { * Gets the object type stored in the current instance. */ public get type(): PictureType { - this.loadIfPossible(); - this.parseRawData(); + this.parseFromRaw(); return this._type; } /** @@ -232,7 +180,7 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { * Picture Frame. */ public set type(value: PictureType) { - this.loadIfPossible(); + this.parseFromRaw(); // Change the frame type depending if this is a picture or a general object const frameId = value === PictureType.NotAPicture @@ -251,22 +199,19 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { /** @inheritDoc */ public clone(): Frame { - if (this._file) { - this.load(); - } - - const frame = new AttachmentFrame(new Id3v2FrameHeader(FrameType.APIC, 4)); - frame._encoding = this._encoding; - frame._mimeType = this._mimeType; - frame.type = this._type; - frame._filename = this._filename; - frame._rawVersion = this._rawVersion; - - if (this._data) { + const frame = new AttachmentFrame(new Id3v2FrameHeader(this.frameId, 4)); + if (this._rawPicture) { + frame._rawPicture = this._rawPicture; + } else if (this._rawData) { + frame._rawData = this._rawData; + frame._rawVersion = this._rawVersion; + } else { frame._data = ByteVector.fromByteVector(this._data); - } - if (this._rawData) { - frame._data = ByteVector.fromByteVector(this._rawData); + frame._encoding = this._encoding; + frame._filename = this._filename; + frame._mimeType = this._mimeType; + frame._rawVersion = this._rawVersion; + frame._type = this._type; } return frame; @@ -294,47 +239,8 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { }); } - /** - * Load the picture data from the file if not done yet. - */ - public load(): void { - // Already loaded? - if (!this._file) { - return; - } - - // Load the picture from the stream - let stream: IStream; - let data: ByteVector; - - try { - if (this._streamSize === 0) { - data = ByteVector.empty(); - } else { - stream = this._file.readStream; - stream.seek(this._streamOffset, SeekOrigin.Begin); - - data = ByteVector.fromInternalStream(stream); - } - } finally { - if (stream && this._file) { - this._file.closeStream(stream); - } - - this._file = undefined; - } - - // Decode the raw data if required using FieldData - // NOTE: It probably seems weird to set the offset to a negative, but it's because we're - // loading the bytes from a file which won't have a frame header - this._rawData = this.fieldData(data, -Id3v2FrameHeader.getSize(this._rawVersion)); - - // Get the actual data - this.parseRawData(); - } - public toString(): string { - this.loadIfPossible(); + this.parseFromRaw(); let builder = ""; if (this.description) { @@ -349,7 +255,6 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { // #endregion protected parseFields(data: ByteVector, version: number): void { - this.loadIfPossible(); if (data.length < 5) { throw new CorruptFileError("A picture frame must contain at least 5 bytes"); } @@ -359,7 +264,7 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { } protected renderFields(version: number) { - this.loadIfPossible(); + this.parseFromRaw(); // Bypass processing if we haven't changed the data. Raw data is cleared when we touch any // fields inside this frame. @@ -411,83 +316,109 @@ export default class AttachmentFrame extends Frame implements ILazy, IPicture { return data; } - private loadIfPossible(): void { - if (this._file) { - this.load(); + private parseFromRaw(): void { + if (this._rawData) { + this.parseFromRawData(); + } else if (this._rawPicture) { + this.parseFromRawPicture(); } } - /** - * Performs the *actual* parsing of the raw data. - * Because of the high parsing cost and relatively low usage of the class, {@see parseFields} - * only stores the field data so it can be parsed on demand. Whenever a property or method is - * called, and only on the first call does it actually parse the data. - */ - private parseRawData(): void { - this.loadIfPossible(); - if (!this._rawData) { - return; - } - - this._data = this._rawData; + private parseFromRawData(): void { + // Indicate raw data has been processed + const data = this._rawData; this._rawData = undefined; - let pos = 0; - let offset: number; - - this._encoding = this._data.get(pos++); + // Determine encoding + this._encoding = data.get(0); const delim = ByteVector.getTextDelimiter(this._encoding); - if (ByteVector.equal(this._header.frameId, FrameType.APIC)) { + let descriptionEndIndex; + if (ByteVector.equal(this.frameId, FrameType.APIC)) { // Retrieve an ID3v2 attached picture if (this._rawVersion > 2) { - offset = this._data.find(ByteVector.getTextDelimiter(StringType.Latin1), pos); - - if (offset < pos) { + // Text encoding $xx + // MIME type $00 + // Picture type $xx + // Description $00 (00) + // Picture data + const mimeTypeEndIndex = data.find(ByteVector.getTextDelimiter(StringType.Latin1)); + if (mimeTypeEndIndex === -1) { return; } - - this._mimeType = this._data.toString(StringType.Latin1, pos, offset - pos); - pos = offset = 1; + const mimeTypeLength = mimeTypeEndIndex - 1; + this._mimeType = data.toString(mimeTypeLength, StringType.Latin1, 1); + + this._type = data.get(mimeTypeEndIndex + 1); + + descriptionEndIndex = data.find(delim, mimeTypeEndIndex + 1); + const descriptionLength = descriptionEndIndex - mimeTypeLength - 1; + this._description = data.toString( + descriptionLength, + this._encoding, + mimeTypeEndIndex + 1 + ); } else { - const ext = this._data.mid(pos, 3); - this._mimeType = Picture.getMimeTypeFromFilename(ext.toString(StringType.UTF8)); - pos += 3; - } - - this.type = this._data.get(pos++); - offset = this._data.find(delim, pos, delim.length); - } else if (ByteVector.equal(this._header.frameId, FrameType.GEOB)) { - // Retrieve and ID3v2 general encapsulated object - offset = this._data.find(ByteVector.getTextDelimiter(StringType.Latin1), pos); - if (offset < pos) { - return; + // Text encoding $xx + // Image format $xx xx xx + // Picture type $xx + // Description $00 (00) + // Picture data + const imageFormat = data.toString(3, StringType.Latin1, 1); + this._mimeType = Picture.getMimeTypeFromFilename(imageFormat); + + descriptionEndIndex = data.find(delim, 5); + const descriptionLength = descriptionEndIndex - 5; + this._description = data.toString(descriptionLength, this._encoding, 5); } - this._mimeType = this._data.toString(StringType.Latin1, pos, offset - pos); - pos = offset + 1; - - offset = this._data.find(delim, pos, delim.length); - if (offset < pos) { + this._data = data.mid(descriptionEndIndex + delim.length); + } else if (ByteVector.equal(this.frameId, FrameType.GEOB)) { + // Retrieve an ID3v2 generic encapsulated object + // Text encoding $xx + // MIME type $00 + // Filename $00 (00) + // Content description $00 (00) + // Encapsulated object + const mimeTypeEndIndex = data.find(ByteVector.getTextDelimiter(StringType.Latin1)); + if (mimeTypeEndIndex === -1) { return; } + const mimeTypeLength = mimeTypeEndIndex - 1; + this._mimeType = data.toString(mimeTypeLength, StringType.Latin1, 1); - this._filename = this._data.toString(this._encoding, pos, offset - pos); - pos = offset + delim.length; + const filenameEndIndex = data.find(delim, mimeTypeEndIndex + 1); + const filenameLength = filenameEndIndex - mimeTypeEndIndex - 1; + this._filename = data.toString(filenameLength, this._encoding, mimeTypeEndIndex + 1); - offset = this._data.find(delim, pos, delim.length); + descriptionEndIndex = data.find(delim, filenameEndIndex + delim.length); + const descriptionLength = descriptionEndIndex - filenameEndIndex - delim.length; + this._description = data.toString(descriptionLength, this._encoding, filenameEndIndex + delim.length); - this.type = PictureType.NotAPicture; + this._data = data.mid(descriptionEndIndex + delim.length); } else { - throw new Error("Invalid operation: bad frame type"); + // Unsupported + throw new Error("Unsupported: AttachmentFrame cannot be used for frame IDs other than GEOB or APIC"); } + } - if (offset < pos) { - return; - } + private parseFromRawPicture(): void { + // Indicate raw picture has been processed + const picture = this._rawPicture; + this._rawPicture = undefined; + + // Bring over values from the picture + this._data = ByteVector.fromByteVector(picture.data); + this._description = picture.description; + this._filename = picture.filename; + this._mimeType = picture.mimeType; + this._type = picture.type; - this._description = this._data.toString(this._encoding, pos, offset - pos); - pos = offset + delim.length; - this._data.removeRange(0, pos); + this._encoding = Id3v2TagSettings.defaultEncoding; + + // Switch the frame ID if we discovered the attachment isn't an image + if (this._type === PictureType.NotAPicture) { + this._header.frameId = FrameType.GEOB; + } } } diff --git a/src/id3v2/frames/frameFactory.ts b/src/id3v2/frames/frameFactory.ts index 229ed6b2..df04b634 100644 --- a/src/id3v2/frames/frameFactory.ts +++ b/src/id3v2/frames/frameFactory.ts @@ -20,6 +20,7 @@ import PrivateFrame from "./privateFrame"; import {EventTimeCodeFrame} from "./eventTimeCodeFrame"; import {UrlLinkFrame, UserUrlLinkFrame} from "./urlLinkFrame"; import {NotImplementedError} from "../../errors"; +import PictureLazy from "../../pictureLazy"; const customFrameCreators: Array<(data: ByteVector, offset: number, header: Id3v2FrameHeader, version: number) => Frame> = []; @@ -136,13 +137,9 @@ export default { ByteVector.equal(header.frameId, FrameTypes.APIC) || ByteVector.equal(header.frameId, FrameTypes.GEOB) ) { + const picture = PictureLazy.fromFile(file.fileAbstraction, filePosition, offset - filePosition); return { - frame: AttachmentFrame.fromFile( - file.fileAbstraction, - filePosition, - offset - filePosition, - header - ), + frame: AttachmentFrame.fromPicture(picture), offset: offset }; } diff --git a/test/id3v2/attachmentsFrameTests.ts b/test/id3v2/attachmentsFrameTests.ts index 4ddcc7a4..e2434e35 100644 --- a/test/id3v2/attachmentsFrameTests.ts +++ b/test/id3v2/attachmentsFrameTests.ts @@ -17,183 +17,4 @@ import {IFileAbstraction} from "../../src/fileAbstraction"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -// Test constants -const description = "foo"; -const filename = "foo.bar"; -const mimeType = "foo/bar"; -const picType = PictureType.ColoredFish; -const rawHeaderBytes = [ - 0x41, 0x50, 0x49, 0x43, // APIC - 0x09, 0x00, 0x00, 0x00, // 9 bytes of data - 0x00, 0x00 -]; -const rawHeader = ByteVector.concatenate( - new Uint8Array(rawHeaderBytes), - TestConstants.testByteVector -); - -const testHeader = new Id3v2FrameHeader(FrameTypes.APIC, 3); - -@suite(timeout(3000), slow(1000)) -class Id3v2_AttachmentFrame_FromFileTests { - @test - public falsyAbstraction() { - // Act/Assert - assert.throws(() => { AttachmentFrame.fromFile(undefined, 0, 10, testHeader); }); - assert.throws(() => { AttachmentFrame.fromFile(null, 0, 10, testHeader); }); - } - - @test - public illegalOffset() { - // Arrange - const mockAbstraction = TypeMoq.Mock.ofType(); - - // Act/Assert - assert.throws(() => { AttachmentFrame.fromFile(mockAbstraction.object, -1, 10, testHeader); }); - assert.throws(() => { AttachmentFrame.fromFile(mockAbstraction.object, 1.5, 10, testHeader); }); - assert.throws(() => { - AttachmentFrame.fromFile(mockAbstraction.object, Number.MAX_SAFE_INTEGER + 1, 10, testHeader); - }); - } - - @test - public illegalSize() { - // Arrange - const mockAbstraction = TypeMoq.Mock.ofType(); - - // Act/Assert - assert.throws(() => { AttachmentFrame.fromFile(mockAbstraction.object, 0, -10, testHeader); }); - assert.throws(() => { AttachmentFrame.fromFile(mockAbstraction.object, 0, 1.5, testHeader); }); - assert.throws(() => { - AttachmentFrame.fromFile(mockAbstraction.object, 0, Number.MAX_SAFE_INTEGER + 1, testHeader); - }); - } - - @test - public falsyHeader() { - // Arrange - const mockAbstraction = TypeMoq.Mock.ofType(); - - // Act/Assert - assert.throws(() => { AttachmentFrame.fromFile(mockAbstraction.object, 0, 10, undefined); }); - assert.throws(() => { AttachmentFrame.fromFile(mockAbstraction.object, 0, 10, null); }); - } - - @test - public validParams() { - // Arrange - const mockAbstraction = TypeMoq.Mock.ofType(); - - // Act - const frame = AttachmentFrame.fromFile(mockAbstraction.object, 0, -1, testHeader); - - // Assert - Make sure file wasn't read yet - mockAbstraction.verify((fa) => fa.readStream, TypeMoq.Times.never()); - } -} - -@suite(timeout(3000), slow(1000)) -class Id3v2_AttachmentFrame_OffsetRawDataTests { - @test - public falsyData() { - // Act/Assert - assert.throws(() => { AttachmentFrame.fromOffsetRawData(undefined, 0, testHeader); }); - assert.throws(() => { AttachmentFrame.fromOffsetRawData(null, 0, testHeader); }); - } - - @test - public invalidOffset() { - // Act/Assert - assert.throws(() => { AttachmentFrame.fromOffsetRawData(TestConstants.testByteVector, -1, testHeader); }); - assert.throws(() => { AttachmentFrame.fromOffsetRawData(TestConstants.testByteVector, 1.5, testHeader); }); - assert.throws(() => { - AttachmentFrame.fromOffsetRawData(TestConstants.testByteVector, Number.MAX_SAFE_INTEGER + 1, testHeader); - }); - } - - @test - public falsyHeader() { - // Act/Assert - assert.throws(() => { AttachmentFrame.fromOffsetRawData(TestConstants.testByteVector, 0, undefined); }); - assert.throws(() => { AttachmentFrame.fromOffsetRawData(TestConstants.testByteVector, 0, null); }); - } - - @test - public dataTooShort() { - // Act/Assert - // @TODO - } - - @test - public validParams() { - // Arrange - // @TODO - } -} - -@suite(timeout(3000), slow(1000)) -class Id3v2_AttachmentFrame_FromPictureTests { - @test - public falsyPicture() { - // Act/Assert - assert.throws(() => { AttachmentFrame.fromPicture(undefined); }); - assert.throws(() => { AttachmentFrame.fromPicture(null); }); - } - - @test - public validPicture() { - // Arrange - const pictureMoq = TypeMoq.Mock.ofType(); - pictureMoq.setup((p) => p.type).returns(() => picType); - pictureMoq.setup((p) => p.mimeType).returns(() => mimeType); - pictureMoq.setup((p) => p.filename).returns(() => filename); - pictureMoq.setup((p) => p.description).returns(() => description); - pictureMoq.setup((p) => p.data).returns(() => TestConstants.testByteVector); - - // Act - const output = AttachmentFrame.fromPicture(pictureMoq.object); - - // Assert - assert.ok(output); - assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.APIC)); - - assert.strictEqual(output.data, TestConstants.testByteVector); - assert.strictEqual(output.description, description); - assert.strictEqual(output.filename, filename); - assert.strictEqual(output.frameClassType, FrameClassType.AttachmentFrame); - assert.isTrue(output.isLoaded); - assert.strictEqual(output.mimeType, mimeType); - assert.strictEqual(output.textEncoding, Id3v2TagSettings.defaultEncoding); - } -} - -@suite(timeout(3000), slow(1000)) -class Id3v2_AttachmentFrame_FromRawDataTests { - @test - public falsyData() { - // Act/Assert - assert.throws(() => { AttachmentFrame.fromRawData(undefined, 3); }); - assert.throws(() => { AttachmentFrame.fromRawData(null, 3); }); - } - - @test - public invalidVersion() { - // Act/Assert - assert.throws(() => { AttachmentFrame.fromRawData(TestConstants.testByteVector, -1); }); - assert.throws(() => { AttachmentFrame.fromRawData(TestConstants.testByteVector, 1.5); }); - assert.throws(() => { AttachmentFrame.fromRawData(TestConstants.testByteVector, 1024); }); - } - - @test - public validParams() { - // Act - const frame = AttachmentFrame.fromRawData(rawHeader, 3); - - // Assert - assert.ok(frame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.APIC)); - - assert.isTrue(ByteVector.equal(frame.data, TestConstants.testByteVector)); - } -} From a06f2256ed43e3e4d671d078bc504b9f36161eed Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Mon, 27 Jan 2020 23:21:53 -0500 Subject: [PATCH 34/71] WIP1 --- src/id3v2/frameIdentifiers.ts | 295 ++++++++++++++++++ src/id3v2/frameTypes.ts | 71 ----- src/id3v2/frames/attachmentFrame.ts | 2 +- src/id3v2/frames/commentsFrame.ts | 2 +- src/id3v2/frames/eventTimeCodeFrame.ts | 2 +- src/id3v2/frames/frameFactory.ts | 2 +- src/id3v2/frames/frameHeader.ts | 2 +- src/id3v2/frames/musicCdIdentifierFrame.ts | 2 +- src/id3v2/frames/playCountFrame.ts | 2 +- src/id3v2/frames/popularimeterFrame.ts | 2 +- src/id3v2/frames/privateFrame.ts | 2 +- src/id3v2/frames/relativeVolumeFrame.ts | 2 +- src/id3v2/frames/synchronizedLyricsFrame.ts | 2 +- src/id3v2/frames/termsOfUseFrame.ts | 2 +- src/id3v2/frames/textInformationFrame.ts | 2 +- src/id3v2/frames/uniqueFileIdentifierFrame.ts | 2 +- src/id3v2/frames/unsynchronizedLyricsFrame.ts | 2 +- src/id3v2/frames/urlLinkFrame.ts | 2 +- src/id3v2/id3v2Tag.ts | 150 +++++---- test/id3v2/attachmentsFrameTests.ts | 2 +- test/id3v2/commentsFrameTests.ts | 2 +- test/id3v2/eventTimeCodeFrameTests.ts | 2 +- test/id3v2/frameConstructorTests.ts | 2 +- test/id3v2/musicCdIdentifierFrameTests.ts | 2 +- test/id3v2/playCountFrameTests.ts | 2 +- test/id3v2/popularimeterFrameTests.ts | 2 +- test/id3v2/privateFrameTests.ts | 2 +- test/id3v2/relativeVolumeFrameTests.ts | 2 +- test/id3v2/synchronizedLyricsFrameTests.ts | 2 +- test/id3v2/termsOfUseFrameTests.ts | 2 +- test/id3v2/textInformationFrameTests.ts | 2 +- test/id3v2/uniqueFileIdentifierFrameTests.ts | 2 +- test/id3v2/unknownFrameTests.ts | 2 +- test/id3v2/unsynchronizedLyricsFrameTests.ts | 2 +- test/id3v2/urlLinkFrameTests.ts | 2 +- test/id3v2/userTextInformationFrameTests.ts | 2 +- test/id3v2/userUrlLinkFrameTests.ts | 2 +- 37 files changed, 403 insertions(+), 181 deletions(-) create mode 100644 src/id3v2/frameIdentifiers.ts delete mode 100644 src/id3v2/frameTypes.ts diff --git a/src/id3v2/frameIdentifiers.ts b/src/id3v2/frameIdentifiers.ts new file mode 100644 index 00000000..f9690ef8 --- /dev/null +++ b/src/id3v2/frameIdentifiers.ts @@ -0,0 +1,295 @@ +import {ByteVector, StringType} from "../byteVector"; +import {Guards} from "../utils"; + +/** + * @summary Represents the identifier of a frame, depending on the version this may be 3 or 4 + * bytes. Provides a simple way to switch between the identifiers used for different versions. + * @description This class is implemented in an attempt to unify frame identifiers, make it easy to + * switch versions, find frames between tags, and determine which frames are supported on which + * version of ID3v2. + * If you have a death wish, you can take your life into your own hands and construct your own + * FrameIdentifier for use in non-standard frames. This is VERY STRONGLY NOT ADVISED. Not only + * will you be breaking the ID3v2 standard making your frame not portable, but you will also + * have to ensure the FrameIdentifier instance you create is used everywhere the frame + * identifier is used. + * To make implementation and less memory intensive, FrameIdentifier instances for built-in + * frame identifiers are statically created and reused. This allows usage of the `===` to + * compare instances because they should always be the same. + */ +export class FrameIdentifier { + private versionTable: {[key: number]: ByteVector} = { + 2: undefined, + 3: undefined, + 4: undefined + }; + + public constructor(v4: string, v3: string, v2: string) { + this.versionTable[4] = v4 + ? ByteVector.fromString(v4, StringType.Latin1, undefined, true) + : undefined; + this.versionTable[4] = v3 + ? (v3 === v4 ? this.versionTable[4] : ByteVector.fromString(v3, StringType.Latin1, undefined, true)) + : undefined; + this.versionTable[2] = v2 + ? ByteVector.fromString(v2, StringType.Latin1, undefined, true) + : undefined; + } + + public render(version: number): ByteVector { + Guards.byte(version, "version"); + Guards.betweenInclusive(version, 2, 4, "version"); + if (!this.versionTable[version]) { + const newest = this.versionTable[4] || this.versionTable[3] || this.versionTable[2]; + throw new Error(`Frame ${newest} is not supported in ID3v2 version ${version}`); + } + + return this.versionTable[version]; + } +} + +export class FrameIdentifiers { + private static dict: {[key: string]: FrameIdentifier} = { + AENC: new FrameIdentifier("AENC", "AENC", "CRA"), // Audio encryption + APIC: new FrameIdentifier("APIC", "APIC", "PIC"), // Attached picture + ASPI: new FrameIdentifier("ASPI", undefined, undefined), // Audio seek point table + COMM: new FrameIdentifier("COMM", "COMM", "COM"), // Comments + COMR: new FrameIdentifier("COMR", "COMR", undefined), // Commercial frame + CRM : new FrameIdentifier(undefined, undefined, "CRM"), // Encrypted meta-frame + ENCR: new FrameIdentifier("ENCR", "ENCR", undefined), // Encryption method registration + EQU2: new FrameIdentifier("EQU2", "EQUA", undefined), // Equalization + ETCO: new FrameIdentifier("ETCO", "ETCO", "ETC"), // Event timing codes + GEOB: new FrameIdentifier("GEOB", "GEOB", "GEO"), // General encapsulated object + GRID: new FrameIdentifier("GRID", "GRID", undefined), // Group identification registration + LINK: new FrameIdentifier("LINK", "LINK", "LNK"), // Linked information + MCDI: new FrameIdentifier("MCDI", "MCDI", "MCI"), // Music CD identifier + MLLT: new FrameIdentifier("MLLT", "MLLT", "MLL"), // MPEG location lookup table + OWNE: new FrameIdentifier("OWNE", "OWNE", undefined), // Ownership frame + PCNT: new FrameIdentifier("PCNT", "PCNT", "CNT"), // Play count + POPM: new FrameIdentifier("POPM", "POPM", "POP"), // Popularimeter + POSS: new FrameIdentifier("POSS", "POSS", undefined), // Position synchronization frame + PRIV: new FrameIdentifier("PRIV", "PRIV", undefined), // Private frame + RBUF: new FrameIdentifier("RBUF", "RBUF", "BUF"), // Recommended buffer size + RVA2: new FrameIdentifier("RVA2", "RVAD", "RVA"), // Relative volume adjustment + RVRB: new FrameIdentifier("RVRB", "RVRB", "REV"), // Reverb + SEEK: new FrameIdentifier("SEEK", undefined, undefined), // Seek frame + SIGN: new FrameIdentifier("SIGN", undefined, undefined), // Signature frame + SYLT: new FrameIdentifier("SYLT", "SYLT", "SLT"), // Synchronized lyric/text + SYTC: new FrameIdentifier("SYTC", "SYTC", "STC"), // Synchronized tempo codes + TALB: new FrameIdentifier("TALB", "TALB", "TAL"), // Album/Movie/Show title + TBPM: new FrameIdentifier("TBPM", "TBPM", "TBP"), // BPM + TCOM: new FrameIdentifier("TCOM", "TCOM", "TCM"), // Composer + TCMP: new FrameIdentifier("TCMP", "TCMP", undefined), // iTunes only "compilation" flag + TCON: new FrameIdentifier("TCON", "TCON", "TCO"), // Content type + TCOP: new FrameIdentifier("TCOP", "TCOP", "TCR"), // Copyright message + TDAT: new FrameIdentifier(undefined, "TDAT", "TDA"), // Date + TDEN: new FrameIdentifier("TDEN", undefined, undefined), // Encoding time + TDLY: new FrameIdentifier("TDLY", "TDLY", "TDY"), // Playlist delay + TDOR: new FrameIdentifier("TDOR", "TORY", "TOR"), // Original release time + TDRC: new FrameIdentifier("TDRC", "TYER", "TYE"), // Recording time (v2.4/v2.3) / Year (v2.2) + TDRL: new FrameIdentifier("TDRL", undefined, undefined), // Release time + TDTG: new FrameIdentifier("TDTG", undefined, undefined), // Tagging time + TENC: new FrameIdentifier("TENC", "TENC", "TEN"), // Encoded by + TEXT: new FrameIdentifier("TEXT", "TEXT", "TXT"), // Lyricist/Text writer + TFLT: new FrameIdentifier("TFLT", "TFLT", "TFT"), // File type + TIME: new FrameIdentifier(undefined, "TIME", "TIM"), // Time + TIPL: new FrameIdentifier("TIPL", "IPLS", "IPL"), // Involved people list + TIT1: new FrameIdentifier("TIT1", "TIT1", "TT1"), // Content group description + TIT2: new FrameIdentifier("TIT2", "TIT2", "TT2"), // Title/songname/content description + TIT3: new FrameIdentifier("TIT3", "TIT3", "TT3"), // Subtitle/description refinement + TKEY: new FrameIdentifier("TKEY", "TKEY", "TKE"), // Initial key + TLAN: new FrameIdentifier("TLAN", "TLAN", "TLA"), // Language(s) + TLEN: new FrameIdentifier("TLEN", "TLEN", "TLE"), // Length + TMCL: new FrameIdentifier("TMCL", undefined, undefined), // Musician credit list + TMED: new FrameIdentifier("TMED", "TMED", "TMT"), // Media type + TMOO: new FrameIdentifier("TMOO", undefined, undefined), // Mood + TOAL: new FrameIdentifier("TOAL", "TOAL", "TOT"), // Original album/movie/show title + TOFN: new FrameIdentifier("TOFN", "TOFN", "TOF"), // Original filename + TOLY: new FrameIdentifier("TOLY", "TOLY", "TOL"), // Original lyricist(s)/text writer(s) + TOPE: new FrameIdentifier("TOPE", "TOPE", "TOA"), // Original artist(s)/performer(s) + TOWN: new FrameIdentifier("TOWN", "TOWN", undefined), // File owner/licensee + TPE1: new FrameIdentifier("TPE1", "TPE1", "TP1"), // Lead performer(s)/soloist(s) + TPE2: new FrameIdentifier("TPE2", "TPE2", "TP2"), // Band/orchestra/accompaniment + TPE3: new FrameIdentifier("TPE3", "TEP3", "TP3"), // Counductor/performer refinement + TPE4: new FrameIdentifier("TPE4", "TEP4", "TP4"), // Interpreted, remixed, or otherwise modified by + TPOS: new FrameIdentifier("TPOS", "TPOS", "TPA"), // Part of a set + TPRO: new FrameIdentifier("TPRO", undefined, undefined), // Produced notice + TPUB: new FrameIdentifier("TPUB", "TPUB", "TPB"), // Publisher + TRCK: new FrameIdentifier("TRCK", "TRCK", "TRK"), // Track number/position in set + TRDA: new FrameIdentifier(undefined, "TRDA", "TRD"), // Recording dates + TRSN: new FrameIdentifier("TRSN", "TRSN", undefined), // Internet radio station name + TRSO: new FrameIdentifier("TRSO", "TRSO", undefined), // Internet radio station owner + TSIZ: new FrameIdentifier(undefined, "TSIZ", "TSI"), // Size + TSOA: new FrameIdentifier("TSOA", undefined, undefined), // Album sort order + TSOP: new FrameIdentifier("TSOP", undefined, undefined), // Performer sort order + TSOT: new FrameIdentifier("TSOT", undefined, undefined), // Title sort order + TSRC: new FrameIdentifier("TSRC", "TSRC", "TRC"), // ISRC (International standard recording code) + TSSE: new FrameIdentifier("TSSE", "TSSE", "TSS"), // Software/hardware and setting used for encoding + TSST: new FrameIdentifier("TSST", undefined, undefined), // Set subtitle + TXXX: new FrameIdentifier("TXXX", "TXXX", "TXX"), // User defined text information frame + UFID: new FrameIdentifier("UFID", "UFID", "UFI"), // Unique file identifer + USER: new FrameIdentifier("USER", "USER", undefined), // Terms of use + USLT: new FrameIdentifier("USLT", "USLT", "ULT"), // Unsynchronised lyric/text transcription + WCOM: new FrameIdentifier("WCOM", "WCOM", "WCM"), // Commercial information URL + WCOP: new FrameIdentifier("WCOP", "WCOP", "WCP"), // Copyright/legal information URL + WOAF: new FrameIdentifier("WOAF", "WOAF", "WAF"), // Official audio file webpage URL + WOAR: new FrameIdentifier("WOAR", "WOAR", "WAR"), // Official artist/performer webpage URL + WOAS: new FrameIdentifier("WOAS", "WOAS", "WAS"), // Official audio source webpage URL + WORS: new FrameIdentifier("WORS", "WORS", undefined), // Official internet radio station homepage URL + WPAY: new FrameIdentifier("WPAY", "WPAY", undefined), // Payment URL + WPUB: new FrameIdentifier("WPUB", "WPUB", "WPB"), // Publishers official webpage URL + WXXX: new FrameIdentifier("WXXX", "WXXX", "WXX"), // User defined URL link frame + }; + + public static AENC = FrameIdentifiers.dict.AENC; + public static APIC = FrameIdentifiers.dict.APIC; + public static ASPI = FrameIdentifiers.dict.ASPI; + public static BUF = FrameIdentifiers.dict.RBUF; + public static CNT = FrameIdentifiers.dict.PCNT; + public static COM = FrameIdentifiers.dict.COMM; + public static COMM = FrameIdentifiers.dict.COMM; + public static COMR = FrameIdentifiers.dict.COMR; + public static CRA = FrameIdentifiers.dict.AENC; + public static CRM = FrameIdentifiers.dict.CRM; + public static ENCR = FrameIdentifiers.dict.ENCR; + public static EQU2 = FrameIdentifiers.dict.EQU2; + public static EQUA = FrameIdentifiers.dict.EQU2; + public static ETC = FrameIdentifiers.dict.ETCO; + public static ETCO = FrameIdentifiers.dict.ETCO; + public static GEO = FrameIdentifiers.dict.GEOB; + public static GEOB = FrameIdentifiers.dict.GEOB; + public static GRID = FrameIdentifiers.dict.GRID; + public static IPLS = FrameIdentifiers.dict.TIPL; + public static LINK = FrameIdentifiers.dict.LINK; + public static LNK = FrameIdentifiers.dict.LINK; + public static MCDI = FrameIdentifiers.dict.MCDI; + public static MCI = FrameIdentifiers.dict.MCDI; + public static MLL = FrameIdentifiers.dict.MLLT; + public static MLLT = FrameIdentifiers.dict.MLLT; + public static OWNE = FrameIdentifiers.dict.OWNE; + public static PCNT = FrameIdentifiers.dict.PCNT; + public static PIC = FrameIdentifiers.dict.APIC; + public static POP = FrameIdentifiers.dict.POPM; + public static POPM = FrameIdentifiers.dict.POPM; + public static POSS = FrameIdentifiers.dict.POSS; + public static PRIV = FrameIdentifiers.dict.PRIV; + public static RBUF = FrameIdentifiers.dict.RBUF; + public static REV = FrameIdentifiers.dict.RVRB; + public static RVA = FrameIdentifiers.dict.RVA2; + public static RVA2 = FrameIdentifiers.dict.RVA2; + public static RVRB = FrameIdentifiers.dict.RVRB; + public static SEEK = FrameIdentifiers.dict.SEEK; + public static SIGN = FrameIdentifiers.dict.SIGN; + public static SLT = FrameIdentifiers.dict.SYLT; + public static STC = FrameIdentifiers.dict.SYTC; + public static SYLT = FrameIdentifiers.dict.SYLT; + public static SYTC = FrameIdentifiers.dict.SYTC; + public static TAL = FrameIdentifiers.dict.TALB; + public static TALB = FrameIdentifiers.dict.TALB; + public static TBP = FrameIdentifiers.dict.TBPM; + public static TBPM = FrameIdentifiers.dict.TBPM; + public static TCM = FrameIdentifiers.dict.TCOM; + public static TCMP = FrameIdentifiers.dict.TCMP; + public static TCO = FrameIdentifiers.dict.TCON; + public static TCOM = FrameIdentifiers.dict.TCOM; + public static TCON = FrameIdentifiers.dict.TCON; + public static TCOP = FrameIdentifiers.dict.TCOP; + public static TCR = FrameIdentifiers.dict.TCOP; + public static TDA = FrameIdentifiers.dict.TDAT; + public static TDAT = FrameIdentifiers.dict.TDAT; + public static TDEN = FrameIdentifiers.dict.TDEN; + public static TDLY = FrameIdentifiers.dict.TDLY; + public static TDOR = FrameIdentifiers.dict.TDOR; + public static TDRC = FrameIdentifiers.dict.TDRC; + public static TDRL = FrameIdentifiers.dict.TDRL; + public static TDTG = FrameIdentifiers.dict.TDTG; + public static TDY = FrameIdentifiers.dict.TDLY; + public static TEN = FrameIdentifiers.dict.TENC; + public static TENC = FrameIdentifiers.dict.TENC; + public static TEXT = FrameIdentifiers.dict.TEXT; + public static TFLT = FrameIdentifiers.dict.TFLT; + public static TFT = FrameIdentifiers.dict.TFLT; + public static TIM = FrameIdentifiers.dict.TIME; + public static TIME = FrameIdentifiers.dict.TIME; + public static TIPL = FrameIdentifiers.dict.TIPL; + public static TIT1 = FrameIdentifiers.dict.TIT1; + public static TIT2 = FrameIdentifiers.dict.TIT2; + public static TIT3 = FrameIdentifiers.dict.TIT3; + public static TKE = FrameIdentifiers.dict.TKEY; + public static TKYE = FrameIdentifiers.dict.TKEY; + public static TLA = FrameIdentifiers.dict.TLAN; + public static TLAN = FrameIdentifiers.dict.TLAN; + public static TLE = FrameIdentifiers.dict.TLEN; + public static TLEN = FrameIdentifiers.dict.TLEN; + public static TMCL = FrameIdentifiers.dict.TMCL; + public static TMED = FrameIdentifiers.dict.TMED; + public static TMOO = FrameIdentifiers.dict.TMOO; + public static TMT = FrameIdentifiers.dict.TMED; + public static TOA = FrameIdentifiers.dict.TOPE; + public static TOAL = FrameIdentifiers.dict.TOAL; + public static TOF = FrameIdentifiers.dict.TOFN; + public static TOFN = FrameIdentifiers.dict.TOFN; + public static TOL = FrameIdentifiers.dict.TOLY; + public static TOLY = FrameIdentifiers.dict.TOLY; + public static TOPE = FrameIdentifiers.dict.TOPE; + public static TOR = FrameIdentifiers.dict.TDOR; + public static TORY = FrameIdentifiers.dict.TDOR; + public static TOT = FrameIdentifiers.dict.TOAL; + public static TOWN = FrameIdentifiers.dict.TOWN; + public static TP1 = FrameIdentifiers.dict.TPE1; + public static TP2 = FrameIdentifiers.dict.TPE2; + public static TP3 = FrameIdentifiers.dict.TPE3; + public static TP4 = FrameIdentifiers.dict.TPE4; + public static TPA = FrameIdentifiers.dict.TPOS; + public static TPB = FrameIdentifiers.dict.TPUB; + public static TPE1 = FrameIdentifiers.dict.TPE1; + public static TPE2 = FrameIdentifiers.dict.TPE2; + public static TPE3 = FrameIdentifiers.dict.TPE3; + public static TPE4 = FrameIdentifiers.dict.TPE4; + public static TPOS = FrameIdentifiers.dict.TPOS; + public static TPRO = FrameIdentifiers.dict.TPRO; + public static TPUB = FrameIdentifiers.dict.TPUB; + public static TRC = FrameIdentifiers.dict.TSRC; + public static TRCK = FrameIdentifiers.dict.TRCK; + public static TRD = FrameIdentifiers.dict.TRDA; + public static TRDA = FrameIdentifiers.dict.TRDA; + public static TRK = FrameIdentifiers.dict.TRCK; + public static TRSN = FrameIdentifiers.dict.TRSN; + public static TRSO = FrameIdentifiers.dict.TRSO; + public static TSI = FrameIdentifiers.dict.TSIZ; + public static TSIZ = FrameIdentifiers.dict.TSIZ; + public static TSOA = FrameIdentifiers.dict.TSOA; + public static TSOP = FrameIdentifiers.dict.TSOP; + public static TSOT = FrameIdentifiers.dict.TSOT; + public static TSRC = FrameIdentifiers.dict.TSRC; + public static TSS = FrameIdentifiers.dict.TSSE; + public static TSSE = FrameIdentifiers.dict.TSSE; + public static TSST = FrameIdentifiers.dict.TSST; + public static TT1 = FrameIdentifiers.dict.TIT1; + public static TT2 = FrameIdentifiers.dict.TIT2; + public static TT3 = FrameIdentifiers.dict.TIT3; + public static TXT = FrameIdentifiers.dict.TEXT; + public static TXX = FrameIdentifiers.dict.TXXX; + public static TXXX = FrameIdentifiers.dict.TXXX; + public static TYE = FrameIdentifiers.dict.TDRC; + public static TYER = FrameIdentifiers.dict.TDRC; + public static UFI = FrameIdentifiers.dict.UFID; + public static UFID = FrameIdentifiers.dict.UFID; + public static ULT = FrameIdentifiers.dict.USLT; + public static USER = FrameIdentifiers.dict.USER; + public static USLT = FrameIdentifiers.dict.USLT; + public static WAF = FrameIdentifiers.dict.WOAF; + public static WAR = FrameIdentifiers.dict.WOAR; + public static WAS = FrameIdentifiers.dict.WOAS; + public static WCM = FrameIdentifiers.dict.WCOM; + public static WCOM = FrameIdentifiers.dict.WCOM; + public static WCOP = FrameIdentifiers.dict.WCOP; + public static WCP = FrameIdentifiers.dict.WCOP; + public static WOAF = FrameIdentifiers.dict.WOAF; + public static WOAR = FrameIdentifiers.dict.WOAR; + public static WORS = FrameIdentifiers.dict.WORS; + public static WPAY = FrameIdentifiers.dict.WPAY; + public static WPB = FrameIdentifiers.dict.WPUB; + public static WPUB = FrameIdentifiers.dict.WPUB; + public static WXX = FrameIdentifiers.dict.WXXX; + public static WXXX = FrameIdentifiers.dict.WXXX; +} diff --git a/src/id3v2/frameTypes.ts b/src/id3v2/frameTypes.ts deleted file mode 100644 index 35e18791..00000000 --- a/src/id3v2/frameTypes.ts +++ /dev/null @@ -1,71 +0,0 @@ -import {ByteVector, StringType} from "../byteVector"; - -/** - * @summary Provides references to different frame types used by the library. - * @description This class is used to severely reduce the number of times these types are created in - * {@see Id3v2Tag}, greatly improving the speed at which warm files are read. It is, however, - * not necessary for external users to use this class. While the library may use - * `getTextAsString(FrameType.TIT2);` an external user could use `tag.getTextAsString("TIT2");` - * with the same result. - */ -export default { - APIC: ByteVector.fromString("APIC", StringType.UTF8, undefined, true), - COMM: ByteVector.fromString("COMM", StringType.UTF8, undefined, true), - EQUA: ByteVector.fromString("EQUA", StringType.UTF8, undefined, true), - ETCO: ByteVector.fromString("ETCO", StringType.UTF8, undefined, true), - GEOB: ByteVector.fromString("GEOB", StringType.UTF8, undefined, true), - MCDI: ByteVector.fromString("MCDI", StringType.UTF8, undefined, true), - PCNT: ByteVector.fromString("PCNT", StringType.UTF8, undefined, true), - POPM: ByteVector.fromString("POPM", StringType.UTF8, undefined, true), - PRIV: ByteVector.fromString("PRIV", StringType.UTF8, undefined, true), - RVA2: ByteVector.fromString("RVA2", StringType.UTF8, undefined, true), - RVAD: ByteVector.fromString("RVAD", StringType.UTF8, undefined, true), - SYLT: ByteVector.fromString("SYLT", StringType.UTF8, undefined, true), - TALB: ByteVector.fromString("TALB", StringType.UTF8, undefined, true), - TBPM: ByteVector.fromString("TBPM", StringType.UTF8, undefined, true), - TCMP: ByteVector.fromString("TCMP", StringType.UTF8, undefined, true), - TCOM: ByteVector.fromString("TCOM", StringType.UTF8, undefined, true), - TCON: ByteVector.fromString("TCON", StringType.UTF8, undefined, true), - TCOP: ByteVector.fromString("TCOP", StringType.UTF8, undefined, true), - TDAT: ByteVector.fromString("TDAT", StringType.UTF8, undefined, true), - TDRC: ByteVector.fromString("TDRC", StringType.UTF8, undefined, true), - TDTG: ByteVector.fromString("TDTG", StringType.UTF8, undefined, true), - TEXT: ByteVector.fromString("TEXT", StringType.UTF8, undefined, true), - TIME: ByteVector.fromString("TIME", StringType.UTF8, undefined, true), - TIT1: ByteVector.fromString("TIT1", StringType.UTF8, undefined, true), - TIT2: ByteVector.fromString("TIT2", StringType.UTF8, undefined, true), - TIT3: ByteVector.fromString("TIT3", StringType.UTF8, undefined, true), - TKEY: ByteVector.fromString("TKEY", StringType.UTF8, undefined, true), - TMCL: ByteVector.fromString("TMCL", StringType.UTF8, undefined, true), - TOLY: ByteVector.fromString("TOLY", StringType.UTF8, undefined, true), - TOPE: ByteVector.fromString("TOPE", StringType.UTF8, undefined, true), - TPE1: ByteVector.fromString("TPE1", StringType.UTF8, undefined, true), - TPE2: ByteVector.fromString("TPE2", StringType.UTF8, undefined, true), - TPE3: ByteVector.fromString("TPE3", StringType.UTF8, undefined, true), - TPE4: ByteVector.fromString("TPE4", StringType.UTF8, undefined, true), - TPOS: ByteVector.fromString("TPOS", StringType.UTF8, undefined, true), - TPUB: ByteVector.fromString("TPUB", StringType.UTF8, undefined, true), - TRCK: ByteVector.fromString("TRCK", StringType.UTF8, undefined, true), - TRDA: ByteVector.fromString("TRDA", StringType.UTF8, undefined, true), - TSIZ: ByteVector.fromString("TSIZ", StringType.UTF8, undefined, true), - TSO2: ByteVector.fromString("TSO2", StringType.UTF8, undefined, true), // Album Artist Sort Frame - TSOA: ByteVector.fromString("TSOA", StringType.UTF8, undefined, true), // Album Title Sort Frame - TSOC: ByteVector.fromString("TSOC", StringType.UTF8, undefined, true), // Composer Sort Frame - TSOP: ByteVector.fromString("TSOP", StringType.UTF8, undefined, true), // Performer Sort Frame - TSOT: ByteVector.fromString("TSOT", StringType.UTF8, undefined, true), // Track Title Sort Frame - TSRC: ByteVector.fromString("TSRC", StringType.UTF8, undefined, true), - TXXX: ByteVector.fromString("TXXX", StringType.UTF8, undefined, true), - TYER: ByteVector.fromString("TYER", StringType.UTF8, undefined, true), - UFID: ByteVector.fromString("UFID", StringType.UTF8, undefined, true), - USER: ByteVector.fromString("USER", StringType.UTF8, undefined, true), - USLT: ByteVector.fromString("USLT", StringType.UTF8, undefined, true), - WCOM: ByteVector.fromString("WCOM", StringType.UTF8, undefined, true), - WCOP: ByteVector.fromString("WCOP", StringType.UTF8, undefined, true), - WOAF: ByteVector.fromString("WOAF", StringType.UTF8, undefined, true), - WOAR: ByteVector.fromString("WOAR", StringType.UTF8, undefined, true), - WOAS: ByteVector.fromString("WOAS", StringType.UTF8, undefined, true), - WORS: ByteVector.fromString("WORS", StringType.UTF8, undefined, true), - WPAY: ByteVector.fromString("WPAY", StringType.UTF8, undefined, true), - WPUB: ByteVector.fromString("WPUB", StringType.UTF8, undefined, true), - WXXX: ByteVector.fromString("WXXX", StringType.UTF8, undefined, true) -}; diff --git a/src/id3v2/frames/attachmentFrame.ts b/src/id3v2/frames/attachmentFrame.ts index df348894..c63e5da3 100644 --- a/src/id3v2/frames/attachmentFrame.ts +++ b/src/id3v2/frames/attachmentFrame.ts @@ -1,4 +1,4 @@ -import FrameType from "../frameTypes"; +import FrameType from "../frameIdentifiers"; import Id3v2TagSettings from "../id3v2TagSettings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; diff --git a/src/id3v2/frames/commentsFrame.ts b/src/id3v2/frames/commentsFrame.ts index d490cee0..7aec1849 100644 --- a/src/id3v2/frames/commentsFrame.ts +++ b/src/id3v2/frames/commentsFrame.ts @@ -1,4 +1,4 @@ -import FrameTypes from "../frameTypes"; +import FrameTypes from "../frameIdentifiers"; import Id3v2TagSettings from "../id3v2TagSettings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; diff --git a/src/id3v2/frames/eventTimeCodeFrame.ts b/src/id3v2/frames/eventTimeCodeFrame.ts index 453ad657..75abc70f 100644 --- a/src/id3v2/frames/eventTimeCodeFrame.ts +++ b/src/id3v2/frames/eventTimeCodeFrame.ts @@ -1,4 +1,4 @@ -import FrameTypes from "../frameTypes"; +import FrameTypes from "../frameIdentifiers"; import {ByteVector} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameFlags, Id3v2FrameHeader} from "./frameHeader"; diff --git a/src/id3v2/frames/frameFactory.ts b/src/id3v2/frames/frameFactory.ts index df04b634..465085eb 100644 --- a/src/id3v2/frames/frameFactory.ts +++ b/src/id3v2/frames/frameFactory.ts @@ -1,4 +1,4 @@ -import FrameTypes from "../frameTypes"; +import FrameTypes from "../frameIdentifiers"; import {ByteVector} from "../../byteVector"; import {Id3v2FrameFlags, Id3v2FrameHeader} from "./frameHeader"; import {File} from "../../file"; diff --git a/src/id3v2/frames/frameHeader.ts b/src/id3v2/frames/frameHeader.ts index f6f74c5f..6dbe33e5 100644 --- a/src/id3v2/frames/frameHeader.ts +++ b/src/id3v2/frames/frameHeader.ts @@ -1,4 +1,4 @@ -import FrameType from "../frameTypes"; +import FrameType from "../frameIdentifiers"; import SyncData from "../syncData"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError, NotImplementedError} from "../../errors"; diff --git a/src/id3v2/frames/musicCdIdentifierFrame.ts b/src/id3v2/frames/musicCdIdentifierFrame.ts index 342c9786..d99ca7be 100644 --- a/src/id3v2/frames/musicCdIdentifierFrame.ts +++ b/src/id3v2/frames/musicCdIdentifierFrame.ts @@ -1,4 +1,4 @@ -import FrameTypes from "../frameTypes"; +import FrameTypes from "../frameIdentifiers"; import {ByteVector} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; diff --git a/src/id3v2/frames/playCountFrame.ts b/src/id3v2/frames/playCountFrame.ts index 25cc79db..f43d31ca 100644 --- a/src/id3v2/frames/playCountFrame.ts +++ b/src/id3v2/frames/playCountFrame.ts @@ -1,5 +1,5 @@ import * as BigInt from "big-integer"; -import FrameTypes from "../frameTypes"; +import FrameTypes from "../frameIdentifiers"; import {ByteVector} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; diff --git a/src/id3v2/frames/popularimeterFrame.ts b/src/id3v2/frames/popularimeterFrame.ts index f40424ed..d46174bd 100644 --- a/src/id3v2/frames/popularimeterFrame.ts +++ b/src/id3v2/frames/popularimeterFrame.ts @@ -1,5 +1,5 @@ import * as BigInt from "big-integer"; -import FrameTypes from "../frameTypes"; +import FrameTypes from "../frameIdentifiers"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; import {Frame, FrameClassType} from "./frame"; diff --git a/src/id3v2/frames/privateFrame.ts b/src/id3v2/frames/privateFrame.ts index 2b361dc8..2c675fff 100644 --- a/src/id3v2/frames/privateFrame.ts +++ b/src/id3v2/frames/privateFrame.ts @@ -1,4 +1,4 @@ -import FrameTypes from "../frameTypes"; +import FrameTypes from "../frameIdentifiers"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError, NotImplementedError} from "../../errors"; import {Frame, FrameClassType} from "./frame"; diff --git a/src/id3v2/frames/relativeVolumeFrame.ts b/src/id3v2/frames/relativeVolumeFrame.ts index 4ef461fd..8d1f6b6d 100644 --- a/src/id3v2/frames/relativeVolumeFrame.ts +++ b/src/id3v2/frames/relativeVolumeFrame.ts @@ -1,5 +1,5 @@ import * as BigInt from "big-integer"; -import FrameTypes from "../frameTypes"; +import FrameTypes from "../frameIdentifiers"; import {ByteVector, StringType} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; diff --git a/src/id3v2/frames/synchronizedLyricsFrame.ts b/src/id3v2/frames/synchronizedLyricsFrame.ts index 95a7a739..93c9f1f9 100644 --- a/src/id3v2/frames/synchronizedLyricsFrame.ts +++ b/src/id3v2/frames/synchronizedLyricsFrame.ts @@ -1,4 +1,4 @@ -import FrameTypes from "../frameTypes"; +import FrameTypes from "../frameIdentifiers"; import Id3v2TagSettings from "../id3v2TagSettings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; diff --git a/src/id3v2/frames/termsOfUseFrame.ts b/src/id3v2/frames/termsOfUseFrame.ts index e9ac0fa6..4a81f305 100644 --- a/src/id3v2/frames/termsOfUseFrame.ts +++ b/src/id3v2/frames/termsOfUseFrame.ts @@ -1,4 +1,4 @@ -import FrameTypes from "../frameTypes"; +import FrameTypes from "../frameIdentifiers"; import Id3v2TagSettings from "../id3v2TagSettings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; diff --git a/src/id3v2/frames/textInformationFrame.ts b/src/id3v2/frames/textInformationFrame.ts index ffbf5049..6c039d73 100644 --- a/src/id3v2/frames/textInformationFrame.ts +++ b/src/id3v2/frames/textInformationFrame.ts @@ -1,4 +1,4 @@ -import FrameTypes from "../frameTypes"; +import FrameTypes from "../frameIdentifiers"; import Genres from "../../genres"; import Id3v2TagSettings from "../id3v2TagSettings"; import {ByteVector, StringType} from "../../byteVector"; diff --git a/src/id3v2/frames/uniqueFileIdentifierFrame.ts b/src/id3v2/frames/uniqueFileIdentifierFrame.ts index 5055984a..6fc05e3e 100644 --- a/src/id3v2/frames/uniqueFileIdentifierFrame.ts +++ b/src/id3v2/frames/uniqueFileIdentifierFrame.ts @@ -1,4 +1,4 @@ -import FrameTypes from "../frameTypes"; +import FrameTypes from "../frameIdentifiers"; import {ByteVector, StringType} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; diff --git a/src/id3v2/frames/unsynchronizedLyricsFrame.ts b/src/id3v2/frames/unsynchronizedLyricsFrame.ts index d7020fec..433b0266 100644 --- a/src/id3v2/frames/unsynchronizedLyricsFrame.ts +++ b/src/id3v2/frames/unsynchronizedLyricsFrame.ts @@ -1,4 +1,4 @@ -import FrameTypes from "../frameTypes"; +import FrameTypes from "../frameIdentifiers"; import Id3v2TagSettings from "../id3v2TagSettings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; diff --git a/src/id3v2/frames/urlLinkFrame.ts b/src/id3v2/frames/urlLinkFrame.ts index 2e667471..e770f665 100644 --- a/src/id3v2/frames/urlLinkFrame.ts +++ b/src/id3v2/frames/urlLinkFrame.ts @@ -1,4 +1,4 @@ -import FrameTypes from "../frameTypes"; +import FrameTypes from "../frameIdentifiers"; import {ByteVector, StringType} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; diff --git a/src/id3v2/id3v2Tag.ts b/src/id3v2/id3v2Tag.ts index 29ac3c73..26e75450 100644 --- a/src/id3v2/id3v2Tag.ts +++ b/src/id3v2/id3v2Tag.ts @@ -1,17 +1,19 @@ import * as DateFormat from "dateformat"; + import AttachmentFrame from "./frames/attachmentFrame"; import CommentsFrame from "./frames/commentsFrame"; import ExtendedHeader from "./extendedHeader"; import FrameFactory from "./frames/frameFactory"; -import FrameTypes from "./frameTypes"; import Genres from "../genres"; import Header from "./header"; +import Id3v2TagSettings from "./id3v2TagSettings"; import SyncData from "./syncData"; import UniqueFileIdentifierFrame from "./frames/uniqueFileIdentifierFrame"; import UnsynchronizedLyricsFrame from "./frames/unsynchronizedLyricsFrame"; import {ByteVector, StringType} from "../byteVector"; import {File, FileAccessMode, ReadStyle} from "../file"; import {Frame, FrameClassType} from "./frames/frame"; +import {FrameIdentifier, FrameIdentifiers} from "./frameIdentifiers"; import {Id3v2FrameFlags, Id3v2FrameHeader} from "./frames/frameHeader"; import {HeaderFlags} from "./headerFlags"; import {IPicture} from "../picture"; @@ -19,7 +21,6 @@ import {Tag, TagTypes} from "../tag"; import {TextInformationFrame, UserTextInformationFrame} from "./frames/textInformationFrame"; import {UrlLinkFrame} from "./frames/urlLinkFrame"; import {Guards} from "../utils"; -import Id3v2TagSettings from "./id3v2TagSettings"; export default class Id3v2Tag extends Tag { private static _language: string = undefined; // @TODO: Use the os-locale module to supply a @@ -77,7 +78,7 @@ export default class Id3v2Tag extends Tag { * feature of the Apple iPod and iTunes products. */ public get isCompilation(): boolean { - const val = this.getTextAsString(FrameTypes.TCMP); + const val = this.getTextAsString(FrameIdentifiers.TCMP); return val && val !== "0"; } /** @@ -87,7 +88,7 @@ export default class Id3v2Tag extends Tag { * @param value Whether or not the album described by the current instance is a compilation */ public set isCompilation(value: boolean) { - this.setTextFrame(FrameTypes.TCMP, value ? "1" : undefined); + this.setTextFrame(FrameIdentifiers.TCMP, value ? "1" : undefined); } /** @@ -117,22 +118,22 @@ export default class Id3v2Tag extends Tag { * @inheritDoc * From TIT2 frame */ - public get title(): string { return this.getTextAsString(FrameTypes.TIT2); } + public get title(): string { return this.getTextAsString(FrameIdentifiers.TIT2); } /** * @inheritDoc * Stored in TIT2 frame */ - public set title(value: string) { this.setTextFrame(FrameTypes.TIT2, value); } + public set title(value: string) { this.setTextFrame(FrameIdentifiers.TIT2, value); } /** @inheritDoc via TSOT frame */ - get titleSort(): string { return this.getTextAsString(FrameTypes.TSOT); } + get titleSort(): string { return this.getTextAsString(FrameIdentifiers.TSOT); } /** @inheritDoc via TSOT frame */ - set titleSort(value: string) { this.setTextFrame(FrameTypes.TSOT, value); } + set titleSort(value: string) { this.setTextFrame(FrameIdentifiers.TSOT, value); } /** @inheritDoc via TIT3 frame */ - get subtitle(): string { return this.getTextAsString(FrameTypes.TIT3); } + get subtitle(): string { return this.getTextAsString(FrameIdentifiers.TIT3); } /** @inheritDoc via TIT3 frame */ - set subtitle(value: string) { this.setTextFrame(FrameTypes.TIT3, value); } + set subtitle(value: string) { this.setTextFrame(FrameIdentifiers.TIT3, value); } /** @inheritDoc via user text frame "description" */ get description(): string { return this.getUserTextAsString("Description"); } @@ -140,14 +141,14 @@ export default class Id3v2Tag extends Tag { set description(value: string) { this.setUserTextAsString("Description", value); } /** @inheritDoc via TPE1 frame */ - get performers(): string[] { return this.getTextAsArray(FrameTypes.TPE1); } + get performers(): string[] { return this.getTextAsArray(FrameIdentifiers.TPE1); } /** @inheritDoc via TPE1 frame */ - set performers(value: string[]) { this.setTextFrame(FrameTypes.TPE1, ...value); } + set performers(value: string[]) { this.setTextFrame(FrameIdentifiers.TPE1, ...value); } /** @inheritDoc via TSOP frame */ - get performersSort(): string[] { return this.getTextAsArray(FrameTypes.TSOP); } + get performersSort(): string[] { return this.getTextAsArray(FrameIdentifiers.TSOP); } /** @inheritDoc via TSOP frame */ - set performersSort(value: string[]) { this.setTextFrame(FrameTypes.TSOP, ...value); } + set performersSort(value: string[]) { this.setTextFrame(FrameIdentifiers.TSOP, ...value); } /** @inheritDoc via TMCL frame */ get performersRole(): string[] { @@ -157,7 +158,7 @@ export default class Id3v2Tag extends Tag { if (!perfRef) { return []; } // Map the instruments to the performers - const map = this.getTextAsArray(FrameTypes.TMCL); + const map = this.getTextAsArray(FrameIdentifiers.TMCL); this._performersRole = []; for (let i = 0; i + 1 < map.length; i += 2) { const inst = map[i]; @@ -187,34 +188,34 @@ export default class Id3v2Tag extends Tag { set performersRole(value: string[]) { this._performersRole = value || []; } /** @inheritDoc via TSO2 frame */ - get albumArtists(): string[] { return this.getTextAsArray(FrameTypes.TSO2); } + get albumArtists(): string[] { return this.getTextAsArray(FrameIdentifiers.TSO2); } /** @inheritDoc via TSO2 frame */ - set albumArtists(value: string[]) { this.setTextFrame(FrameTypes.TSO2, ...value); } + set albumArtists(value: string[]) { this.setTextFrame(FrameIdentifiers.TSO2, ...value); } /** @inheritDoc via TPE2 frame */ - get albumArtistsSort(): string[] { return this.getTextAsArray(FrameTypes.TPE2); } + get albumArtistsSort(): string[] { return this.getTextAsArray(FrameIdentifiers.TPE2); } /** @inheritDoc via TPE2 frame */ - set albumArtistsSort(value: string[]) { this.setTextFrame(FrameTypes.TPE2, ...value); } + set albumArtistsSort(value: string[]) { this.setTextFrame(FrameIdentifiers.TPE2, ...value); } /** @inheritDoc via TCOM frame */ - get composers(): string[] { return this.getTextAsArray(FrameTypes.TCOM); } + get composers(): string[] { return this.getTextAsArray(FrameIdentifiers.TCOM); } /** @inheritDoc via TCOM frame */ - set composers(value: string[]) { this.setTextFrame(FrameTypes.TCOM, ...value); } + set composers(value: string[]) { this.setTextFrame(FrameIdentifiers.TCOM, ...value); } /** @inheritDoc via TSOC frame */ - get composersSort(): string[] { return this.getTextAsArray(FrameTypes.TSOC); } + get composersSort(): string[] { return this.getTextAsArray(FrameIdentifiers.TSOC); } /** @inheritDoc via TSOC frame */ - set composersSort(value: string[]) { this.setTextFrame(FrameTypes.TSOC, ...value); } + set composersSort(value: string[]) { this.setTextFrame(FrameIdentifiers.TSOC, ...value); } /** @inheritDoc via TALB frame */ - get album(): string { return this.getTextAsString(FrameTypes.TALB); } + get album(): string { return this.getTextAsString(FrameIdentifiers.TALB); } /** @inheritDoc via TALB frame */ - set album(value: string) { this.setTextFrame(FrameTypes.TALB, value); } + set album(value: string) { this.setTextFrame(FrameIdentifiers.TALB, value); } /** @inheritDoc via TSOA frame */ - get albumSort(): string { return this.getTextAsString(FrameTypes.TSOA); } + get albumSort(): string { return this.getTextAsString(FrameIdentifiers.TSOA); } /** @inheritDoc via TSOA fram */ - set albumSort(value: string) { this.setTextFrame(FrameTypes.TSOA, value); } + set albumSort(value: string) { this.setTextFrame(FrameIdentifiers.TSOA, value); } /** @inheritDoc via COMM frame */ get comment(): string { @@ -248,7 +249,7 @@ export default class Id3v2Tag extends Tag { /** @inheritDoc via TCON frame */ get genres(): string[] { - const text = this.getTextAsArray(FrameTypes.TCON); + const text = this.getTextAsArray(FrameIdentifiers.TCON); if (text.length === 0) { return text; } const list = []; @@ -269,7 +270,7 @@ export default class Id3v2Tag extends Tag { /** @inheritDoc via TCON frame */ set genres(value: string[]) { if (!value || !Id3v2TagSettings.useNumericGenres) { - this.setTextFrame(FrameTypes.TCON, ...value); + this.setTextFrame(FrameIdentifiers.TCON, ...value); return; } @@ -282,12 +283,12 @@ export default class Id3v2Tag extends Tag { } } - this.setTextFrame(FrameTypes.TCON, ...value); + this.setTextFrame(FrameIdentifiers.TCON, ...value); } /** @inheritDoc via TDRC frame */ get year(): number { - const text = this.getTextAsString(FrameTypes.TDRC); + const text = this.getTextAsString(FrameIdentifiers.TDRC); if (!text || text.length < 4) { return 0; } const year = Number.parseInt(text.substr(0, 4), 10); @@ -308,28 +309,28 @@ export default class Id3v2Tag extends Tag { if (value > 9999) { value = 0; } - this.setNumberFrame(FrameTypes.TDRC, value, 0); + this.setNumberFrame(FrameIdentifiers.TDRC, value, 0); } /** @inheritDoc via TRCK frame */ - get track(): number { return this.getTextAsUint32(FrameTypes.TRCK, 0); } + get track(): number { return this.getTextAsUint32(FrameIdentifiers.TRCK, 0); } /** @inheritDoc via TRCK frame */ - set track(value: number) { this.setNumberFrame(FrameTypes.TRCK, value, this.trackCount, 2); } + set track(value: number) { this.setNumberFrame(FrameIdentifiers.TRCK, value, this.trackCount, 2); } /** @inheritDoc via TRCK frame */ - get trackCount(): number { return this.getTextAsUint32(FrameTypes.TRCK, 1); } + get trackCount(): number { return this.getTextAsUint32(FrameIdentifiers.TRCK, 1); } /** @inheritDoc via TRCK frame */ - set trackCount(value: number) { this.setNumberFrame(FrameTypes.TRCK, this.track, value); } + set trackCount(value: number) { this.setNumberFrame(FrameIdentifiers.TRCK, this.track, value); } /** @inheritDoc via TPOS frame */ - get disc(): number { return this.getTextAsUint32(FrameTypes.TPOS, 0); } + get disc(): number { return this.getTextAsUint32(FrameIdentifiers.TPOS, 0); } /** @inheritDoc via TPOS frame */ - set disc(value: number) { this.setNumberFrame(FrameTypes.TPOS, value, this.discCount); } + set disc(value: number) { this.setNumberFrame(FrameIdentifiers.TPOS, value, this.discCount); } /** @inheritDoc via TPOS frame */ - get discCount(): number { return this.getTextAsUint32(FrameTypes.TPOS, 1); } + get discCount(): number { return this.getTextAsUint32(FrameIdentifiers.TPOS, 1); } /** @inheritDoc via TPOS frame */ - set discCount(value: number) { this.setNumberFrame(FrameTypes.TPOS, this.disc, value); } + set discCount(value: number) { this.setNumberFrame(FrameIdentifiers.TPOS, this.disc, value); } /** @inheritDoc via USLT frame */ get lyrics(): string { @@ -362,33 +363,33 @@ export default class Id3v2Tag extends Tag { } /** @inheritDoc via TIT1 frame */ - get grouping(): string { return this.getTextAsString(FrameTypes.TIT1); } + get grouping(): string { return this.getTextAsString(FrameIdentifiers.TIT1); } /** @inheritDoc via TIT1 frame */ - set grouping(value: string) { this.setTextFrame(FrameTypes.TIT1, value); } + set grouping(value: string) { this.setTextFrame(FrameIdentifiers.TIT1, value); } /** @inheritDoc via TBPM frame */ get beatsPerMinute(): number { - const text = this.getTextAsString(FrameTypes.TBPM); + const text = this.getTextAsString(FrameIdentifiers.TBPM); if (!text) { return 0; } const num = Number.parseFloat(text); return Number.isNaN(num) || num < 0.0 ? 0 : Math.round(num); } /** @inheritDoc via TBPM frame */ - set beatsPerMinute(value: number) { this.setNumberFrame(FrameTypes.TBPM, value, 0); } + set beatsPerMinute(value: number) { this.setNumberFrame(FrameIdentifiers.TBPM, value, 0); } /** @inheritDoc via TPE3 frame */ - get conductor(): string { return this.getTextAsString(FrameTypes.TPE3); } + get conductor(): string { return this.getTextAsString(FrameIdentifiers.TPE3); } /** @inheritDoc via TPE3 frame */ - set conductor(value: string) { this.setTextFrame(FrameTypes.TPE3, value); } + set conductor(value: string) { this.setTextFrame(FrameIdentifiers.TPE3, value); } /** @inheritDoc via TCOP frame */ - get copyright(): string { return this.getTextAsString(FrameTypes.TCOP); } + get copyright(): string { return this.getTextAsString(FrameIdentifiers.TCOP); } /** @inheritDoc via TCOP frame */ - set copyright(value: string) { this.setTextFrame(FrameTypes.TCOP, value); } + set copyright(value: string) { this.setTextFrame(FrameIdentifiers.TCOP, value); } /** @inheritDoc via TDTG frame */ get dateTagged(): Date | undefined { - const strValue = this.getTextAsString(FrameTypes.TDTG); + const strValue = this.getTextAsString(FrameIdentifiers.TDTG); if (!strValue) { return undefined; } const dateValue = new Date(strValue); return isNaN(dateValue.getTime()) ? undefined : dateValue; @@ -400,7 +401,7 @@ export default class Id3v2Tag extends Tag { strValue = DateFormat(value, "yyyy-mm-dd HH:MM:ss"); strValue = strValue.replace(" ", "T"); } - this.setTextFrame(FrameTypes.TDTG, strValue); + this.setTextFrame(FrameIdentifiers.TDTG, strValue); } /** @inheritDoc via TXXX:MusicBrainz Artist Id frame */ @@ -531,24 +532,24 @@ export default class Id3v2Tag extends Tag { } /** @inheritDoc via TKEY frame */ - get initialKey(): string { return this.getTextAsString(FrameTypes.TKEY); } + get initialKey(): string { return this.getTextAsString(FrameIdentifiers.TKEY); } /** @inheritDoc via TKEY frame */ - set initialKey(value: string) { this.setTextFrame(FrameTypes.TKEY, value); } + set initialKey(value: string) { this.setTextFrame(FrameIdentifiers.TKEY, value); } /** @inheritDoc via TPE4 frame */ - get remixedBy(): string { return this.getTextAsString(FrameTypes.TPE4); } + get remixedBy(): string { return this.getTextAsString(FrameIdentifiers.TPE4); } /** @inheritDoc via TPE4 frame */ - set remixedBy(value: string) { this.setTextFrame(FrameTypes.TPE4, value); } + set remixedBy(value: string) { this.setTextFrame(FrameIdentifiers.TPE4, value); } /** @inheritDoc via TPUB frame */ - get publisher(): string { return this.getTextAsString(FrameTypes.TPUB); } + get publisher(): string { return this.getTextAsString(FrameIdentifiers.TPUB); } /** @inheritDoc via TPUB frame */ - set publisher(value: string) { this.setTextFrame(FrameTypes.TPUB, value); } + set publisher(value: string) { this.setTextFrame(FrameIdentifiers.TPUB, value); } /** @inheritDoc via TSRC frame */ - get isrc(): string { return this.getTextAsString(FrameTypes.TSRC); } + get isrc(): string { return this.getTextAsString(FrameIdentifiers.TSRC); } /** @inheritDoc via TSRC frame */ - set isrc(value: string) { this.setTextFrame(FrameTypes.TSRC, value); } + set isrc(value: string) { this.setTextFrame(FrameIdentifiers.TSRC, value); } /** @inheritDoc via APIC frame */ get pictures(): IPicture[] { @@ -556,8 +557,8 @@ export default class Id3v2Tag extends Tag { } /** @inheritDoc via APIC frame */ set pictures(value: IPicture[]) { - this.removeFrames(FrameTypes.APIC); - this.removeFrames(FrameTypes.GEOB); + this.removeFrames(FrameIdentifiers.APIC); + this.removeFrames(FrameIdentifiers.GEOB); if (!value || value.length === 0) { return; } @@ -758,7 +759,7 @@ export default class Id3v2Tag extends Tag { } } - this.setTextFrame(FrameTypes.TMCL, ...ret); + this.setTextFrame(FrameIdentifiers.TMCL, ...ret); // We need to render the "tag data" first so that we have to correct size to render in the // tag's header. The "tag data" (everything that is included in Header.tagSize) includes @@ -871,11 +872,8 @@ export default class Id3v2Tag extends Tag { * @param text Text to set for the specified frame or `undefined`/`null`/`""` to remove all * frames with that identifier. */ - public setTextFrame(ident: ByteVector, ...text: string[]): void { + public setTextFrame(ident: FrameIdentifier, ...text: string[]): void { Guards.truthy(ident, "ident"); - if (ident.length !== 4) { - throw new Error("Argument out of range: ident must be 4 characters"); - } // Check if all the elements provided are empty. If they are, remove the frame. let empty = true; @@ -924,15 +922,15 @@ export default class Id3v2Tag extends Tag { // If the entire tag is marked as unsynchronized, and this tag is version ID3v2.3 or lower, // resynchronize it. const fullTagUnsync = this._header.majorVersion < 4 - && (this._header.flags & HeaderFlags.Unsynchronication) != 0; + && (this._header.flags & HeaderFlags.Unsynchronication) !== 0; // Avoid loading all the ID3 tag if PictureLazy is enabled and size is significant enough // (ID3v2.4 and later only) if (data && ( fullTagUnsync || this._header.tagSize < 1024 || - (style & ReadStyle.PictureLazy) != 0 || - (this._header.flags & HeaderFlags.ExtendedHeader) != 0 + (style & ReadStyle.PictureLazy) !== 0 || + (this._header.flags & HeaderFlags.ExtendedHeader) !== 0 )) { file.seek(position); data = file.readBlock(this._header.tagSize); @@ -947,7 +945,7 @@ export default class Id3v2Tag extends Tag { + frameDataPosition - Id3v2FrameHeader.getSize(this._header.majorVersion); // Check for the extended header - if ((this._header.flags & HeaderFlags.ExtendedHeader) != 0) { + if ((this._header.flags & HeaderFlags.ExtendedHeader) !== 0) { this._extendedHeader = ExtendedHeader.fromData(data, this._header.majorVersion); if (this._extendedHeader.size <= data.length) { @@ -1001,13 +999,13 @@ export default class Id3v2Tag extends Tag { } // Load up the first instance of each for post-processing - if (!tdrc && ByteVector.equal(frame.frameId, FrameTypes.TDRC)) { + if (!tdrc && ByteVector.equal(frame.frameId, FrameIdentifiers.TDRC)) { tdrc = frame; - } else if (!tyer && ByteVector.equal(frame.frameId, FrameTypes.TYER)) { + } else if (!tyer && ByteVector.equal(frame.frameId, FrameIdentifiers.TYER)) { tyer = frame; - } else if (!tdat && ByteVector.equal(frame.frameId, FrameTypes.TDAT)) { + } else if (!tdat && ByteVector.equal(frame.frameId, FrameIdentifiers.TDAT)) { tdat = frame; - } else if (!time && ByteVector.equal(frame.frameId, FrameTypes.TIME)) { + } else if (!time && ByteVector.equal(frame.frameId, FrameIdentifiers.TIME)) { time = frame; } } @@ -1035,7 +1033,7 @@ export default class Id3v2Tag extends Tag { tdrcText += `T${timeText.substr(0, 2)}:${timeText.substr(2, 2)}`; } - this.removeFrames(FrameTypes.TDAT); + this.removeFrames(FrameIdentifiers.TDAT); } } @@ -1066,13 +1064,13 @@ export default class Id3v2Tag extends Tag { this.parse(undefined, file, position, style); } - private getTextAsArray(ident: ByteVector): string[] { + private getTextAsArray(ident: FrameIdentifier): string[] { const frames = this.getFramesByClassType(FrameClassType.TextInformationFrame); const frame = TextInformationFrame.findTextInformationFrame(frames, ident); return frame ? frame.text : []; } - private getTextAsUint32(ident: ByteVector, index: number): number { + private getTextAsUint32(ident: FrameIdentifier, index: number): number { const text = this.getTextAsString(ident); if (text === null || text === undefined) { return 0; diff --git a/test/id3v2/attachmentsFrameTests.ts b/test/id3v2/attachmentsFrameTests.ts index e2434e35..63600308 100644 --- a/test/id3v2/attachmentsFrameTests.ts +++ b/test/id3v2/attachmentsFrameTests.ts @@ -7,7 +7,7 @@ import AttachmentFrame from "../../src/id3v2/frames/attachmentFrame"; import {IPicture, PictureType} from "../../src/picture"; import TestConstants from "../testConstants"; import {FrameClassType} from "../../src/id3v2/frames/frame"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import {ByteVector} from "../../src/byteVector"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; diff --git a/test/id3v2/commentsFrameTests.ts b/test/id3v2/commentsFrameTests.ts index 97a37cfb..2b89610d 100644 --- a/test/id3v2/commentsFrameTests.ts +++ b/test/id3v2/commentsFrameTests.ts @@ -5,7 +5,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import CommentsFrame from "../../src/id3v2/frames/commentsFrame"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; diff --git a/test/id3v2/eventTimeCodeFrameTests.ts b/test/id3v2/eventTimeCodeFrameTests.ts index daa8c6d5..4279a4b6 100644 --- a/test/id3v2/eventTimeCodeFrameTests.ts +++ b/test/id3v2/eventTimeCodeFrameTests.ts @@ -4,7 +4,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import {ByteVector} from "../../src/byteVector"; import {EventTimeCode, EventTimeCodeFrame} from "../../src/id3v2/frames/eventTimeCodeFrame"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; diff --git a/test/id3v2/frameConstructorTests.ts b/test/id3v2/frameConstructorTests.ts index 72305160..5d45410e 100644 --- a/test/id3v2/frameConstructorTests.ts +++ b/test/id3v2/frameConstructorTests.ts @@ -5,7 +5,7 @@ import {test} from "mocha-typescript"; import {ByteVector} from "../../src/byteVector"; import {Frame} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; // Setup chai Chai.use(ChaiAsPromised); diff --git a/test/id3v2/musicCdIdentifierFrameTests.ts b/test/id3v2/musicCdIdentifierFrameTests.ts index 85433b1a..8d29570a 100644 --- a/test/id3v2/musicCdIdentifierFrameTests.ts +++ b/test/id3v2/musicCdIdentifierFrameTests.ts @@ -4,7 +4,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import MusicCdIdentifierFrame from "../../src/id3v2/frames/musicCdIdentifierFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; diff --git a/test/id3v2/playCountFrameTests.ts b/test/id3v2/playCountFrameTests.ts index 1324acdf..39f8d605 100644 --- a/test/id3v2/playCountFrameTests.ts +++ b/test/id3v2/playCountFrameTests.ts @@ -5,7 +5,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import PlayCountFrame from "../../src/id3v2/frames/playCountFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; diff --git a/test/id3v2/popularimeterFrameTests.ts b/test/id3v2/popularimeterFrameTests.ts index 1819fa14..6df97113 100644 --- a/test/id3v2/popularimeterFrameTests.ts +++ b/test/id3v2/popularimeterFrameTests.ts @@ -5,7 +5,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import PopularimeterFrame from "../../src/id3v2/frames/popularimeterFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; diff --git a/test/id3v2/privateFrameTests.ts b/test/id3v2/privateFrameTests.ts index 6c065bf6..36432582 100644 --- a/test/id3v2/privateFrameTests.ts +++ b/test/id3v2/privateFrameTests.ts @@ -4,7 +4,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import PrivateFrame from "../../src/id3v2/frames/privateFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; diff --git a/test/id3v2/relativeVolumeFrameTests.ts b/test/id3v2/relativeVolumeFrameTests.ts index ca759761..7ba7672f 100644 --- a/test/id3v2/relativeVolumeFrameTests.ts +++ b/test/id3v2/relativeVolumeFrameTests.ts @@ -5,7 +5,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import ConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import {ChannelData, ChannelType, RelativeVolumeFrame} from "../../src/id3v2/frames/relativeVolumeFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; diff --git a/test/id3v2/synchronizedLyricsFrameTests.ts b/test/id3v2/synchronizedLyricsFrameTests.ts index ec90b738..fd2a970c 100644 --- a/test/id3v2/synchronizedLyricsFrameTests.ts +++ b/test/id3v2/synchronizedLyricsFrameTests.ts @@ -4,7 +4,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; diff --git a/test/id3v2/termsOfUseFrameTests.ts b/test/id3v2/termsOfUseFrameTests.ts index 0598a15f..3f2934dc 100644 --- a/test/id3v2/termsOfUseFrameTests.ts +++ b/test/id3v2/termsOfUseFrameTests.ts @@ -4,7 +4,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import TermsOfUseFrame from "../../src/id3v2/frames/termsOfUseFrame"; import {ByteVector, StringType} from "../../src/byteVector"; diff --git a/test/id3v2/textInformationFrameTests.ts b/test/id3v2/textInformationFrameTests.ts index abaa9cae..8542f7eb 100644 --- a/test/id3v2/textInformationFrameTests.ts +++ b/test/id3v2/textInformationFrameTests.ts @@ -4,7 +4,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import {TextInformationFrame} from "../../src/id3v2/frames/textInformationFrame"; import {ByteVector, StringType} from "../../src/byteVector"; diff --git a/test/id3v2/uniqueFileIdentifierFrameTests.ts b/test/id3v2/uniqueFileIdentifierFrameTests.ts index c91a67f4..ef11641c 100644 --- a/test/id3v2/uniqueFileIdentifierFrameTests.ts +++ b/test/id3v2/uniqueFileIdentifierFrameTests.ts @@ -4,7 +4,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import UniqueFileIdentifierFrame from "../../src/id3v2/frames/uniqueFileIdentifierFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; diff --git a/test/id3v2/unknownFrameTests.ts b/test/id3v2/unknownFrameTests.ts index 318e756c..58dbcfa2 100644 --- a/test/id3v2/unknownFrameTests.ts +++ b/test/id3v2/unknownFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import UnknownFrame from "../../src/id3v2/frames/unknownFrame"; import {ByteVector} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; diff --git a/test/id3v2/unsynchronizedLyricsFrameTests.ts b/test/id3v2/unsynchronizedLyricsFrameTests.ts index c9397309..36af3538 100644 --- a/test/id3v2/unsynchronizedLyricsFrameTests.ts +++ b/test/id3v2/unsynchronizedLyricsFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import UnsynchronizedLyricsFrame from "../../src/id3v2/frames/unsynchronizedLyricsFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; diff --git a/test/id3v2/urlLinkFrameTests.ts b/test/id3v2/urlLinkFrameTests.ts index 51e946f3..34589428 100644 --- a/test/id3v2/urlLinkFrameTests.ts +++ b/test/id3v2/urlLinkFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import TestConstants from "../testConstants"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; diff --git a/test/id3v2/userTextInformationFrameTests.ts b/test/id3v2/userTextInformationFrameTests.ts index 958abf1b..6ba5f439 100644 --- a/test/id3v2/userTextInformationFrameTests.ts +++ b/test/id3v2/userTextInformationFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import {UserTextInformationFrame} from "../../src/id3v2/frames/textInformationFrame"; import {ByteVector, StringType} from "../../src/byteVector"; diff --git a/test/id3v2/userUrlLinkFrameTests.ts b/test/id3v2/userUrlLinkFrameTests.ts index b3da76cd..50d41456 100644 --- a/test/id3v2/userUrlLinkFrameTests.ts +++ b/test/id3v2/userUrlLinkFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FrameTypes from "../../src/id3v2/frameTypes"; +import FrameTypes from "../../src/id3v2/frameIdentifiers"; import TestConstants from "../testConstants"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; From fc844ac005401ab38baee460b9035e66e7bc58a8 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 28 Jan 2020 20:44:04 -0500 Subject: [PATCH 35/71] More WIP --- src/id3v2/frameIdentifiers.ts | 492 ++++++++++++++-------------- src/id3v2/frames/attachmentFrame.ts | 4 +- src/id3v2/frames/frameHeader.ts | 390 +++------------------- src/id3v2/header.ts | 4 +- 4 files changed, 303 insertions(+), 587 deletions(-) diff --git a/src/id3v2/frameIdentifiers.ts b/src/id3v2/frameIdentifiers.ts index f9690ef8..664a49db 100644 --- a/src/id3v2/frameIdentifiers.ts +++ b/src/id3v2/frameIdentifiers.ts @@ -47,249 +47,251 @@ export class FrameIdentifier { } } -export class FrameIdentifiers { - private static dict: {[key: string]: FrameIdentifier} = { - AENC: new FrameIdentifier("AENC", "AENC", "CRA"), // Audio encryption - APIC: new FrameIdentifier("APIC", "APIC", "PIC"), // Attached picture - ASPI: new FrameIdentifier("ASPI", undefined, undefined), // Audio seek point table - COMM: new FrameIdentifier("COMM", "COMM", "COM"), // Comments - COMR: new FrameIdentifier("COMR", "COMR", undefined), // Commercial frame - CRM : new FrameIdentifier(undefined, undefined, "CRM"), // Encrypted meta-frame - ENCR: new FrameIdentifier("ENCR", "ENCR", undefined), // Encryption method registration - EQU2: new FrameIdentifier("EQU2", "EQUA", undefined), // Equalization - ETCO: new FrameIdentifier("ETCO", "ETCO", "ETC"), // Event timing codes - GEOB: new FrameIdentifier("GEOB", "GEOB", "GEO"), // General encapsulated object - GRID: new FrameIdentifier("GRID", "GRID", undefined), // Group identification registration - LINK: new FrameIdentifier("LINK", "LINK", "LNK"), // Linked information - MCDI: new FrameIdentifier("MCDI", "MCDI", "MCI"), // Music CD identifier - MLLT: new FrameIdentifier("MLLT", "MLLT", "MLL"), // MPEG location lookup table - OWNE: new FrameIdentifier("OWNE", "OWNE", undefined), // Ownership frame - PCNT: new FrameIdentifier("PCNT", "PCNT", "CNT"), // Play count - POPM: new FrameIdentifier("POPM", "POPM", "POP"), // Popularimeter - POSS: new FrameIdentifier("POSS", "POSS", undefined), // Position synchronization frame - PRIV: new FrameIdentifier("PRIV", "PRIV", undefined), // Private frame - RBUF: new FrameIdentifier("RBUF", "RBUF", "BUF"), // Recommended buffer size - RVA2: new FrameIdentifier("RVA2", "RVAD", "RVA"), // Relative volume adjustment - RVRB: new FrameIdentifier("RVRB", "RVRB", "REV"), // Reverb - SEEK: new FrameIdentifier("SEEK", undefined, undefined), // Seek frame - SIGN: new FrameIdentifier("SIGN", undefined, undefined), // Signature frame - SYLT: new FrameIdentifier("SYLT", "SYLT", "SLT"), // Synchronized lyric/text - SYTC: new FrameIdentifier("SYTC", "SYTC", "STC"), // Synchronized tempo codes - TALB: new FrameIdentifier("TALB", "TALB", "TAL"), // Album/Movie/Show title - TBPM: new FrameIdentifier("TBPM", "TBPM", "TBP"), // BPM - TCOM: new FrameIdentifier("TCOM", "TCOM", "TCM"), // Composer - TCMP: new FrameIdentifier("TCMP", "TCMP", undefined), // iTunes only "compilation" flag - TCON: new FrameIdentifier("TCON", "TCON", "TCO"), // Content type - TCOP: new FrameIdentifier("TCOP", "TCOP", "TCR"), // Copyright message - TDAT: new FrameIdentifier(undefined, "TDAT", "TDA"), // Date - TDEN: new FrameIdentifier("TDEN", undefined, undefined), // Encoding time - TDLY: new FrameIdentifier("TDLY", "TDLY", "TDY"), // Playlist delay - TDOR: new FrameIdentifier("TDOR", "TORY", "TOR"), // Original release time - TDRC: new FrameIdentifier("TDRC", "TYER", "TYE"), // Recording time (v2.4/v2.3) / Year (v2.2) - TDRL: new FrameIdentifier("TDRL", undefined, undefined), // Release time - TDTG: new FrameIdentifier("TDTG", undefined, undefined), // Tagging time - TENC: new FrameIdentifier("TENC", "TENC", "TEN"), // Encoded by - TEXT: new FrameIdentifier("TEXT", "TEXT", "TXT"), // Lyricist/Text writer - TFLT: new FrameIdentifier("TFLT", "TFLT", "TFT"), // File type - TIME: new FrameIdentifier(undefined, "TIME", "TIM"), // Time - TIPL: new FrameIdentifier("TIPL", "IPLS", "IPL"), // Involved people list - TIT1: new FrameIdentifier("TIT1", "TIT1", "TT1"), // Content group description - TIT2: new FrameIdentifier("TIT2", "TIT2", "TT2"), // Title/songname/content description - TIT3: new FrameIdentifier("TIT3", "TIT3", "TT3"), // Subtitle/description refinement - TKEY: new FrameIdentifier("TKEY", "TKEY", "TKE"), // Initial key - TLAN: new FrameIdentifier("TLAN", "TLAN", "TLA"), // Language(s) - TLEN: new FrameIdentifier("TLEN", "TLEN", "TLE"), // Length - TMCL: new FrameIdentifier("TMCL", undefined, undefined), // Musician credit list - TMED: new FrameIdentifier("TMED", "TMED", "TMT"), // Media type - TMOO: new FrameIdentifier("TMOO", undefined, undefined), // Mood - TOAL: new FrameIdentifier("TOAL", "TOAL", "TOT"), // Original album/movie/show title - TOFN: new FrameIdentifier("TOFN", "TOFN", "TOF"), // Original filename - TOLY: new FrameIdentifier("TOLY", "TOLY", "TOL"), // Original lyricist(s)/text writer(s) - TOPE: new FrameIdentifier("TOPE", "TOPE", "TOA"), // Original artist(s)/performer(s) - TOWN: new FrameIdentifier("TOWN", "TOWN", undefined), // File owner/licensee - TPE1: new FrameIdentifier("TPE1", "TPE1", "TP1"), // Lead performer(s)/soloist(s) - TPE2: new FrameIdentifier("TPE2", "TPE2", "TP2"), // Band/orchestra/accompaniment - TPE3: new FrameIdentifier("TPE3", "TEP3", "TP3"), // Counductor/performer refinement - TPE4: new FrameIdentifier("TPE4", "TEP4", "TP4"), // Interpreted, remixed, or otherwise modified by - TPOS: new FrameIdentifier("TPOS", "TPOS", "TPA"), // Part of a set - TPRO: new FrameIdentifier("TPRO", undefined, undefined), // Produced notice - TPUB: new FrameIdentifier("TPUB", "TPUB", "TPB"), // Publisher - TRCK: new FrameIdentifier("TRCK", "TRCK", "TRK"), // Track number/position in set - TRDA: new FrameIdentifier(undefined, "TRDA", "TRD"), // Recording dates - TRSN: new FrameIdentifier("TRSN", "TRSN", undefined), // Internet radio station name - TRSO: new FrameIdentifier("TRSO", "TRSO", undefined), // Internet radio station owner - TSIZ: new FrameIdentifier(undefined, "TSIZ", "TSI"), // Size - TSOA: new FrameIdentifier("TSOA", undefined, undefined), // Album sort order - TSOP: new FrameIdentifier("TSOP", undefined, undefined), // Performer sort order - TSOT: new FrameIdentifier("TSOT", undefined, undefined), // Title sort order - TSRC: new FrameIdentifier("TSRC", "TSRC", "TRC"), // ISRC (International standard recording code) - TSSE: new FrameIdentifier("TSSE", "TSSE", "TSS"), // Software/hardware and setting used for encoding - TSST: new FrameIdentifier("TSST", undefined, undefined), // Set subtitle - TXXX: new FrameIdentifier("TXXX", "TXXX", "TXX"), // User defined text information frame - UFID: new FrameIdentifier("UFID", "UFID", "UFI"), // Unique file identifer - USER: new FrameIdentifier("USER", "USER", undefined), // Terms of use - USLT: new FrameIdentifier("USLT", "USLT", "ULT"), // Unsynchronised lyric/text transcription - WCOM: new FrameIdentifier("WCOM", "WCOM", "WCM"), // Commercial information URL - WCOP: new FrameIdentifier("WCOP", "WCOP", "WCP"), // Copyright/legal information URL - WOAF: new FrameIdentifier("WOAF", "WOAF", "WAF"), // Official audio file webpage URL - WOAR: new FrameIdentifier("WOAR", "WOAR", "WAR"), // Official artist/performer webpage URL - WOAS: new FrameIdentifier("WOAS", "WOAS", "WAS"), // Official audio source webpage URL - WORS: new FrameIdentifier("WORS", "WORS", undefined), // Official internet radio station homepage URL - WPAY: new FrameIdentifier("WPAY", "WPAY", undefined), // Payment URL - WPUB: new FrameIdentifier("WPUB", "WPUB", "WPB"), // Publishers official webpage URL - WXXX: new FrameIdentifier("WXXX", "WXXX", "WXX"), // User defined URL link frame - }; +// Pre-create the unique identifiers +const uniqueFrameIdentifiers: {[key: string]: FrameIdentifier} = { + AENC: new FrameIdentifier("AENC", "AENC", "CRA"), // Audio encryption + APIC: new FrameIdentifier("APIC", "APIC", "PIC"), // Attached picture + ASPI: new FrameIdentifier("ASPI", undefined, undefined), // Audio seek point table + COMM: new FrameIdentifier("COMM", "COMM", "COM"), // Comments + COMR: new FrameIdentifier("COMR", "COMR", undefined), // Commercial frame + CRM : new FrameIdentifier(undefined, undefined, "CRM"), // Encrypted meta-frame + ENCR: new FrameIdentifier("ENCR", "ENCR", undefined), // Encryption method registration + EQU2: new FrameIdentifier("EQU2", "EQUA", undefined), // Equalization + ETCO: new FrameIdentifier("ETCO", "ETCO", "ETC"), // Event timing codes + GEOB: new FrameIdentifier("GEOB", "GEOB", "GEO"), // General encapsulated object + GRID: new FrameIdentifier("GRID", "GRID", undefined), // Group identification registration + LINK: new FrameIdentifier("LINK", "LINK", "LNK"), // Linked information + MCDI: new FrameIdentifier("MCDI", "MCDI", "MCI"), // Music CD identifier + MLLT: new FrameIdentifier("MLLT", "MLLT", "MLL"), // MPEG location lookup table + OWNE: new FrameIdentifier("OWNE", "OWNE", undefined), // Ownership frame + PCNT: new FrameIdentifier("PCNT", "PCNT", "CNT"), // Play count + POPM: new FrameIdentifier("POPM", "POPM", "POP"), // Popularimeter + POSS: new FrameIdentifier("POSS", "POSS", undefined), // Position synchronization frame + PRIV: new FrameIdentifier("PRIV", "PRIV", undefined), // Private frame + RBUF: new FrameIdentifier("RBUF", "RBUF", "BUF"), // Recommended buffer size + RVA2: new FrameIdentifier("RVA2", "RVAD", "RVA"), // Relative volume adjustment + RVRB: new FrameIdentifier("RVRB", "RVRB", "REV"), // Reverb + SEEK: new FrameIdentifier("SEEK", undefined, undefined), // Seek frame + SIGN: new FrameIdentifier("SIGN", undefined, undefined), // Signature frame + SYLT: new FrameIdentifier("SYLT", "SYLT", "SLT"), // Synchronized lyric/text + SYTC: new FrameIdentifier("SYTC", "SYTC", "STC"), // Synchronized tempo codes + TALB: new FrameIdentifier("TALB", "TALB", "TAL"), // Album/Movie/Show title + TBPM: new FrameIdentifier("TBPM", "TBPM", "TBP"), // BPM + TCMP: new FrameIdentifier("TCMP", "TCMP", undefined), // iTunes only "compilation" flag + TCOM: new FrameIdentifier("TCOM", "TCOM", "TCM"), // Composer + TCON: new FrameIdentifier("TCON", "TCON", "TCO"), // Content type + TCOP: new FrameIdentifier("TCOP", "TCOP", "TCR"), // Copyright message + TDAT: new FrameIdentifier(undefined, "TDAT", "TDA"), // Date + TDEN: new FrameIdentifier("TDEN", undefined, undefined), // Encoding time + TDLY: new FrameIdentifier("TDLY", "TDLY", "TDY"), // Playlist delay + TDOR: new FrameIdentifier("TDOR", "TORY", "TOR"), // Original release time + TDRC: new FrameIdentifier("TDRC", "TYER", "TYE"), // Recording time (v2.4/v2.3) / Year (v2.2) + TDRL: new FrameIdentifier("TDRL", undefined, undefined), // Release time + TDTG: new FrameIdentifier("TDTG", undefined, undefined), // Tagging time + TENC: new FrameIdentifier("TENC", "TENC", "TEN"), // Encoded by + TEXT: new FrameIdentifier("TEXT", "TEXT", "TXT"), // Lyricist/Text writer + TFLT: new FrameIdentifier("TFLT", "TFLT", "TFT"), // File type + TIME: new FrameIdentifier(undefined, "TIME", "TIM"), // Time + TIPL: new FrameIdentifier("TIPL", "IPLS", "IPL"), // Involved people list + TIT1: new FrameIdentifier("TIT1", "TIT1", "TT1"), // Content group description + TIT2: new FrameIdentifier("TIT2", "TIT2", "TT2"), // Title/songname/content description + TIT3: new FrameIdentifier("TIT3", "TIT3", "TT3"), // Subtitle/description refinement + TKEY: new FrameIdentifier("TKEY", "TKEY", "TKE"), // Initial key + TLAN: new FrameIdentifier("TLAN", "TLAN", "TLA"), // Language(s) + TLEN: new FrameIdentifier("TLEN", "TLEN", "TLE"), // Length + TMCL: new FrameIdentifier("TMCL", undefined, undefined), // Musician credit list + TMED: new FrameIdentifier("TMED", "TMED", "TMT"), // Media type + TMOO: new FrameIdentifier("TMOO", undefined, undefined), // Mood + TOAL: new FrameIdentifier("TOAL", "TOAL", "TOT"), // Original album/movie/show title + TOFN: new FrameIdentifier("TOFN", "TOFN", "TOF"), // Original filename + TOLY: new FrameIdentifier("TOLY", "TOLY", "TOL"), // Original lyricist(s)/text writer(s) + TOPE: new FrameIdentifier("TOPE", "TOPE", "TOA"), // Original artist(s)/performer(s) + TOWN: new FrameIdentifier("TOWN", "TOWN", undefined), // File owner/licensee + TPE1: new FrameIdentifier("TPE1", "TPE1", "TP1"), // Lead performer(s)/soloist(s) + TPE2: new FrameIdentifier("TPE2", "TPE2", "TP2"), // Band/orchestra/accompaniment + TPE3: new FrameIdentifier("TPE3", "TEP3", "TP3"), // Counductor/performer refinement + TPE4: new FrameIdentifier("TPE4", "TEP4", "TP4"), // Interpreted, remixed, or otherwise modified by + TPOS: new FrameIdentifier("TPOS", "TPOS", "TPA"), // Part of a set + TPRO: new FrameIdentifier("TPRO", undefined, undefined), // Produced notice + TPUB: new FrameIdentifier("TPUB", "TPUB", "TPB"), // Publisher + TRCK: new FrameIdentifier("TRCK", "TRCK", "TRK"), // Track number/position in set + TRDA: new FrameIdentifier(undefined, "TRDA", "TRD"), // Recording dates + TRSN: new FrameIdentifier("TRSN", "TRSN", undefined), // Internet radio station name + TRSO: new FrameIdentifier("TRSO", "TRSO", undefined), // Internet radio station owner + TSIZ: new FrameIdentifier(undefined, "TSIZ", "TSI"), // Size + TSOA: new FrameIdentifier("TSOA", undefined, undefined), // Album sort order + TSOP: new FrameIdentifier("TSOP", undefined, undefined), // Performer sort order + TSOT: new FrameIdentifier("TSOT", undefined, undefined), // Title sort order + TSRC: new FrameIdentifier("TSRC", "TSRC", "TRC"), // ISRC (International standard recording code) + TSSE: new FrameIdentifier("TSSE", "TSSE", "TSS"), // Software/hardware and setting used for encoding + TSST: new FrameIdentifier("TSST", undefined, undefined), // Set subtitle + TXXX: new FrameIdentifier("TXXX", "TXXX", "TXX"), // User defined text information frame + UFID: new FrameIdentifier("UFID", "UFID", "UFI"), // Unique file identifer + USER: new FrameIdentifier("USER", "USER", undefined), // Terms of use + USLT: new FrameIdentifier("USLT", "USLT", "ULT"), // Unsynchronised lyric/text transcription + WCOM: new FrameIdentifier("WCOM", "WCOM", "WCM"), // Commercial information URL + WCOP: new FrameIdentifier("WCOP", "WCOP", "WCP"), // Copyright/legal information URL + WOAF: new FrameIdentifier("WOAF", "WOAF", "WAF"), // Official audio file webpage URL + WOAR: new FrameIdentifier("WOAR", "WOAR", "WAR"), // Official artist/performer webpage URL + WOAS: new FrameIdentifier("WOAS", "WOAS", "WAS"), // Official audio source webpage URL + WORS: new FrameIdentifier("WORS", "WORS", undefined), // Official internet radio station homepage URL + WPAY: new FrameIdentifier("WPAY", "WPAY", undefined), // Payment URL + WPUB: new FrameIdentifier("WPUB", "WPUB", "WPB"), // Publishers official webpage URL + WXXX: new FrameIdentifier("WXXX", "WXXX", "WXX"), // User defined URL link frame +}; - public static AENC = FrameIdentifiers.dict.AENC; - public static APIC = FrameIdentifiers.dict.APIC; - public static ASPI = FrameIdentifiers.dict.ASPI; - public static BUF = FrameIdentifiers.dict.RBUF; - public static CNT = FrameIdentifiers.dict.PCNT; - public static COM = FrameIdentifiers.dict.COMM; - public static COMM = FrameIdentifiers.dict.COMM; - public static COMR = FrameIdentifiers.dict.COMR; - public static CRA = FrameIdentifiers.dict.AENC; - public static CRM = FrameIdentifiers.dict.CRM; - public static ENCR = FrameIdentifiers.dict.ENCR; - public static EQU2 = FrameIdentifiers.dict.EQU2; - public static EQUA = FrameIdentifiers.dict.EQU2; - public static ETC = FrameIdentifiers.dict.ETCO; - public static ETCO = FrameIdentifiers.dict.ETCO; - public static GEO = FrameIdentifiers.dict.GEOB; - public static GEOB = FrameIdentifiers.dict.GEOB; - public static GRID = FrameIdentifiers.dict.GRID; - public static IPLS = FrameIdentifiers.dict.TIPL; - public static LINK = FrameIdentifiers.dict.LINK; - public static LNK = FrameIdentifiers.dict.LINK; - public static MCDI = FrameIdentifiers.dict.MCDI; - public static MCI = FrameIdentifiers.dict.MCDI; - public static MLL = FrameIdentifiers.dict.MLLT; - public static MLLT = FrameIdentifiers.dict.MLLT; - public static OWNE = FrameIdentifiers.dict.OWNE; - public static PCNT = FrameIdentifiers.dict.PCNT; - public static PIC = FrameIdentifiers.dict.APIC; - public static POP = FrameIdentifiers.dict.POPM; - public static POPM = FrameIdentifiers.dict.POPM; - public static POSS = FrameIdentifiers.dict.POSS; - public static PRIV = FrameIdentifiers.dict.PRIV; - public static RBUF = FrameIdentifiers.dict.RBUF; - public static REV = FrameIdentifiers.dict.RVRB; - public static RVA = FrameIdentifiers.dict.RVA2; - public static RVA2 = FrameIdentifiers.dict.RVA2; - public static RVRB = FrameIdentifiers.dict.RVRB; - public static SEEK = FrameIdentifiers.dict.SEEK; - public static SIGN = FrameIdentifiers.dict.SIGN; - public static SLT = FrameIdentifiers.dict.SYLT; - public static STC = FrameIdentifiers.dict.SYTC; - public static SYLT = FrameIdentifiers.dict.SYLT; - public static SYTC = FrameIdentifiers.dict.SYTC; - public static TAL = FrameIdentifiers.dict.TALB; - public static TALB = FrameIdentifiers.dict.TALB; - public static TBP = FrameIdentifiers.dict.TBPM; - public static TBPM = FrameIdentifiers.dict.TBPM; - public static TCM = FrameIdentifiers.dict.TCOM; - public static TCMP = FrameIdentifiers.dict.TCMP; - public static TCO = FrameIdentifiers.dict.TCON; - public static TCOM = FrameIdentifiers.dict.TCOM; - public static TCON = FrameIdentifiers.dict.TCON; - public static TCOP = FrameIdentifiers.dict.TCOP; - public static TCR = FrameIdentifiers.dict.TCOP; - public static TDA = FrameIdentifiers.dict.TDAT; - public static TDAT = FrameIdentifiers.dict.TDAT; - public static TDEN = FrameIdentifiers.dict.TDEN; - public static TDLY = FrameIdentifiers.dict.TDLY; - public static TDOR = FrameIdentifiers.dict.TDOR; - public static TDRC = FrameIdentifiers.dict.TDRC; - public static TDRL = FrameIdentifiers.dict.TDRL; - public static TDTG = FrameIdentifiers.dict.TDTG; - public static TDY = FrameIdentifiers.dict.TDLY; - public static TEN = FrameIdentifiers.dict.TENC; - public static TENC = FrameIdentifiers.dict.TENC; - public static TEXT = FrameIdentifiers.dict.TEXT; - public static TFLT = FrameIdentifiers.dict.TFLT; - public static TFT = FrameIdentifiers.dict.TFLT; - public static TIM = FrameIdentifiers.dict.TIME; - public static TIME = FrameIdentifiers.dict.TIME; - public static TIPL = FrameIdentifiers.dict.TIPL; - public static TIT1 = FrameIdentifiers.dict.TIT1; - public static TIT2 = FrameIdentifiers.dict.TIT2; - public static TIT3 = FrameIdentifiers.dict.TIT3; - public static TKE = FrameIdentifiers.dict.TKEY; - public static TKYE = FrameIdentifiers.dict.TKEY; - public static TLA = FrameIdentifiers.dict.TLAN; - public static TLAN = FrameIdentifiers.dict.TLAN; - public static TLE = FrameIdentifiers.dict.TLEN; - public static TLEN = FrameIdentifiers.dict.TLEN; - public static TMCL = FrameIdentifiers.dict.TMCL; - public static TMED = FrameIdentifiers.dict.TMED; - public static TMOO = FrameIdentifiers.dict.TMOO; - public static TMT = FrameIdentifiers.dict.TMED; - public static TOA = FrameIdentifiers.dict.TOPE; - public static TOAL = FrameIdentifiers.dict.TOAL; - public static TOF = FrameIdentifiers.dict.TOFN; - public static TOFN = FrameIdentifiers.dict.TOFN; - public static TOL = FrameIdentifiers.dict.TOLY; - public static TOLY = FrameIdentifiers.dict.TOLY; - public static TOPE = FrameIdentifiers.dict.TOPE; - public static TOR = FrameIdentifiers.dict.TDOR; - public static TORY = FrameIdentifiers.dict.TDOR; - public static TOT = FrameIdentifiers.dict.TOAL; - public static TOWN = FrameIdentifiers.dict.TOWN; - public static TP1 = FrameIdentifiers.dict.TPE1; - public static TP2 = FrameIdentifiers.dict.TPE2; - public static TP3 = FrameIdentifiers.dict.TPE3; - public static TP4 = FrameIdentifiers.dict.TPE4; - public static TPA = FrameIdentifiers.dict.TPOS; - public static TPB = FrameIdentifiers.dict.TPUB; - public static TPE1 = FrameIdentifiers.dict.TPE1; - public static TPE2 = FrameIdentifiers.dict.TPE2; - public static TPE3 = FrameIdentifiers.dict.TPE3; - public static TPE4 = FrameIdentifiers.dict.TPE4; - public static TPOS = FrameIdentifiers.dict.TPOS; - public static TPRO = FrameIdentifiers.dict.TPRO; - public static TPUB = FrameIdentifiers.dict.TPUB; - public static TRC = FrameIdentifiers.dict.TSRC; - public static TRCK = FrameIdentifiers.dict.TRCK; - public static TRD = FrameIdentifiers.dict.TRDA; - public static TRDA = FrameIdentifiers.dict.TRDA; - public static TRK = FrameIdentifiers.dict.TRCK; - public static TRSN = FrameIdentifiers.dict.TRSN; - public static TRSO = FrameIdentifiers.dict.TRSO; - public static TSI = FrameIdentifiers.dict.TSIZ; - public static TSIZ = FrameIdentifiers.dict.TSIZ; - public static TSOA = FrameIdentifiers.dict.TSOA; - public static TSOP = FrameIdentifiers.dict.TSOP; - public static TSOT = FrameIdentifiers.dict.TSOT; - public static TSRC = FrameIdentifiers.dict.TSRC; - public static TSS = FrameIdentifiers.dict.TSSE; - public static TSSE = FrameIdentifiers.dict.TSSE; - public static TSST = FrameIdentifiers.dict.TSST; - public static TT1 = FrameIdentifiers.dict.TIT1; - public static TT2 = FrameIdentifiers.dict.TIT2; - public static TT3 = FrameIdentifiers.dict.TIT3; - public static TXT = FrameIdentifiers.dict.TEXT; - public static TXX = FrameIdentifiers.dict.TXXX; - public static TXXX = FrameIdentifiers.dict.TXXX; - public static TYE = FrameIdentifiers.dict.TDRC; - public static TYER = FrameIdentifiers.dict.TDRC; - public static UFI = FrameIdentifiers.dict.UFID; - public static UFID = FrameIdentifiers.dict.UFID; - public static ULT = FrameIdentifiers.dict.USLT; - public static USER = FrameIdentifiers.dict.USER; - public static USLT = FrameIdentifiers.dict.USLT; - public static WAF = FrameIdentifiers.dict.WOAF; - public static WAR = FrameIdentifiers.dict.WOAR; - public static WAS = FrameIdentifiers.dict.WOAS; - public static WCM = FrameIdentifiers.dict.WCOM; - public static WCOM = FrameIdentifiers.dict.WCOM; - public static WCOP = FrameIdentifiers.dict.WCOP; - public static WCP = FrameIdentifiers.dict.WCOP; - public static WOAF = FrameIdentifiers.dict.WOAF; - public static WOAR = FrameIdentifiers.dict.WOAR; - public static WORS = FrameIdentifiers.dict.WORS; - public static WPAY = FrameIdentifiers.dict.WPAY; - public static WPB = FrameIdentifiers.dict.WPUB; - public static WPUB = FrameIdentifiers.dict.WPUB; - public static WXX = FrameIdentifiers.dict.WXXX; - public static WXXX = FrameIdentifiers.dict.WXXX; -} +// Export all the frame identifiers +export const FrameIdentifiers: {[key: string]: FrameIdentifier} = { + AENC: uniqueFrameIdentifiers.AENC, + APIC: uniqueFrameIdentifiers.APIC, + ASPI: uniqueFrameIdentifiers.ASPI, + BUF: uniqueFrameIdentifiers.RBUF, + CNT: uniqueFrameIdentifiers.PCNT, + COM: uniqueFrameIdentifiers.COMM, + COMM: uniqueFrameIdentifiers.COMM, + COMR: uniqueFrameIdentifiers.COMR, + CRA: uniqueFrameIdentifiers.AENC, + CRM: uniqueFrameIdentifiers.CRM, + ENCR: uniqueFrameIdentifiers.ENCR, + EQU2: uniqueFrameIdentifiers.EQU2, + EQUA: uniqueFrameIdentifiers.EQU2, + ETC: uniqueFrameIdentifiers.ETCO, + ETCO: uniqueFrameIdentifiers.ETCO, + GEO: uniqueFrameIdentifiers.GEOB, + GEOB: uniqueFrameIdentifiers.GEOB, + GRID: uniqueFrameIdentifiers.GRID, + IPLS: uniqueFrameIdentifiers.TIPL, + LINK: uniqueFrameIdentifiers.LINK, + LNK: uniqueFrameIdentifiers.LINK, + MCDI: uniqueFrameIdentifiers.MCDI, + MCI: uniqueFrameIdentifiers.MCDI, + MLL: uniqueFrameIdentifiers.MLLT, + MLLT: uniqueFrameIdentifiers.MLLT, + OWNE: uniqueFrameIdentifiers.OWNE, + PCNT: uniqueFrameIdentifiers.PCNT, + PIC: uniqueFrameIdentifiers.APIC, + POP: uniqueFrameIdentifiers.POPM, + POPM: uniqueFrameIdentifiers.POPM, + POSS: uniqueFrameIdentifiers.POSS, + PRIV: uniqueFrameIdentifiers.PRIV, + RBUF: uniqueFrameIdentifiers.RBUF, + REV: uniqueFrameIdentifiers.RVRB, + RVA: uniqueFrameIdentifiers.RVA2, + RVA2: uniqueFrameIdentifiers.RVA2, + RVRB: uniqueFrameIdentifiers.RVRB, + SEEK: uniqueFrameIdentifiers.SEEK, + SIGN: uniqueFrameIdentifiers.SIGN, + SLT: uniqueFrameIdentifiers.SYLT, + STC: uniqueFrameIdentifiers.SYTC, + SYLT: uniqueFrameIdentifiers.SYLT, + SYTC: uniqueFrameIdentifiers.SYTC, + TAL: uniqueFrameIdentifiers.TALB, + TALB: uniqueFrameIdentifiers.TALB, + TBP: uniqueFrameIdentifiers.TBPM, + TBPM: uniqueFrameIdentifiers.TBPM, + TCM: uniqueFrameIdentifiers.TCOM, + TCMP: uniqueFrameIdentifiers.TCMP, + TCO: uniqueFrameIdentifiers.TCON, + TCOM: uniqueFrameIdentifiers.TCOM, + TCON: uniqueFrameIdentifiers.TCON, + TCOP: uniqueFrameIdentifiers.TCOP, + TCR: uniqueFrameIdentifiers.TCOP, + TDA: uniqueFrameIdentifiers.TDAT, + TDAT: uniqueFrameIdentifiers.TDAT, + TDEN: uniqueFrameIdentifiers.TDEN, + TDLY: uniqueFrameIdentifiers.TDLY, + TDOR: uniqueFrameIdentifiers.TDOR, + TDRC: uniqueFrameIdentifiers.TDRC, + TDRL: uniqueFrameIdentifiers.TDRL, + TDTG: uniqueFrameIdentifiers.TDTG, + TDY: uniqueFrameIdentifiers.TDLY, + TEN: uniqueFrameIdentifiers.TENC, + TENC: uniqueFrameIdentifiers.TENC, + TEXT: uniqueFrameIdentifiers.TEXT, + TFLT: uniqueFrameIdentifiers.TFLT, + TFT: uniqueFrameIdentifiers.TFLT, + TIM: uniqueFrameIdentifiers.TIME, + TIME: uniqueFrameIdentifiers.TIME, + TIPL: uniqueFrameIdentifiers.TIPL, + TIT1: uniqueFrameIdentifiers.TIT1, + TIT2: uniqueFrameIdentifiers.TIT2, + TIT3: uniqueFrameIdentifiers.TIT3, + TKE: uniqueFrameIdentifiers.TKEY, + TKYE: uniqueFrameIdentifiers.TKEY, + TLA: uniqueFrameIdentifiers.TLAN, + TLAN: uniqueFrameIdentifiers.TLAN, + TLE: uniqueFrameIdentifiers.TLEN, + TLEN: uniqueFrameIdentifiers.TLEN, + TMCL: uniqueFrameIdentifiers.TMCL, + TMED: uniqueFrameIdentifiers.TMED, + TMOO: uniqueFrameIdentifiers.TMOO, + TMT: uniqueFrameIdentifiers.TMED, + TOA: uniqueFrameIdentifiers.TOPE, + TOAL: uniqueFrameIdentifiers.TOAL, + TOF: uniqueFrameIdentifiers.TOFN, + TOFN: uniqueFrameIdentifiers.TOFN, + TOL: uniqueFrameIdentifiers.TOLY, + TOLY: uniqueFrameIdentifiers.TOLY, + TOPE: uniqueFrameIdentifiers.TOPE, + TOR: uniqueFrameIdentifiers.TDOR, + TORY: uniqueFrameIdentifiers.TDOR, + TOT: uniqueFrameIdentifiers.TOAL, + TOWN: uniqueFrameIdentifiers.TOWN, + TP1: uniqueFrameIdentifiers.TPE1, + TP2: uniqueFrameIdentifiers.TPE2, + TP3: uniqueFrameIdentifiers.TPE3, + TP4: uniqueFrameIdentifiers.TPE4, + TPA: uniqueFrameIdentifiers.TPOS, + TPB: uniqueFrameIdentifiers.TPUB, + TPE1: uniqueFrameIdentifiers.TPE1, + TPE2: uniqueFrameIdentifiers.TPE2, + TPE3: uniqueFrameIdentifiers.TPE3, + TPE4: uniqueFrameIdentifiers.TPE4, + TPOS: uniqueFrameIdentifiers.TPOS, + TPRO: uniqueFrameIdentifiers.TPRO, + TPUB: uniqueFrameIdentifiers.TPUB, + TRC: uniqueFrameIdentifiers.TSRC, + TRCK: uniqueFrameIdentifiers.TRCK, + TRD: uniqueFrameIdentifiers.TRDA, + TRDA: uniqueFrameIdentifiers.TRDA, + TRK: uniqueFrameIdentifiers.TRCK, + TRSN: uniqueFrameIdentifiers.TRSN, + TRSO: uniqueFrameIdentifiers.TRSO, + TSI: uniqueFrameIdentifiers.TSIZ, + TSIZ: uniqueFrameIdentifiers.TSIZ, + TSOA: uniqueFrameIdentifiers.TSOA, + TSOP: uniqueFrameIdentifiers.TSOP, + TSOT: uniqueFrameIdentifiers.TSOT, + TSRC: uniqueFrameIdentifiers.TSRC, + TSS: uniqueFrameIdentifiers.TSSE, + TSSE: uniqueFrameIdentifiers.TSSE, + TSST: uniqueFrameIdentifiers.TSST, + TT1: uniqueFrameIdentifiers.TIT1, + TT2: uniqueFrameIdentifiers.TIT2, + TT3: uniqueFrameIdentifiers.TIT3, + TXT: uniqueFrameIdentifiers.TEXT, + TXX: uniqueFrameIdentifiers.TXXX, + TXXX: uniqueFrameIdentifiers.TXXX, + TYE: uniqueFrameIdentifiers.TDRC, + TYER: uniqueFrameIdentifiers.TDRC, + UFI: uniqueFrameIdentifiers.UFID, + UFID: uniqueFrameIdentifiers.UFID, + ULT: uniqueFrameIdentifiers.USLT, + USER: uniqueFrameIdentifiers.USER, + USLT: uniqueFrameIdentifiers.USLT, + WAF: uniqueFrameIdentifiers.WOAF, + WAR: uniqueFrameIdentifiers.WOAR, + WAS: uniqueFrameIdentifiers.WOAS, + WCM: uniqueFrameIdentifiers.WCOM, + WCOM: uniqueFrameIdentifiers.WCOM, + WCOP: uniqueFrameIdentifiers.WCOP, + WCP: uniqueFrameIdentifiers.WCOP, + WOAF: uniqueFrameIdentifiers.WOAF, + WOAR: uniqueFrameIdentifiers.WOAR, + WORS: uniqueFrameIdentifiers.WORS, + WPAY: uniqueFrameIdentifiers.WPAY, + WPB: uniqueFrameIdentifiers.WPUB, + WPUB: uniqueFrameIdentifiers.WPUB, + WXX: uniqueFrameIdentifiers.WXXX, + WXXX: uniqueFrameIdentifiers.WXXX, +}; diff --git a/src/id3v2/frames/attachmentFrame.ts b/src/id3v2/frames/attachmentFrame.ts index c63e5da3..b34340ca 100644 --- a/src/id3v2/frames/attachmentFrame.ts +++ b/src/id3v2/frames/attachmentFrame.ts @@ -1,8 +1,8 @@ -import FrameType from "../frameIdentifiers"; import Id3v2TagSettings from "../id3v2TagSettings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; import {Frame, FrameClassType} from "./frame"; +import {FrameIdentifiers} from "../frameIdentifiers"; import {Id3v2FrameHeader} from "./frameHeader"; import {IPicture, Picture, PictureType} from "../../picture"; import {Guards} from "../../utils"; @@ -58,7 +58,7 @@ export default class AttachmentFrame extends Frame { Guards.truthy(picture, "picture"); // In this case we will assume the frame is an APIC until the picture is parsed - const frame = new AttachmentFrame(new Id3v2FrameHeader(FrameType.APIC, 4)); + const frame = new AttachmentFrame(new Id3v2FrameHeader(FrameIdentifiers.APIC, 4)); frame._rawPicture = picture; return frame; } diff --git a/src/id3v2/frames/frameHeader.ts b/src/id3v2/frames/frameHeader.ts index 6dbe33e5..5fa0751f 100644 --- a/src/id3v2/frames/frameHeader.ts +++ b/src/id3v2/frames/frameHeader.ts @@ -1,8 +1,8 @@ -import FrameType from "../frameIdentifiers"; import SyncData from "../syncData"; import {ByteVector, StringType} from "../../byteVector"; -import {CorruptFileError, NotImplementedError} from "../../errors"; +import {CorruptFileError} from "../../errors"; import {Guards} from "../../utils"; +import {FrameIdentifier, FrameIdentifiers} from "../frameIdentifiers"; export enum Id3v2FrameFlags { /** @@ -52,268 +52,18 @@ export enum Id3v2FrameFlags { } export class Id3v2FrameHeader { - // #region Member Variables - - private static readonly version2Frames: ByteVector[][] = [ - [ - ByteVector.fromString("BUF", StringType.UTF8, undefined, true), - ByteVector.fromString("RBUF", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("CNT", StringType.UTF8, undefined, true), - ByteVector.fromString("PCNT", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("COM", StringType.UTF8, undefined, true), - ByteVector.fromString("COMM", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("CRA", StringType.UTF8, undefined, true), - ByteVector.fromString("AENC", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("ETC", StringType.UTF8, undefined, true), - ByteVector.fromString("ETCO", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("GEO", StringType.UTF8, undefined, true), - ByteVector.fromString("GEOB", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("IPL", StringType.UTF8, undefined, true), - ByteVector.fromString("TIPL", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("MCI", StringType.UTF8, undefined, true), - ByteVector.fromString("MCDI", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("MLL", StringType.UTF8, undefined, true), - ByteVector.fromString("MLLT", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("PIC", StringType.UTF8, undefined, true), - ByteVector.fromString("APIC", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("POP", StringType.UTF8, undefined, true), - ByteVector.fromString("POPM", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("REV", StringType.UTF8, undefined, true), - ByteVector.fromString("RVRB", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("SLT", StringType.UTF8, undefined, true), - ByteVector.fromString("SYLT", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("STC", StringType.UTF8, undefined, true), - ByteVector.fromString("SYTC", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TAL", StringType.UTF8, undefined, true), - ByteVector.fromString("TALB", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TBP", StringType.UTF8, undefined, true), - ByteVector.fromString("TBPM", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TCM", StringType.UTF8, undefined, true), - ByteVector.fromString("TCOM", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TCO", StringType.UTF8, undefined, true), - ByteVector.fromString("TCON", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TCP", StringType.UTF8, undefined, true), - ByteVector.fromString("TCMP", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TCR", StringType.UTF8, undefined, true), - ByteVector.fromString("TCOP", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TDA", StringType.UTF8, undefined, true), - ByteVector.fromString("TDAT", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TIM", StringType.UTF8, undefined, true), - ByteVector.fromString("TIME", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TDY", StringType.UTF8, undefined, true), - ByteVector.fromString("TDLY", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TEN", StringType.UTF8, undefined, true), - ByteVector.fromString("TENC", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TFT", StringType.UTF8, undefined, true), - ByteVector.fromString("TFLT", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TKE", StringType.UTF8, undefined, true), - ByteVector.fromString("TKEY", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TLA", StringType.UTF8, undefined, true), - ByteVector.fromString("TLAN", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TLE", StringType.UTF8, undefined, true), - ByteVector.fromString("TLEN", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TMT", StringType.UTF8, undefined, true), - ByteVector.fromString("TMED", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TOA", StringType.UTF8, undefined, true), - ByteVector.fromString("TOAL", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TOF", StringType.UTF8, undefined, true), - ByteVector.fromString("TOFN", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TOL", StringType.UTF8, undefined, true), - ByteVector.fromString("TOLY", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TOR", StringType.UTF8, undefined, true), - ByteVector.fromString("TDOR", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TOT", StringType.UTF8, undefined, true), - ByteVector.fromString("TOAL", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TP1", StringType.UTF8, undefined, true), - ByteVector.fromString("TPE1", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TP2", StringType.UTF8, undefined, true), - ByteVector.fromString("TPE2", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TP3", StringType.UTF8, undefined, true), - ByteVector.fromString("TPE3", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TP4", StringType.UTF8, undefined, true), - ByteVector.fromString("TPE4", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TPA", StringType.UTF8, undefined, true), - ByteVector.fromString("TPOS", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TPB", StringType.UTF8, undefined, true), - ByteVector.fromString("TPUB", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TRC", StringType.UTF8, undefined, true), - ByteVector.fromString("TSRC", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TRK", StringType.UTF8, undefined, true), - ByteVector.fromString("TRCK", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TSS", StringType.UTF8, undefined, true), - ByteVector.fromString("TSSE", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TT1", StringType.UTF8, undefined, true), - ByteVector.fromString("TIT1", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TT2", StringType.UTF8, undefined, true), - ByteVector.fromString("TIT2", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TT3", StringType.UTF8, undefined, true), - ByteVector.fromString("TIT3", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TXT", StringType.UTF8, undefined, true), - ByteVector.fromString("TOLY", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TXX", StringType.UTF8, undefined, true), - ByteVector.fromString("TXXX", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TYE", StringType.UTF8, undefined, true), - ByteVector.fromString("TDRC", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("UFI", StringType.UTF8, undefined, true), - ByteVector.fromString("UFID", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("ULT", StringType.UTF8, undefined, true), - ByteVector.fromString("USLT", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("WAF", StringType.UTF8, undefined, true), - ByteVector.fromString("WOAF", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("WAR", StringType.UTF8, undefined, true), - ByteVector.fromString("WOAR", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("WAS", StringType.UTF8, undefined, true), - ByteVector.fromString("WOAS", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("WCM", StringType.UTF8, undefined, true), - ByteVector.fromString("WCOM", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("WCP", StringType.UTF8, undefined, true), - ByteVector.fromString("WCOP", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("WPB", StringType.UTF8, undefined, true), - ByteVector.fromString("WPUB", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("WXX", StringType.UTF8, undefined, true), - ByteVector.fromString("WXXX", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("XRV", StringType.UTF8, undefined, true), - ByteVector.fromString("RVA2", StringType.UTF8, undefined, true) - ] - ]; - private static readonly version3Frames: ByteVector[][] = [ - [ - ByteVector.fromString("TORY", StringType.UTF8, undefined, true), - ByteVector.fromString("TDOR", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("TYER", StringType.UTF8, undefined, true), - ByteVector.fromString("TDRC", StringType.UTF8, undefined, true) - ], - [ - ByteVector.fromString("XRVA", StringType.UTF8, undefined, true), - ByteVector.fromString("RVA2", StringType.UTF8, undefined, true) - ] - ]; - - private readonly _version: number; - private _flags: Id3v2FrameFlags; - private _frameId: ByteVector; + private _frameId: FrameIdentifier; private _frameSize: number; - // #endregion + public constructor(id: FrameIdentifier, flags: Id3v2FrameFlags, frameSize: number) { + Guards.truthy(id, "id"); + Guards.uint(frameSize, "frameSize"); + + this._frameId = id; + this._flags = flags; + this._frameSize = frameSize; + } /** * Constructs and initializes a new instance of {@see FrameHeader} by reading it from raw @@ -323,56 +73,64 @@ export class Id3v2FrameHeader { * a frame identifer and the remaining values are zeroed. * @param version ID3v2 version with which the data in {@see data} was encoded. */ - public constructor(data: ByteVector, version: number) { + public static fromData(data: ByteVector, version: number): Id3v2FrameHeader { Guards.truthy(data, "data"); Guards.byte(version, "version"); + Guards.betweenInclusive(version, 2, 4, "version"); - this._flags = 0; - this._frameSize = 0; - - if (version < 2 || version > 4) { - throw new CorruptFileError("Unsupported tag version"); - } if (data.length < (version === 2 ? 3 : 4)) { - throw new CorruptFileError("Data must contain at least a frame ID"); + throw new Error("Data must contain at least a frame ID"); } - this._version = version; - + let frameId; + let flags = 0; + let frameSize = 0; switch (version) { case 2: + if (data.length < 3) { + throw new CorruptFileError("Data must contain at least a 3 byte frame identifier"); + } + // Set frame ID -- first 3 bytes - this._frameId = this.convertId(data.mid(0, 3), this._version, false); + frameId = FrameIdentifiers[data.toString(3, StringType.Latin1)]; // If the full header information was not passed in, do not continue to the steps // to parse the frame size and flags. if (data.length < 6) { - return; + break; } - this._frameSize = data.mid(3, 3).toUInt(); + frameSize = data.mid(3, 3).toUInt(); break; case 3: + if (data.length < 4) { + throw new CorruptFileError("Data must contain at least a 4 byte frame identifier"); + } + // Set the frame ID -- first 4 bytes - this._frameId = this.convertId(data.mid(0, 4), this._version, false); + frameId = FrameIdentifiers[data.toString(4, StringType.Latin1)]; // If the full header information was not passed in, do not continue to the steps // to parse the frame size and flags. if (data.length < 10) { - return; + break; } // Store the flags internally as version 2.4 - this._frameSize = data.mid(4, 4).toUInt(); - this._flags = ((data.get(8) << 7) & 0x7000) + frameSize = data.mid(4, 4).toUInt(); + flags = ((data.get(8) << 7) & 0x7000) | ((data.get(9) >> 4) & 0x000C) | ((data.get(9) << 1) & 0x0040); break; case 4: + if (data.length < 4) { + throw new CorruptFileError("Data must contain at least 4 byte frame identifier"); + } + // Set the frame ID -- the first 4 bytes - this._frameId = ByteVector.fromByteVector(data.mid(0, 4), true); + frameId = FrameIdentifiers[data.toString(4, StringType.Latin1)]; // If the full header information was not passed in, do not continue to the steps to // ... eh, you probably get it by now. @@ -380,13 +138,16 @@ export class Id3v2FrameHeader { return; } - this._frameSize = SyncData.toUint(data.mid(4, 4)); - this._flags = data.mid(8, 2).toUShort(); + frameSize = SyncData.toUint(data.mid(4, 4)); + flags = data.mid(8, 2).toUShort(); break; - - default: - throw new CorruptFileError("Unsupported tag version."); } + + return new Id3v2FrameHeader(frameId, flags, frameSize); + } + + public static fromFrameIdentifier(id: FrameIdentifier): Id3v2FrameHeader { + return new Id3v2FrameHeader(id, Id3v2FrameFlags.None, 0); } // #region Properties @@ -408,15 +169,13 @@ export class Id3v2FrameHeader { /** * Gets the identifier of the frame described by the current instance. */ - public get frameId(): ByteVector { return this._frameId; } + public get frameId(): FrameIdentifier { return this._frameId; } /** * Sets the identifier of the frame described by the current instance. */ - public set frameId(value: ByteVector) { + public set frameId(value: FrameIdentifier) { Guards.truthy(value, "value"); - this._frameId = value.length === 4 - ? ByteVector.fromByteVector(value, true) - : ByteVector.fromByteVector(value.mid(0, 4), true); + this._frameId = value; } /** @@ -432,11 +191,6 @@ export class Id3v2FrameHeader { this._frameSize = value; } - /** - * Gets the ID3v2 version this frame is encoded with - */ - public get version(): number { return this._version; } - // #endregion // #region Public Methods @@ -455,13 +209,11 @@ export class Id3v2FrameHeader { * @param version Version of ID3v2 to use when encoding the current instance. */ public render(version: number): ByteVector { - const data = ByteVector.empty(); - const id = this.convertId(this._frameId, version, true); + Guards.byte(version, "version"); + Guards.betweenInclusive(version, 2, 4, "version"); - if (!id) { - throw new NotImplementedError(); - } - data.addByteVector(id); + // Start by rendering the frame identifier + const data = this._frameId.render(version); switch (version) { case 2: @@ -481,48 +233,10 @@ export class Id3v2FrameHeader { data.addByteVector(SyncData.fromUint(this._frameSize)); data.addByteVector(ByteVector.fromUShort(this._flags)); break; - - default: - throw new NotImplementedError("Unsuppoted tag version."); } return data; } // #endregion - - private convertId(id: ByteVector, version: number, toVersion: boolean) { - if (version >= 4) { - return ByteVector.fromByteVector(id); - } - - if (!id || version < 2) { - return undefined; - } - - if (!toVersion && ( - ByteVector.equal(id, FrameType.EQUA) || - ByteVector.equal(id, FrameType.RVAD) || - ByteVector.equal(id, FrameType.TRDA) || - ByteVector.equal(id, FrameType.TSIZ) - )) { - return undefined; - } - - if (version === 2) { - const frame = Id3v2FrameHeader.version2Frames.find((f) => ByteVector.equal(f[toVersion ? 1 : 0], id)); - if (frame) { return frame[toVersion ? 0 : 1]; } - } - - if (version === 3) { - const frame = Id3v2FrameHeader.version3Frames.find((f) => ByteVector.equal(f[toVersion ? 1 : 0], id)); - if (frame) { return frame[toVersion ? 0 : 1]; } - } - - if ((id.length !== 4 && version > 2) || (id.length !== 3 && version === 2)) { - return undefined; - } - - return ByteVector.fromByteVector(id, true); - } } diff --git a/src/id3v2/header.ts b/src/id3v2/header.ts index a26c35fb..6fda5768 100644 --- a/src/id3v2/header.ts +++ b/src/id3v2/header.ts @@ -30,7 +30,7 @@ export default class Header { this._flags = data.get(5); // Make sure flags provided are legal - if (this._majorVersion === 2 && (this._flags & 63) != 0) { + if (this._majorVersion === 2 && (this._flags & 63) !== 0) { throw new CorruptFileError("Invalid flags set on version 2 tag"); } if (this._majorVersion === 3 && (this._flags & 15) > 0) { @@ -147,7 +147,7 @@ export default class Header { */ public set tagSize(value: number) { Guards.uint(value, "value"); - if ((value & 0xF0000000) != 0) { + if ((value & 0xF0000000) !== 0) { throw new Error("Argument out of range: value must be a 28-bit unsigned integer"); } From fb822ced3ed7298b2beae4316fc053c67c0cd616 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sun, 2 Feb 2020 15:55:53 -0500 Subject: [PATCH 36/71] More WIP 2 --- src/id3v2/footer.ts | 12 ++++---- src/id3v2/frames/attachmentFrame.ts | 32 ++++++++++++---------- src/id3v2/frames/commentsFrame.ts | 16 +++++++---- src/id3v2/frames/eventTimeCodeFrame.ts | 17 +++++++----- src/id3v2/frames/frame.ts | 17 +++++++----- src/id3v2/frames/frameHeader.ts | 10 ++++++- src/id3v2/frames/musicCdIdentifierFrame.ts | 14 ++++++---- src/id3v2/utilTypes.ts | 2 +- 8 files changed, 72 insertions(+), 48 deletions(-) diff --git a/src/id3v2/footer.ts b/src/id3v2/footer.ts index d77e684f..d22888b7 100644 --- a/src/id3v2/footer.ts +++ b/src/id3v2/footer.ts @@ -30,13 +30,13 @@ export default class Footer { this._flags = data.get(5); // TODO: Is there any point to supporting footers on versions less than 4? - if (this._majorVersion === 2 && (this._flags & 127) != 0) { + if (this._majorVersion === 2 && (this._flags & 127) !== 0) { throw new CorruptFileError("Invalid flags set on version 2 tag"); } - if (this._majorVersion === 3 && (this._flags & 15) != 0) { + if (this._majorVersion === 3 && (this._flags & 15) !== 0) { throw new CorruptFileError("Invalid flags set on version 3 tag"); } - if (this._majorVersion === 4 && (this._flags & 7) != 0) { + if (this._majorVersion === 4 && (this._flags & 7) !== 0) { throw new CorruptFileError("Invalid flags set on version 4 tag"); } @@ -75,11 +75,11 @@ export default class Footer { */ public set flags(value: HeaderFlags) { const version3Flags = HeaderFlags.ExtendedHeader | HeaderFlags.ExperimentalIndicator; - if ((value & version3Flags) != 0 && this.majorVersion < 3) { + if ((value & version3Flags) !== 0 && this.majorVersion < 3) { throw new Error("Feature only supported in version 2.3+"); } const version4Flags = HeaderFlags.FooterPresent; - if ((value & version4Flags) != 0 && this.majorVersion < 4) { + if ((value & version4Flags) !== 0 && this.majorVersion < 4) { throw new Error("Feature only supported in version 2.4+"); } @@ -137,7 +137,7 @@ export default class Footer { */ public set tagSize(value: number) { Guards.uint(value, "value"); - if ((value & 0xF0000000) != 0) { + if ((value & 0xF0000000) !== 0) { throw new Error("Argument out of range: value must be a 28-bit unsigned integer"); } diff --git a/src/id3v2/frames/attachmentFrame.ts b/src/id3v2/frames/attachmentFrame.ts index b34340ca..9b1157ec 100644 --- a/src/id3v2/frames/attachmentFrame.ts +++ b/src/id3v2/frames/attachmentFrame.ts @@ -2,8 +2,8 @@ import Id3v2TagSettings from "../id3v2TagSettings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; import {Frame, FrameClassType} from "./frame"; -import {FrameIdentifiers} from "../frameIdentifiers"; import {Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifiers} from "../frameIdentifiers"; import {IPicture, Picture, PictureType} from "../../picture"; import {Guards} from "../../utils"; @@ -30,18 +30,21 @@ export default class AttachmentFrame extends Frame { * @param data ByteVector containing the raw representation of the new frame * @param offset Index into {@paramref data} where the frame actually begins * @param header Header of the frame found at {@paramref offset} in the data + * @param version ID3v2 version the frame was originally encoded with */ public static fromOffsetRawData( data: ByteVector, offset: number, - header: Id3v2FrameHeader + header: Id3v2FrameHeader, + version: number ): AttachmentFrame { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); + Guards.byte(version, "version"); const frame = new AttachmentFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -73,8 +76,8 @@ export default class AttachmentFrame extends Frame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new AttachmentFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new AttachmentFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } @@ -159,8 +162,9 @@ export default class AttachmentFrame extends Frame { /** * Sets the text encoding to use when storing the current instance. * @param value Text encoding to use when storing the current instance. - * This encoding is overridden when rendering if {@see Id3v2Tag.ForceDefaultEncoding} is - * `true` or the render version does not support it. + * This encoding is overridden when rendering if + * {@see Id3v2TagSettings.forceDefaultEncoding} is `true` or the render version does not + * support it. */ public set textEncoding(value: StringType) { this.parseFromRaw(); @@ -184,8 +188,8 @@ export default class AttachmentFrame extends Frame { // Change the frame type depending if this is a picture or a general object const frameId = value === PictureType.NotAPicture - ? FrameType.GEOB - : FrameType.APIC; + ? FrameIdentifiers.GEOB + : FrameIdentifiers.APIC; if (this._header.frameId !== frameId) { this._header = new Id3v2FrameHeader(frameId, 4); } @@ -275,7 +279,7 @@ export default class AttachmentFrame extends Frame { const encoding = AttachmentFrame.correctEncoding(this.textEncoding, version); const data = ByteVector.empty(); - if (this._header.frameId === FrameType.APIC) { + if (this.frameId === FrameIdentifiers.APIC) { // Render an ID3v2 attached picture data.addByte(encoding); @@ -290,7 +294,7 @@ export default class AttachmentFrame extends Frame { data.addByte(this._type); data.addByteVector(ByteVector.fromString(this.description, encoding)); data.addByteVector(ByteVector.getTextDelimiter(encoding)); - } else if (this._header.frameId === FrameType.GEOB) { + } else if (this.frameId === FrameIdentifiers.GEOB) { // Make an ID3v2 general encapsulated object data.addByte(encoding); @@ -334,7 +338,7 @@ export default class AttachmentFrame extends Frame { const delim = ByteVector.getTextDelimiter(this._encoding); let descriptionEndIndex; - if (ByteVector.equal(this.frameId, FrameType.APIC)) { + if (this.frameId === FrameIdentifiers.APIC) { // Retrieve an ID3v2 attached picture if (this._rawVersion > 2) { // Text encoding $xx @@ -373,7 +377,7 @@ export default class AttachmentFrame extends Frame { } this._data = data.mid(descriptionEndIndex + delim.length); - } else if (ByteVector.equal(this.frameId, FrameType.GEOB)) { + } else if (this.frameId === FrameIdentifiers.GEOB) { // Retrieve an ID3v2 generic encapsulated object // Text encoding $xx // MIME type $00 @@ -418,7 +422,7 @@ export default class AttachmentFrame extends Frame { // Switch the frame ID if we discovered the attachment isn't an image if (this._type === PictureType.NotAPicture) { - this._header.frameId = FrameType.GEOB; + this._header.frameId = FrameIdentifiers.GEOB; } } } diff --git a/src/id3v2/frames/commentsFrame.ts b/src/id3v2/frames/commentsFrame.ts index 7aec1849..c15edae6 100644 --- a/src/id3v2/frames/commentsFrame.ts +++ b/src/id3v2/frames/commentsFrame.ts @@ -1,9 +1,9 @@ -import FrameTypes from "../frameIdentifiers"; import Id3v2TagSettings from "../id3v2TagSettings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifiers} from "../frameIdentifiers"; import {Guards} from "../../utils"; /** @@ -39,7 +39,7 @@ export default class CommentsFrame extends Frame { ): CommentsFrame { Guards.notNullOrUndefined(description, "description"); - const frame = new CommentsFrame(new Id3v2FrameHeader(FrameTypes.COMM, 4)); + const frame = new CommentsFrame(new Id3v2FrameHeader(FrameIdentifiers.COMM)); frame.textEncoding = encoding; frame._language = language; frame._description = description; @@ -54,18 +54,21 @@ export default class CommentsFrame extends Frame { * @param offset What offset in {@paramref data} the frame actually begins. Must be positive, * safe integer * @param header Header of the frame found at {@paramref data} in the data + * @param version ID3v2 version the frame was originally encoded with */ public static fromOffsetRawData( data: ByteVector, offset: number, - header: Id3v2FrameHeader + header: Id3v2FrameHeader, + version: number ): CommentsFrame { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); + Guards.byte(version, "version"); const frame = new CommentsFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -74,13 +77,14 @@ export default class CommentsFrame extends Frame { * ID3v2 version. * @param data Raw representation of the new frame * @param version ID3v2 version the raw frame is encoded with, must be a positive 8-bit integer + * @param version ID3v2 version the frame was originally encoded with */ public static fromRawData(data: ByteVector, version: number): CommentsFrame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new CommentsFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new CommentsFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } diff --git a/src/id3v2/frames/eventTimeCodeFrame.ts b/src/id3v2/frames/eventTimeCodeFrame.ts index 75abc70f..18ccafa4 100644 --- a/src/id3v2/frames/eventTimeCodeFrame.ts +++ b/src/id3v2/frames/eventTimeCodeFrame.ts @@ -1,7 +1,7 @@ -import FrameTypes from "../frameIdentifiers"; import {ByteVector} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameFlags, Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifiers} from "../frameIdentifiers"; import {Guards} from "../../utils"; import {EventType, TimestampFormat} from "../utilTypes"; @@ -58,7 +58,7 @@ export class EventTimeCodeFrame extends Frame { * Constructs and initializes a new instance without contents */ public static fromEmpty(): EventTimeCodeFrame { - const frame = new EventTimeCodeFrame(new Id3v2FrameHeader(FrameTypes.ETCO, 4)); + const frame = new EventTimeCodeFrame(new Id3v2FrameHeader(FrameIdentifiers.ETCO)); frame.flags = Id3v2FrameFlags.FileAlterPreservation; return frame; } @@ -68,7 +68,7 @@ export class EventTimeCodeFrame extends Frame { * @param timestampFormat Timestamp format for the event codes stored in this frame */ public static fromTimestampFormat(timestampFormat: TimestampFormat): EventTimeCodeFrame { - const frame = new EventTimeCodeFrame(new Id3v2FrameHeader(FrameTypes.ETCO, 4)); + const frame = new EventTimeCodeFrame(new Id3v2FrameHeader(FrameIdentifiers.ETCO)); frame.flags = Id3v2FrameFlags.FileAlterPreservation; frame.timestampFormat = timestampFormat; return frame; @@ -81,18 +81,21 @@ export class EventTimeCodeFrame extends Frame { * @param offset What offset in {@paramref data} the frame actually begins. Must be positive, * safe integer * @param header Header of the frame found at {@paramref data} in the data + * @param version ID3v2 version the frame was originally encoded with */ public static fromOffsetRawData( data: ByteVector, offset: number, - header: Id3v2FrameHeader + header: Id3v2FrameHeader, + version: number ) { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); + Guards.byte(version, "version"); const frame = new EventTimeCodeFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -106,8 +109,8 @@ export class EventTimeCodeFrame extends Frame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new EventTimeCodeFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new EventTimeCodeFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } diff --git a/src/id3v2/frames/frame.ts b/src/id3v2/frames/frame.ts index 2de96250..7ee88c52 100644 --- a/src/id3v2/frames/frame.ts +++ b/src/id3v2/frames/frame.ts @@ -5,6 +5,7 @@ import {CorruptFileError} from "../../errors"; import {Id3v2FrameFlags, Id3v2FrameHeader} from "./frameHeader"; import {Guards} from "../../utils"; import {NotImplementedError} from "../../errors"; +import {FrameIdentifier} from "../frameIdentifiers"; export enum FrameClassType { AttachmentFrame, @@ -89,9 +90,9 @@ export abstract class Frame { /** * Gets the frame ID for the current instance. - * @returns ByteVector Object containing the four-byte ID3v2.4 frame header for this frame + * @returns FrameIdentifier Object representing of the identifier of the frame */ - public get frameId(): ByteVector { return this._header.frameId; } + public get frameId(): FrameIdentifier { return this._header.frameId; } /** * Gets the grouping ID applied to the current instance. @@ -227,9 +228,10 @@ export abstract class Frame { * grouping ID. * @param frameData Raw frame data * @param offset Index at which the data is contained + * @param version Version of the ID3v2 tag the data was originally encoded with */ - protected fieldData(frameData: ByteVector, offset: number): ByteVector { - let dataOffset = offset + Id3v2FrameHeader.getSize(this._header.version); + protected fieldData(frameData: ByteVector, offset: number, version: number): ByteVector { + let dataOffset = offset + Id3v2FrameHeader.getSize(version); let dataLength = this.size; if ((this.flags & (Id3v2FrameFlags.Compression | Id3v2FrameFlags.DataLengthIndicator)) !== 0) { @@ -296,12 +298,13 @@ export abstract class Frame { * @param data Raw ID3v2 frame * @param offset Offset in {@paramref data} at which the frame begins. * @param readHeader Whether or not to read the reader into the current instance. + * @param version Version of the ID3v2 tag the data was encoded with */ - protected setData(data: ByteVector, offset: number, readHeader: boolean): void { + protected setData(data: ByteVector, offset: number, readHeader: boolean, version: number): void { if (readHeader) { - this._header = new Id3v2FrameHeader(data, this._header.version); + this._header = Id3v2FrameHeader.fromData(data, version); } - this.parseFields(this.fieldData(data, offset), this._header.version); + this.parseFields(this.fieldData(data, offset, version), version); } // #endregion diff --git a/src/id3v2/frames/frameHeader.ts b/src/id3v2/frames/frameHeader.ts index 5fa0751f..45d52698 100644 --- a/src/id3v2/frames/frameHeader.ts +++ b/src/id3v2/frames/frameHeader.ts @@ -56,7 +56,15 @@ export class Id3v2FrameHeader { private _frameId: FrameIdentifier; private _frameSize: number; - public constructor(id: FrameIdentifier, flags: Id3v2FrameFlags, frameSize: number) { + /** + * Constructs and initializes a new instance by prociding the data for the frame header. + * @param id Identifier of the frame + * @param flags Flags to assign to the frame (if omitted, defaults to + * {@see Id3v2FrameFlags.None}) + * @param frameSize Size of the frame in bytes, excluding the size of the header (if omitted, + * defaults to 0) + */ + public constructor(id: FrameIdentifier, flags: Id3v2FrameFlags = Id3v2FrameFlags.None, frameSize: number = 0) { Guards.truthy(id, "id"); Guards.uint(frameSize, "frameSize"); diff --git a/src/id3v2/frames/musicCdIdentifierFrame.ts b/src/id3v2/frames/musicCdIdentifierFrame.ts index d99ca7be..f1a1f757 100644 --- a/src/id3v2/frames/musicCdIdentifierFrame.ts +++ b/src/id3v2/frames/musicCdIdentifierFrame.ts @@ -1,7 +1,7 @@ -import FrameTypes from "../frameIdentifiers"; import {ByteVector} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifiers} from "../frameIdentifiers"; import {Guards} from "../../utils"; /** @@ -23,14 +23,16 @@ export default class MusicCdIdentifierFrame extends Frame { * @param offset Offset into {@paramref data} where the frame actually begins. Must be a * positive, safe integer * @param header Header of the frame found at {@paramref offset} in the data + * @param version ID3v2 version the frame was originally encoded with */ - public static fromOffsetRawData(data: ByteVector, offset: number, header: Id3v2FrameHeader) { + public static fromOffsetRawData(data: ByteVector, offset: number, header: Id3v2FrameHeader, version: number) { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); + Guards.byte(version, "version"); const frame = new MusicCdIdentifierFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -44,8 +46,8 @@ export default class MusicCdIdentifierFrame extends Frame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new MusicCdIdentifierFrame(new Id3v2FrameHeader(FrameTypes.MCDI, 4)); - frame.setData(data, 0, true); + const frame = new MusicCdIdentifierFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } @@ -64,7 +66,7 @@ export default class MusicCdIdentifierFrame extends Frame { /** @inheritDoc */ public clone(): Frame { - const frame = new MusicCdIdentifierFrame(new Id3v2FrameHeader(FrameTypes.MCDI, 4)); + const frame = new MusicCdIdentifierFrame(new Id3v2FrameHeader(FrameIdentifiers.MCDI)); if (this.data) { frame.data = ByteVector.fromByteVector(this.data); } diff --git a/src/id3v2/utilTypes.ts b/src/id3v2/utilTypes.ts index cbe5089f..9cd41cc8 100644 --- a/src/id3v2/utilTypes.ts +++ b/src/id3v2/utilTypes.ts @@ -193,4 +193,4 @@ export enum EventType { * End of the audio file */ AudioFileEnd = 0xFE -} \ No newline at end of file +} From 94fba4a9d4f14aa2df7349cb4bcba9e4e0092a0b Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Mon, 3 Feb 2020 20:52:28 -0500 Subject: [PATCH 37/71] Converting rest of the frame classes --- src/id3v2/frames/playCountFrame.ts | 27 ++++-- src/id3v2/frames/popularimeterFrame.ts | 15 ++-- src/id3v2/frames/privateFrame.ts | 15 ++-- src/id3v2/frames/relativeVolumeFrame.ts | 17 ++-- src/id3v2/frames/synchronizedLyricsFrame.ts | 13 +-- src/id3v2/frames/termsOfUseFrame.ts | 15 ++-- src/id3v2/frames/textInformationFrame.ts | 85 ++++++++++--------- src/id3v2/frames/uniqueFileIdentifierFrame.ts | 16 ++-- src/id3v2/frames/unknownFrame.ts | 20 +++-- src/id3v2/frames/unsynchronizedLyricsFrame.ts | 15 ++-- src/id3v2/frames/urlLinkFrame.ts | 42 ++++----- 11 files changed, 160 insertions(+), 120 deletions(-) diff --git a/src/id3v2/frames/playCountFrame.ts b/src/id3v2/frames/playCountFrame.ts index f43d31ca..463334fb 100644 --- a/src/id3v2/frames/playCountFrame.ts +++ b/src/id3v2/frames/playCountFrame.ts @@ -1,8 +1,8 @@ import * as BigInt from "big-integer"; -import FrameTypes from "../frameIdentifiers"; import {ByteVector} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifiers} from "../frameIdentifiers"; import {Guards} from "../../utils"; /** @@ -22,20 +22,31 @@ export default class PlayCountFrame extends Frame { * Constructs and initializes a new instance with a count of zero */ public static fromEmpty(): PlayCountFrame { - return new PlayCountFrame(new Id3v2FrameHeader(FrameTypes.PCNT, 4)); + return new PlayCountFrame(new Id3v2FrameHeader(FrameIdentifiers.PCNT)); } + /** + * Constructs and initializes a new instance of frame by reading its raw data in a specified + * ID3v2 version starting at a specified offset. + * @param data Raw representation of the new frame. + * @param offset Offset into {@paramref data} where the frame actually begins. Must be a + * positive, safe integer + * @param header Header of the frame found at {@paramref offset} in the data + * @param version ID3v2 version the frame was originally encoded with + */ public static fromOffsetRawData( data: ByteVector, offset: number, - header: Id3v2FrameHeader + header: Id3v2FrameHeader, + version: number ): PlayCountFrame { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); + Guards.byte(version, "version"); const frame = new PlayCountFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -43,14 +54,14 @@ export default class PlayCountFrame extends Frame { * Constructs and initializes a new instance by reading its raw data in a specified ID3v2 * version * @param data ByteVector starting with the raw representation of the new frame - * @param version ID3v2 veersion the raw frame is encoded in, must be a positive 8-bit integer + * @param version ID3v2 version the raw frame is encoded in, must be a positive 8-bit integer */ public static fromRawData(data: ByteVector, version: number): PlayCountFrame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new PlayCountFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new PlayCountFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } @@ -78,7 +89,7 @@ export default class PlayCountFrame extends Frame { /** @inheritDoc */ public clone(): Frame { - const frame = new PlayCountFrame(new Id3v2FrameHeader(FrameTypes.PCNT, 4)); + const frame = new PlayCountFrame(new Id3v2FrameHeader(FrameIdentifiers.PCNT)); frame.playCount = this.playCount; return frame; } diff --git a/src/id3v2/frames/popularimeterFrame.ts b/src/id3v2/frames/popularimeterFrame.ts index d46174bd..2a3caf88 100644 --- a/src/id3v2/frames/popularimeterFrame.ts +++ b/src/id3v2/frames/popularimeterFrame.ts @@ -1,9 +1,9 @@ import * as BigInt from "big-integer"; -import FrameTypes from "../frameIdentifiers"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifiers} from "../frameIdentifiers"; import {Guards} from "../../utils"; /** @@ -27,18 +27,21 @@ export default class PopularimeterFrame extends Frame { * @param offset What offset in {@paramref data} the frame actually begins. Must be positive, * safe integer * @param header Header of the frame found at {@paramref data} in the data + * @param version ID3v2 version the frame was originally encoded with */ public static fromOffsetRawData( data: ByteVector, offset: number, - header: Id3v2FrameHeader + header: Id3v2FrameHeader, + version: number ): PopularimeterFrame { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); + Guards.byte(version, "version"); const frame = new PopularimeterFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -52,8 +55,8 @@ export default class PopularimeterFrame extends Frame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new PopularimeterFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new PopularimeterFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } @@ -63,7 +66,7 @@ export default class PopularimeterFrame extends Frame { * @param user Email of the user that gave the rating */ public static fromUser(user: string): PopularimeterFrame { - const frame = new PopularimeterFrame(new Id3v2FrameHeader(FrameTypes.POPM, 4)); + const frame = new PopularimeterFrame(new Id3v2FrameHeader(FrameIdentifiers.POPM)); frame.user = user; return frame; } diff --git a/src/id3v2/frames/privateFrame.ts b/src/id3v2/frames/privateFrame.ts index 2c675fff..ab06cac0 100644 --- a/src/id3v2/frames/privateFrame.ts +++ b/src/id3v2/frames/privateFrame.ts @@ -1,8 +1,8 @@ -import FrameTypes from "../frameIdentifiers"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError, NotImplementedError} from "../../errors"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifiers} from "../frameIdentifiers"; import {Guards} from "../../utils"; /** @@ -25,7 +25,7 @@ export default class PrivateFrame extends Frame { * @param owner Owner of the private frame */ public static fromOwner(owner: string): PrivateFrame { - const frame = new PrivateFrame(new Id3v2FrameHeader(FrameTypes.PRIV, 4)); + const frame = new PrivateFrame(new Id3v2FrameHeader(FrameIdentifiers.PRIV)); frame._owner = owner; frame._privateData = ByteVector.empty(); return frame; @@ -38,18 +38,21 @@ export default class PrivateFrame extends Frame { * @param offset What offset in {@paramref data} the frame actually begins. Must be positive, * safe integer * @param header Header of the frame found at {@paramref data} in the data + * @param version ID3v2 version the frame was originally encoded with */ public static fromOffsetRawData( data: ByteVector, offset: number, - header: Id3v2FrameHeader + header: Id3v2FrameHeader, + version: number ): PrivateFrame { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); + Guards.byte(version, "version"); const frame = new PrivateFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -63,8 +66,8 @@ export default class PrivateFrame extends Frame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new PrivateFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new PrivateFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } diff --git a/src/id3v2/frames/relativeVolumeFrame.ts b/src/id3v2/frames/relativeVolumeFrame.ts index 8d1f6b6d..30376b29 100644 --- a/src/id3v2/frames/relativeVolumeFrame.ts +++ b/src/id3v2/frames/relativeVolumeFrame.ts @@ -1,8 +1,8 @@ import * as BigInt from "big-integer"; -import FrameTypes from "../frameIdentifiers"; import {ByteVector, StringType} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifiers} from "../frameIdentifiers"; import {Guards} from "../../utils"; /** @@ -56,7 +56,7 @@ export enum ChannelType { } export class ChannelData { - private _channel: ChannelType; + private readonly _channel: ChannelType; private _peakBits: number; private _peakVolume: BigInt.BigInteger; private _volumeAdjustment: number; @@ -181,7 +181,7 @@ export class RelativeVolumeFrame extends Frame { * @param identification Identification ot use for the new frame */ public static fromIdentification(identification: string): RelativeVolumeFrame { - const frame = new RelativeVolumeFrame(new Id3v2FrameHeader(FrameTypes.RVA2, 4)); + const frame = new RelativeVolumeFrame(new Id3v2FrameHeader(FrameIdentifiers.RVA2)); frame._identification = identification; return frame; } @@ -193,18 +193,21 @@ export class RelativeVolumeFrame extends Frame { * @param offset Offset into {@paramref data} where the frame actually begins. Must be a * positive, 32-bit integer * @param header Header of the frame found at {@paramref offset} in {@paramref data} + * @param version ID3v2 version the frame was originally encoded with */ public static fromOffsetRawData( data: ByteVector, offset: number, - header: Id3v2FrameHeader + header: Id3v2FrameHeader, + version: number ): RelativeVolumeFrame { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); + Guards.byte(version, "version"); const frame = new RelativeVolumeFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -218,8 +221,8 @@ export class RelativeVolumeFrame extends Frame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new RelativeVolumeFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new RelativeVolumeFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } diff --git a/src/id3v2/frames/synchronizedLyricsFrame.ts b/src/id3v2/frames/synchronizedLyricsFrame.ts index 93c9f1f9..183bc2f4 100644 --- a/src/id3v2/frames/synchronizedLyricsFrame.ts +++ b/src/id3v2/frames/synchronizedLyricsFrame.ts @@ -1,9 +1,9 @@ -import FrameTypes from "../frameIdentifiers"; import Id3v2TagSettings from "../id3v2TagSettings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifiers} from "../frameIdentifiers"; import {Guards} from "../../utils"; import {SynchronizedTextType, TimestampFormat} from "../utilTypes"; @@ -82,7 +82,7 @@ export class SynchronizedLyricsFrame extends Frame { textType: SynchronizedTextType, encoding: StringType = Id3v2TagSettings.defaultEncoding ): SynchronizedLyricsFrame { - const frame = new SynchronizedLyricsFrame(new Id3v2FrameHeader(FrameTypes.SYLT, 4)); + const frame = new SynchronizedLyricsFrame(new Id3v2FrameHeader(FrameIdentifiers.SYLT)); frame.textEncoding = encoding; frame._language = language; frame.description = description; @@ -97,14 +97,15 @@ export class SynchronizedLyricsFrame extends Frame { * @param offset Offset into {@paramref data} where the frame begins. Must be unsigned, safe * integer * @param header Header of the frame found at {@paramref offset} in {@paramref data} + * @param version ID3v2 version the frame was originally encoded with */ - public static fromOffsetRawData(data: ByteVector, offset: number, header: Id3v2FrameHeader) { + public static fromOffsetRawData(data: ByteVector, offset: number, header: Id3v2FrameHeader, version: number) { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); const frame = new SynchronizedLyricsFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -118,8 +119,8 @@ export class SynchronizedLyricsFrame extends Frame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new SynchronizedLyricsFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new SynchronizedLyricsFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } diff --git a/src/id3v2/frames/termsOfUseFrame.ts b/src/id3v2/frames/termsOfUseFrame.ts index 4a81f305..1f5fd003 100644 --- a/src/id3v2/frames/termsOfUseFrame.ts +++ b/src/id3v2/frames/termsOfUseFrame.ts @@ -1,9 +1,9 @@ -import FrameTypes from "../frameIdentifiers"; import Id3v2TagSettings from "../id3v2TagSettings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifiers} from "../frameIdentifiers"; import {Guards} from "../../utils"; export default class TermsOfUseFrame extends Frame { @@ -27,7 +27,7 @@ export default class TermsOfUseFrame extends Frame { language: string, textEncoding: StringType = Id3v2TagSettings.defaultEncoding ): TermsOfUseFrame { - const f = new TermsOfUseFrame(new Id3v2FrameHeader(FrameTypes.USER, 4)); + const f = new TermsOfUseFrame(new Id3v2FrameHeader(FrameIdentifiers.USER)); f.textEncoding = textEncoding; f._language = language; return f; @@ -40,18 +40,21 @@ export default class TermsOfUseFrame extends Frame { * @param offset What offset in {@paramref data} the frame actually begins. Must be positive, * safe integer * @param header Header of the frame found at {@paramref data} in the data + * @param version ID3v2 version the frame was originally encoded with */ public static fromOffsetRawData( data: ByteVector, offset: number, - header: Id3v2FrameHeader + header: Id3v2FrameHeader, + version: number ): TermsOfUseFrame { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); + Guards.byte(version, "version"); const frame = new TermsOfUseFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -65,8 +68,8 @@ export default class TermsOfUseFrame extends Frame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new TermsOfUseFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new TermsOfUseFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } diff --git a/src/id3v2/frames/textInformationFrame.ts b/src/id3v2/frames/textInformationFrame.ts index 6c039d73..9ca0bcb5 100644 --- a/src/id3v2/frames/textInformationFrame.ts +++ b/src/id3v2/frames/textInformationFrame.ts @@ -1,9 +1,9 @@ -import FrameTypes from "../frameIdentifiers"; import Genres from "../../genres"; import Id3v2TagSettings from "../id3v2TagSettings"; import {ByteVector, StringType} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifier, FrameIdentifiers} from "../frameIdentifiers"; import {Guards, StringComparison} from "../../utils"; /** @@ -144,15 +144,15 @@ export class TextInformationFrame extends Frame { /** * Constructs and initializes a new instance with a specified identifier - * @param ident Byte vector containing the identifier for the frame + * @param identifier Byte vector containing the identifier for the frame * @param encoding Optionally, the encoding to use for the new instance. If omitted, defaults * to {@see Id3v2Tag.defaultEncoding} */ public static fromIdentifier( - ident: ByteVector, + identifier: FrameIdentifier, encoding: StringType = Id3v2TagSettings.defaultEncoding ): TextInformationFrame { - const frame = new TextInformationFrame(new Id3v2FrameHeader(ident, 4)); + const frame = new TextInformationFrame(new Id3v2FrameHeader(identifier)); frame._encoding = encoding; return frame; } @@ -164,18 +164,20 @@ export class TextInformationFrame extends Frame { * @param offset What offset in {@paramref data} the frame actually begins. Must be positive, * safe integer * @param header Header of the frame found at {@paramref data} in the data + * @param version ID3v2 version the frame was originally encoded with */ public static fromOffsetRawData( data: ByteVector, offset: number, - header: Id3v2FrameHeader + header: Id3v2FrameHeader, + version: number ): TextInformationFrame { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); const frame = new TextInformationFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -189,8 +191,8 @@ export class TextInformationFrame extends Frame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new TextInformationFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new TextInformationFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } @@ -248,15 +250,12 @@ export class TextInformationFrame extends Frame { */ public static findTextInformationFrame( frames: TextInformationFrame[], - ident: ByteVector + ident: FrameIdentifier ): TextInformationFrame { Guards.truthy(frames, "frames"); Guards.truthy(ident, "ident"); - if (ident.length !== 4) { - throw new Error("Argument out of range: Identifier must be four bytes long"); - } - return frames.find((f) => ByteVector.equal(f.frameId, ident)); + return frames.find((f) => f.frameId === ident); } /** @inheritDoc */ @@ -279,7 +278,7 @@ export class TextInformationFrame extends Frame { public render(version: number): ByteVector { Guards.byte(version, "version"); - if (version !== 3 || this.frameId !== FrameTypes.TDRC) { + if (version !== 3 || this.frameId !== FrameIdentifiers.TDRC) { return super.render(version); } @@ -289,11 +288,11 @@ export class TextInformationFrame extends Frame { } const output = ByteVector.empty(); - let frame = new TextInformationFrame(new Id3v2FrameHeader(FrameTypes.TYER, this._encoding)); + let frame = new TextInformationFrame(new Id3v2FrameHeader(FrameIdentifiers.TYER)); frame.text = [text.substring(0, 4)]; output.addByteVector(frame.render(version)); - frame = new TextInformationFrame(new Id3v2FrameHeader(FrameTypes.TDAT, this._encoding)); + frame = new TextInformationFrame(new Id3v2FrameHeader(FrameIdentifiers.TDAT)); frame.text = [text.substring(5, 7) + text.substring(8, 10)]; output.addByteVector(frame.render(version)); @@ -301,7 +300,7 @@ export class TextInformationFrame extends Frame { return output; } - frame = new TextInformationFrame(new Id3v2FrameHeader(FrameTypes.TIME, this._encoding)); + frame = new TextInformationFrame(new Id3v2FrameHeader(FrameIdentifiers.TIME)); frame.text = [text.substring(11, 13) + text.substring(14, 16)]; output.addByteVector(frame.render(version)); @@ -347,7 +346,7 @@ export class TextInformationFrame extends Frame { const fieldList = []; const delim = ByteVector.getTextDelimiter(this._encoding); - if (this._rawVersion > 3 || ByteVector.equal(this.frameId, FrameTypes.TXXX)) { + if (this._rawVersion > 3 || this.frameId === FrameIdentifiers.TXXX) { fieldList.push(... data.toStrings(this._encoding, 1)); } else if (data.length > 1 && !ByteVector.equal(data.mid(1, delim.length), delim)) { let value = data.toString(data.length - 1, this._encoding, 1); @@ -359,23 +358,23 @@ export class TextInformationFrame extends Frame { } const splitFrameTypes = [ - FrameTypes.TCOM, - FrameTypes.TEXT, - FrameTypes.TMCL, - FrameTypes.TOLY, - FrameTypes.TOPE, - FrameTypes.TSOC, - FrameTypes.TSOP, - FrameTypes.TSO2, - FrameTypes.TPE1, - FrameTypes.TPE2, - FrameTypes.TPE3, - FrameTypes.TPE4 + FrameIdentifiers.TCOM, + FrameIdentifiers.TEXT, + FrameIdentifiers.TMCL, + FrameIdentifiers.TOLY, + FrameIdentifiers.TOPE, + FrameIdentifiers.TSOC, + FrameIdentifiers.TSOP, + FrameIdentifiers.TSO2, + FrameIdentifiers.TPE1, + FrameIdentifiers.TPE2, + FrameIdentifiers.TPE3, + FrameIdentifiers.TPE4 ]; - if (splitFrameTypes.some((ft) => ByteVector.equal(ft, this.frameId))) { + if (splitFrameTypes.some((ft) => ft === this.frameId)) { // Some frames are designed to be split into multiple parts by a / fieldList.push(... value.split("/")); - } else if (ByteVector.equal(this.frameId, FrameTypes.TCON)) { + } else if (this.frameId === FrameIdentifiers.TCON) { // TCON can take various formats. The ID3v2.3 docs specify it can be: // * (xx) - where xx is a number from the ID3v1 genre list // * (xx)yyy - where xx is a number from the ID3v1 genre list and yyy is a @@ -453,7 +452,7 @@ export class TextInformationFrame extends Frame { v.addByte(encoding); - const isTxxx = ByteVector.equal(this.frameId, FrameTypes.TXXX); + const isTxxx = this.frameId === FrameIdentifiers.TXXX; if (version > 3 || isTxxx) { if (isTxxx) { if (text.length === 0) { @@ -474,7 +473,7 @@ export class TextInformationFrame extends Frame { v.addByteVector(ByteVector.fromString(text[i], encoding)); } } - } else if (ByteVector.equal(this.frameId, FrameTypes.TCON)) { + } else if (this.frameId === FrameIdentifiers.TCON) { let data = ""; for (const s of text) { if (s === TextInformationFrame.COVER_STRING) { @@ -515,7 +514,10 @@ export class TextInformationFrame extends Frame { previousStrings.push(output); } else { // String is a genre, only store it if the genre number isn't stored in the previous strings - if (previousStrings.length === 0 || previousStrings[previousStrings.length - 1] !== genreNumber.toString(10)) { + if ( + previousStrings.length === 0 || + previousStrings[previousStrings.length - 1] !== genreNumber.toString(10) + ) { previousStrings.push(output); } } @@ -540,7 +542,7 @@ export class UserTextInformationFrame extends TextInformationFrame { description: string, encoding: StringType = Id3v2TagSettings.defaultEncoding ): UserTextInformationFrame { - const frame = new UserTextInformationFrame(new Id3v2FrameHeader(FrameTypes.TXXX, 4)); + const frame = new UserTextInformationFrame(new Id3v2FrameHeader(FrameIdentifiers.TXXX)); frame._encoding = encoding; frame.description = description; return frame; @@ -553,18 +555,21 @@ export class UserTextInformationFrame extends TextInformationFrame { * @param offset What offset in {@paramref data} the frame actually begins. Must be positive, * safe integer * @param header Header of the frame found at {@paramref data} in the data + * @param version ID3v2 version the frame was originally encoded with */ public static fromOffsetRawData( data: ByteVector, offset: number, - header: Id3v2FrameHeader + header: Id3v2FrameHeader, + version: number ): UserTextInformationFrame { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); + Guards.byte(version, "version"); const frame = new UserTextInformationFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -578,8 +583,8 @@ export class UserTextInformationFrame extends TextInformationFrame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new UserTextInformationFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new UserTextInformationFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } diff --git a/src/id3v2/frames/uniqueFileIdentifierFrame.ts b/src/id3v2/frames/uniqueFileIdentifierFrame.ts index 6fc05e3e..8b32dd37 100644 --- a/src/id3v2/frames/uniqueFileIdentifierFrame.ts +++ b/src/id3v2/frames/uniqueFileIdentifierFrame.ts @@ -1,7 +1,7 @@ -import FrameTypes from "../frameIdentifiers"; import {ByteVector, StringType} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifiers} from "../frameIdentifiers"; import {Guards} from "../../utils"; /** @@ -29,7 +29,7 @@ export default class UniqueFileIdentifierFrame extends Frame { throw new Error("Argument out of range: Identifier cannot be longer than 64 bytes"); } - const frame = new UniqueFileIdentifierFrame(new Id3v2FrameHeader(FrameTypes.UFID, 4)); + const frame = new UniqueFileIdentifierFrame(new Id3v2FrameHeader(FrameIdentifiers.UFID)); frame._owner = owner; frame._identifier = identifier; return frame; @@ -42,18 +42,20 @@ export default class UniqueFileIdentifierFrame extends Frame { * @param offset What offset in {@paramref data} the frame actually begins. Must be positive, * safe integer * @param header Header of the frame found at {@paramref data} in the data + * @param version ID3v2 version the frame was originally encoded with */ public static fromOffsetRawData( data: ByteVector, offset: number, - header: Id3v2FrameHeader + header: Id3v2FrameHeader, + version: number ): UniqueFileIdentifierFrame { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); const frame = new UniqueFileIdentifierFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -67,8 +69,8 @@ export default class UniqueFileIdentifierFrame extends Frame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new UniqueFileIdentifierFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new UniqueFileIdentifierFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } @@ -117,7 +119,7 @@ export default class UniqueFileIdentifierFrame extends Frame { /** @inheritDoc */ public clone(): Frame { - const frame = new UniqueFileIdentifierFrame(new Id3v2FrameHeader(FrameTypes.UFID, 4)); + const frame = new UniqueFileIdentifierFrame(new Id3v2FrameHeader(FrameIdentifiers.UFID, 4)); frame._owner = this._owner; if (this._identifier) { frame.identifier = ByteVector.fromByteVector(this.identifier); diff --git a/src/id3v2/frames/unknownFrame.ts b/src/id3v2/frames/unknownFrame.ts index 16b6079c..fe1ad36e 100644 --- a/src/id3v2/frames/unknownFrame.ts +++ b/src/id3v2/frames/unknownFrame.ts @@ -1,6 +1,7 @@ import {ByteVector} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifier} from "../frameIdentifiers"; import {Guards} from "../../utils"; /** @@ -13,13 +14,13 @@ export default class UnknownFrame extends Frame { /** * Constructs and initializes a new instance with a specified type - * @param type ID3v2.4 frame identifier + * @param identifier ID3v2 frame identifier * @param data Contents of the frame */ - public static fromData(type: ByteVector, data?: ByteVector): UnknownFrame { - Guards.truthy(type, "type"); + public static fromData(identifier: FrameIdentifier, data?: ByteVector): UnknownFrame { + Guards.truthy(identifier, "identifier"); - const frame = new UnknownFrame(new Id3v2FrameHeader(type, 4)); + const frame = new UnknownFrame(new Id3v2FrameHeader(identifier)); if (data) { frame.data = data; } else { @@ -35,18 +36,21 @@ export default class UnknownFrame extends Frame { * @param offset What offset in {@paramref data} the frame actually begins. Must be positive, * safe integer * @param header Header of the frame found at {@paramref data} in the data + * @param version ID3v2 version the frame was originally encoded with */ public static fromOffsetRawData( data: ByteVector, offset: number, - header: Id3v2FrameHeader + header: Id3v2FrameHeader, + version: number ): UnknownFrame { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); + Guards.byte(version, "version"); const frame = new UnknownFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -60,8 +64,8 @@ export default class UnknownFrame extends Frame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new UnknownFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new UnknownFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } diff --git a/src/id3v2/frames/unsynchronizedLyricsFrame.ts b/src/id3v2/frames/unsynchronizedLyricsFrame.ts index 433b0266..326dbc2c 100644 --- a/src/id3v2/frames/unsynchronizedLyricsFrame.ts +++ b/src/id3v2/frames/unsynchronizedLyricsFrame.ts @@ -1,9 +1,9 @@ -import FrameTypes from "../frameIdentifiers"; import Id3v2TagSettings from "../id3v2TagSettings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifiers} from "../frameIdentifiers"; import {Guards} from "../../utils"; export default class UnsynchronizedLyricsFrame extends Frame { @@ -29,7 +29,7 @@ export default class UnsynchronizedLyricsFrame extends Frame { language?: string, encoding: StringType = Id3v2TagSettings.defaultEncoding ): UnsynchronizedLyricsFrame { - const frame = new UnsynchronizedLyricsFrame(new Id3v2FrameHeader(FrameTypes.USLT, 4)); + const frame = new UnsynchronizedLyricsFrame(new Id3v2FrameHeader(FrameIdentifiers.USLT)); frame.textEncoding = encoding; frame._language = language; frame._description = description; @@ -43,18 +43,21 @@ export default class UnsynchronizedLyricsFrame extends Frame { * @param offset What offset in {@paramref data} the frame actually begins. Must be positive, * safe integer * @param header Header of the frame found at {@paramref data} in the data + * @param version ID3v2 version the frame was originally encoded with */ public static fromOffsetRawData( data: ByteVector, offset: number, - header: Id3v2FrameHeader + header: Id3v2FrameHeader, + version: number ): UnsynchronizedLyricsFrame { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); + Guards.byte(version, "version"); const frame = new UnsynchronizedLyricsFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -68,8 +71,8 @@ export default class UnsynchronizedLyricsFrame extends Frame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new UnsynchronizedLyricsFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new UnsynchronizedLyricsFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } diff --git a/src/id3v2/frames/urlLinkFrame.ts b/src/id3v2/frames/urlLinkFrame.ts index e770f665..af7df830 100644 --- a/src/id3v2/frames/urlLinkFrame.ts +++ b/src/id3v2/frames/urlLinkFrame.ts @@ -1,7 +1,7 @@ -import FrameTypes from "../frameIdentifiers"; import {ByteVector, StringType} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifier, FrameIdentifiers} from "../frameIdentifiers"; import {Guards} from "../../utils"; /** @@ -47,9 +47,9 @@ export class UrlLinkFrame extends Frame { * Constructs and initializes an empty frame with the provided frame identity * @param ident Identity of the frame to construct */ - public static fromIdentity(ident: ByteVector): UrlLinkFrame { + public static fromIdentity(ident: FrameIdentifier): UrlLinkFrame { Guards.truthy(ident, "ident"); - return new UrlLinkFrame(new Id3v2FrameHeader(ident, 4)); + return new UrlLinkFrame(new Id3v2FrameHeader(ident)); } /** @@ -59,18 +59,21 @@ export class UrlLinkFrame extends Frame { * @param offset What offset in {@paramref data} the frame actually begins. Must be positive, * safe integer * @param header Header of the frame found at {@paramref data} in the data + * @param version ID3v2 version the frame was originally encoded with */ public static fromOffsetRawData( data: ByteVector, offset: number, - header: Id3v2FrameHeader + header: Id3v2FrameHeader, + version: number ): UrlLinkFrame { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); + Guards.byte(version, "version"); const frame = new UrlLinkFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -84,8 +87,8 @@ export class UrlLinkFrame extends Frame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new UrlLinkFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new UrlLinkFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } @@ -133,18 +136,15 @@ export class UrlLinkFrame extends Frame { /** * Gets the first frame that matches the provided type * @param frames Object to search in - * @param ident Frame identifier to search for. Must be 4 bytes + * @param ident Frame identifier to search for * @returns UrlLinkFrame Frame containing the matching frameId, `undefined` if a match was * not found */ - public static findUrlLinkFrame(frames: UrlLinkFrame[], ident: ByteVector): UrlLinkFrame { + public static findUrlLinkFrame(frames: UrlLinkFrame[], ident: FrameIdentifier): UrlLinkFrame { Guards.truthy(frames, "frames"); Guards.truthy(ident, "ident"); - if (ident.length !== 4) { - throw new Error("Identifier must be 4 bytes long."); - } - return frames.find((f) => ByteVector.equal(f.frameId, ident)); + return frames.find((f) => f.frameId === ident); } /** @inheritDoc */ @@ -181,7 +181,7 @@ export class UrlLinkFrame extends Frame { const fieldList = []; const delim = ByteVector.getTextDelimiter(this._encoding); - if (this.frameId !== FrameTypes.WXXX) { + if (this.frameId !== FrameIdentifiers.WXXX) { fieldList.push(... data.toStrings(StringType.Latin1, 0)); } else if (data.length > 1 && !ByteVector.equal(data.mid(0, delim.length), delim)) { let value = data.toString(StringType.Latin1, 1, data.length - 1); @@ -214,7 +214,7 @@ export class UrlLinkFrame extends Frame { } const encoding = UrlLinkFrame.correctEncoding(this.textEncoding, version); - const isWxxx = this.frameId === FrameTypes.WXXX; + const isWxxx = this.frameId === FrameIdentifiers.WXXX; const v = isWxxx ? ByteVector.fromByteArray(new Uint8Array([encoding])) : ByteVector.empty(); @@ -253,7 +253,7 @@ export class UserUrlLinkFrame extends UrlLinkFrame { * @param description Description to use as text of the frame. */ public static fromDescription(description: string): UserUrlLinkFrame { - const frame = new UserUrlLinkFrame(new Id3v2FrameHeader(FrameTypes.WXXX, 4)); + const frame = new UserUrlLinkFrame(new Id3v2FrameHeader(FrameIdentifiers.WXXX)); frame.text = [description]; return frame; } @@ -265,18 +265,20 @@ export class UserUrlLinkFrame extends UrlLinkFrame { * @param offset What offset in {@paramref data} the frame actually begins. Must be positive, * safe integer * @param header Header of the frame found at {@paramref data} in the data + * @param version ID3v2 version the frame was originally encoded with */ public static fromOffsetRawData( data: ByteVector, offset: number, - header: Id3v2FrameHeader + header: Id3v2FrameHeader, + version: number ): UserUrlLinkFrame { Guards.truthy(data, "data"); Guards.uint(offset, "offset"); Guards.truthy(header, "header"); const frame = new UserUrlLinkFrame(header); - frame.setData(data, offset, false); + frame.setData(data, offset, false, version); return frame; } @@ -290,8 +292,8 @@ export class UserUrlLinkFrame extends UrlLinkFrame { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const frame = new UserUrlLinkFrame(new Id3v2FrameHeader(data, version)); - frame.setData(data, 0, true); + const frame = new UserUrlLinkFrame(Id3v2FrameHeader.fromData(data, version)); + frame.setData(data, 0, true, version); return frame; } From 91a77f15d7cd5957a63ec7d6c6c247a76eaca540 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 4 Feb 2020 20:33:44 -0500 Subject: [PATCH 38/71] More WIP --- src/id3v2/frameIdentifiers.ts | 14 +++++ src/id3v2/frames/frameFactory.ts | 90 ++++++++++++++------------------ src/id3v2/id3v2Tag.ts | 50 +++++++----------- 3 files changed, 72 insertions(+), 82 deletions(-) diff --git a/src/id3v2/frameIdentifiers.ts b/src/id3v2/frameIdentifiers.ts index 664a49db..c1fd3cd0 100644 --- a/src/id3v2/frameIdentifiers.ts +++ b/src/id3v2/frameIdentifiers.ts @@ -35,6 +35,20 @@ export class FrameIdentifier { : undefined; } + public get isTextFrame(): boolean { + const T = "T".codePointAt(0); + return (this.versionTable[2] && this.versionTable[2].get(0) === T) + || (this.versionTable[3] && this.versionTable[3].get(0) === T) + || (this.versionTable[4] && this.versionTable[4].get(0) === T); + } + + public get isUrlFrame(): boolean { + const W = "W".codePointAt(0); + return (this.versionTable[2] && this.versionTable[2].get(0) === W) + || (this.versionTable[3] && this.versionTable[3].get(0) === W) + || (this.versionTable[4] && this.versionTable[4].get(0) === W); + } + public render(version: number): ByteVector { Guards.byte(version, "version"); Guards.betweenInclusive(version, 2, 4, "version"); diff --git a/src/id3v2/frames/frameFactory.ts b/src/id3v2/frames/frameFactory.ts index 465085eb..19925f3e 100644 --- a/src/id3v2/frames/frameFactory.ts +++ b/src/id3v2/frames/frameFactory.ts @@ -1,29 +1,30 @@ -import FrameTypes from "../frameIdentifiers"; -import {ByteVector} from "../../byteVector"; -import {Id3v2FrameFlags, Id3v2FrameHeader} from "./frameHeader"; -import {File} from "../../file"; -import {Guards} from "../../utils"; import AttachmentFrame from "./attachmentFrame"; -import {TextInformationFrame, UserTextInformationFrame} from "./textInformationFrame"; -import {Frame} from "./frame"; -import UniqueFileIdentifierFrame from "./uniqueFileIdentifierFrame"; -import MusicCdIdentifierFrame from "./musicCdIdentifierFrame"; -import UnsynchronizedLyricsFrame from "./unsynchronizedLyricsFrame"; -import {SynchronizedLyricsFrame} from "./synchronizedLyricsFrame"; import CommentsFrame from "./commentsFrame"; -import {RelativeVolumeFrame} from "./relativeVolumeFrame"; +import MusicCdIdentifierFrame from "./musicCdIdentifierFrame"; +import PictureLazy from "../../pictureLazy"; import PlayCountFrame from "./playCountFrame"; import PopularimeterFrame from "./popularimeterFrame"; -import UnknownFrame from "./unknownFrame"; -import TermsOfUseFrame from "./termsOfUseFrame"; import PrivateFrame from "./privateFrame"; +import TermsOfUseFrame from "./termsOfUseFrame"; +import UniqueFileIdentifierFrame from "./uniqueFileIdentifierFrame"; +import UnknownFrame from "./unknownFrame"; +import UnsynchronizedLyricsFrame from "./unsynchronizedLyricsFrame"; +import {ByteVector} from "../../byteVector"; +import {NotImplementedError} from "../../errors"; import {EventTimeCodeFrame} from "./eventTimeCodeFrame"; +import {File} from "../../file"; +import {Frame} from "./frame"; +import {Id3v2FrameFlags, Id3v2FrameHeader} from "./frameHeader"; +import {FrameIdentifiers} from "../frameIdentifiers"; +import {RelativeVolumeFrame} from "./relativeVolumeFrame"; +import {SynchronizedLyricsFrame} from "./synchronizedLyricsFrame"; +import {TextInformationFrame, UserTextInformationFrame} from "./textInformationFrame"; import {UrlLinkFrame, UserUrlLinkFrame} from "./urlLinkFrame"; -import {NotImplementedError} from "../../errors"; -import PictureLazy from "../../pictureLazy"; +import {Guards} from "../../utils"; -const customFrameCreators: Array<(data: ByteVector, offset: number, header: Id3v2FrameHeader, version: number) => Frame> - = []; +export type FrameCreator = (data: ByteVector, offset: number, header: Id3v2FrameHeader, version: number) => Frame; + +const customFrameCreators: FrameCreator[] = []; /** * Performs the necessary operations to determine and create the correct child classes of @@ -83,20 +84,11 @@ export default { return undefined; } - const header = new Id3v2FrameHeader(data.mid(position, frameHeaderSize), version); + const header = Id3v2FrameHeader.fromData(data.mid(position, frameHeaderSize), version); const filePosition = offset + frameHeaderSize; offset += header.frameSize + frameHeaderSize; - // Filter out illegal frames - if (!header.frameId) { - throw new Error("No frame ID found, frame is invalid"); - } - for (const b of header.frameId) { - // (bZ) && (b<0 || b>9) - if ((b < 65 || b > 90) && (b < 48 || b > 57)) { - return undefined; - } - } + // Illegal frames are filtered out when creating the frame header // Mark the frame as unsynchronized if the entire tag is already unsynchronized // @TODO Standardize on "Desynchronized" @@ -133,10 +125,7 @@ export default { if (file) { // Attached picture (frames 4.14) // General encapsulated object (frames 4.15) - if ( - ByteVector.equal(header.frameId, FrameTypes.APIC) || - ByteVector.equal(header.frameId, FrameTypes.GEOB) - ) { + if (header.frameId === FrameIdentifiers.APIC || header.frameId === FrameIdentifiers.GEOB) { const picture = PictureLazy.fromFile(file.fileAbstraction, filePosition, offset - filePosition); return { frame: AttachmentFrame.fromPicture(picture), @@ -150,55 +139,52 @@ export default { } let func: any = UnknownFrame.fromOffsetRawData; - if (ByteVector.equal(header.frameId, FrameTypes.TXXX)) { + if (header.frameId === FrameIdentifiers.TXXX) { // User text identification frame func = UserTextInformationFrame.fromOffsetRawData; - } else if (header.frameId.get(0) === 84) { + } else if (header.frameId.isTextFrame) { // Text identifiacation frame (frames 4.2) Starts with T func = TextInformationFrame.fromOffsetRawData; - } else if (ByteVector.equal(header.frameId, FrameTypes.UFID)) { + } else if (header.frameId === FrameIdentifiers.UFID) { // Unique file identifier (frames 4.1) func = UniqueFileIdentifierFrame.fromOffsetRawData; - } else if (ByteVector.equal(header.frameId, FrameTypes.MCDI)) { + } else if (header.frameId === FrameIdentifiers.MCDI) { // Music CD identifier (frames 4.5) func = MusicCdIdentifierFrame.fromOffsetRawData; - } else if (ByteVector.equal(header.frameId, FrameTypes.USLT)) { + } else if (header.frameId === FrameIdentifiers.USLT) { // Unsynchronized lyrics (frames 4.8) func = UnsynchronizedLyricsFrame.fromOffsetRawData; - } else if (ByteVector.equal(header.frameId, FrameTypes.SYLT)) { + } else if (header.frameId === FrameIdentifiers.SYLT) { // Synchronized lyrics (frames 4.8) func = SynchronizedLyricsFrame.fromOffsetRawData; - } else if (ByteVector.equal(header.frameId, FrameTypes.COMM)) { + } else if (header.frameId === FrameIdentifiers.COMM) { // Comments (frames 4.10) func = CommentsFrame.fromOffsetRawData; - } else if (ByteVector.equal(header.frameId, FrameTypes.RVA2)) { + } else if (header.frameId === FrameIdentifiers.RVA2) { // Relative volume adjustment (frames 4.11) func = RelativeVolumeFrame.fromOffsetRawData; - } else if ( - ByteVector.equal(header.frameId, FrameTypes.APIC) || - ByteVector.equal(header.frameId, FrameTypes.GEOB) - ) { + } else if (header.frameId === FrameIdentifiers.APIC || header.frameId === FrameIdentifiers.GEOB) { // Attached picture (frames 4.14) func = AttachmentFrame.fromOffsetRawData; - } else if (ByteVector.equal(header.frameId, FrameTypes.PCNT)) { + } else if (header.frameId === FrameIdentifiers.PCNT) { // Play count (frames 4.16) func = PlayCountFrame.fromOffsetRawData; - } else if (ByteVector.equal(header.frameId, FrameTypes.POPM)) { + } else if (header.frameId === FrameIdentifiers.POPM) { // Popularimeter (frames 4.17) func = PopularimeterFrame.fromOffsetRawData; - } else if (ByteVector.equal(header.frameId, FrameTypes.USER)) { + } else if (header.frameId === FrameIdentifiers.USER) { // Terms of Use (frames 4.22) func = TermsOfUseFrame.fromOffsetRawData; - } else if (ByteVector.equal(header.frameId, FrameTypes.PRIV)) { + } else if (header.frameId === FrameIdentifiers.PRIV) { // Private (frames 4.27) func = PrivateFrame.fromOffsetRawData; - } else if (ByteVector.equal(header.frameId, FrameTypes.WXXX)) { + } else if (header.frameId === FrameIdentifiers.WXXX) { // User URL link func = UserUrlLinkFrame.fromOffsetRawData; - } else if (header.frameId.get(0) === 87) { + } else if (header.frameId.isUrlFrame) { // URL link (frame 4.3.1) starts with 'W' func = UrlLinkFrame.fromOffsetRawData; - } else if (ByteVector.equal(header.frameId, FrameTypes.ETCO)) { + } else if (header.frameId === FrameIdentifiers.ETCO) { // Event timing codes (frames 4.6) func = EventTimeCodeFrame.fromOffsetRawData; } diff --git a/src/id3v2/id3v2Tag.ts b/src/id3v2/id3v2Tag.ts index 26e75450..cdddfc5a 100644 --- a/src/id3v2/id3v2Tag.ts +++ b/src/id3v2/id3v2Tag.ts @@ -608,7 +608,7 @@ export default class Id3v2Tag extends Tag { match.removeFrames(ident); } else { for (const f of match._frameList) { - if (ByteVector.equal(f.frameId, ident)) { + if (f.frameId === ident) { copy = false; break; } @@ -617,7 +617,7 @@ export default class Id3v2Tag extends Tag { let i = 0; while (i < frames.length) { - if (ByteVector.equal(frames[i].frameId, ident)) { + if (frames[i].frameId === ident) { if (copy) { match._frameList.push(frames[i].clone()); } @@ -654,16 +654,12 @@ export default class Id3v2Tag extends Tag { * @param ident Identifier of the frame * @returns TFrame[] Array of frames with the desired frame identifier */ - public getFramesByIdentifier(type: FrameClassType, ident: ByteVector): TFrame[] { + public getFramesByIdentifier(type: FrameClassType, ident: FrameIdentifier): TFrame[] { Guards.notNullOrUndefined(type, "type"); Guards.truthy(ident, "ident"); - if (ident.length !== 4) { - throw new Error("Argument out of range: ident must be 4 characters"); - } - return this._frameList.filter((f) => { - return f && f.frameClassType === type && ByteVector.equal(f.frameId, ident); - }).map((f) => f); + return this._frameList.filter((f) => f && f.frameClassType === type && f.frameId === ident) + .map((f) => f); } /** @@ -672,11 +668,11 @@ export default class Id3v2Tag extends Tag { * @param ident Frame identifier of the text information frame to get the value from * @returns string Text of the specified frame, or `undefined` if no value was found */ - public getTextAsString(ident: ByteVector): string { + public getTextAsString(ident: FrameIdentifier): string { Guards.truthy(ident, "ident"); let frame: Frame; - if (ident.get(0) === "W".codePointAt(0)) { + if (ident.isUrlFrame) { const frames = this.getFramesByClassType(FrameClassType.UrlLinkFrame); frame = UrlLinkFrame.findUrlLinkFrame(frames, ident); } else { @@ -705,14 +701,11 @@ export default class Id3v2Tag extends Tag { * Removes all frames with a specified identifier from the current instance. * @param ident Identifier of the frames to remove */ - public removeFrames(ident: ByteVector): void { + public removeFrames(ident: FrameIdentifier): void { Guards.truthy(ident, "ident"); - if (ident.length !== 4) { - throw new Error("Argument out of range: ident must be 4 characters"); - } for (let i = this._frameList.length - 1; i >= 0; i--) { - if (ByteVector.equal(this._frameList[i].frameId, ident)) { + if (this._frameList[i].frameId === ident) { this._frameList.splice(i, 1); } } @@ -765,10 +758,10 @@ export default class Id3v2Tag extends Tag { // tag's header. The "tag data" (everything that is included in Header.tagSize) includes // the extended header, frames and padding, but does not include the tag's header or footer - const hasFooter = (this._header.flags & HeaderFlags.FooterPresent) != 0; - const unsyncAtFrameLevel = (this._header.flags & HeaderFlags.Unsynchronication) != 0 + const hasFooter = (this._header.flags & HeaderFlags.FooterPresent) !== 0; + const unsyncAtFrameLevel = (this._header.flags & HeaderFlags.Unsynchronication) !== 0 && this.version >= 4; - const unsyncAtTagLevel = (this._header.flags & HeaderFlags.Unsynchronication) != 0 + const unsyncAtTagLevel = (this._header.flags & HeaderFlags.Unsynchronication) !== 0 && this.version < 4; this._header.majorVersion = hasFooter ? 4 : this.version; @@ -783,7 +776,7 @@ export default class Id3v2Tag extends Tag { if (unsyncAtFrameLevel) { frame.flags |= Id3v2FrameFlags.Desynchronized; } - if ((frame.flags & Id3v2FrameFlags.TagAlterPreservation) != 0 ) { + if ((frame.flags & Id3v2FrameFlags.TagAlterPreservation) !== 0 ) { continue; } @@ -847,14 +840,11 @@ export default class Id3v2Tag extends Tag { * @param minPlaces Mininum number of digits to use to display the {@paramref numerator}, if * the numerator has less than this number of digits, it will be filled with leading zeroes. */ - public setNumberFrame(ident: ByteVector, numerator: number, denominator: number, minPlaces: number = 1): void { + public setNumberFrame(ident: FrameIdentifier, numerator: number, denominator: number, minPlaces: number = 1): void { Guards.truthy(ident, "ident"); Guards.uint(numerator, "value"); Guards.uint(denominator, "count"); Guards.byte(minPlaces, "minPlaces"); - if (ident.length !== 4) { - throw new Error("Argument out of range: ident must be 4 characters"); - } if (numerator === 0 && denominator === 0) { this.removeFrames(ident); @@ -891,7 +881,7 @@ export default class Id3v2Tag extends Tag { return; } - if (ident.get(0) === "W".codePointAt(0)) { + if (ident.isUrlFrame) { const frames = this.getFramesByClassType(FrameClassType.UrlLinkFrame); let urlFrame = UrlLinkFrame.findUrlLinkFrame(frames, ident); if (!urlFrame) { @@ -999,13 +989,13 @@ export default class Id3v2Tag extends Tag { } // Load up the first instance of each for post-processing - if (!tdrc && ByteVector.equal(frame.frameId, FrameIdentifiers.TDRC)) { + if (!tdrc && frame.frameId === FrameIdentifiers.TDRC) { tdrc = frame; - } else if (!tyer && ByteVector.equal(frame.frameId, FrameIdentifiers.TYER)) { + } else if (!tyer && frame.frameId === FrameIdentifiers.TYER) { tyer = frame; - } else if (!tdat && ByteVector.equal(frame.frameId, FrameIdentifiers.TDAT)) { + } else if (!tdat && frame.frameId === FrameIdentifiers.TDAT) { tdat = frame; - } else if (!time && ByteVector.equal(frame.frameId, FrameIdentifiers.TIME)) { + } else if (!time && frame.frameId === FrameIdentifiers.TIME) { time = frame; } } @@ -1116,7 +1106,7 @@ export default class Id3v2Tag extends Tag { let swapping: Frame; for (let i = 0; i < this._frameList.length; i++) { if (!swapping) { - if (ByteVector.equal(this._frameList[i].frameId, type)) { + if (this._frameList[i].frameId === type) { swapping = frame; } else { continue; From f48a5c076b5b9681c205949b4478ad3d139d0b6d Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 8 Feb 2020 12:46:17 -0500 Subject: [PATCH 39/71] More work in progress. Running tests and some are failing --- src/id3v2/frameIdentifiers.ts | 5 +- src/id3v2/frames/frameFactory.ts | 2 - src/id3v2/frames/urlLinkFrame.ts | 46 +++++----- test/id3v2/attachmentsFrameTests.ts | 3 +- test/id3v2/commentsFrameTests.ts | 42 +++++----- test/id3v2/eventTimeCodeFrameTests.ts | 22 ++--- test/id3v2/frameConstructorTests.ts | 22 ++--- test/id3v2/musicCdIdentifierFrameTests.ts | 20 ++--- test/id3v2/playCountFrameTests.ts | 27 +++--- test/id3v2/popularimeterFrameTests.ts | 32 +++---- test/id3v2/privateFrameTests.ts | 22 ++--- test/id3v2/relativeVolumeFrameTests.ts | 16 ++-- test/id3v2/synchronizedLyricsFrameTests.ts | 28 +++---- test/id3v2/termsOfUseFrameTests.ts | 23 +++-- test/id3v2/textInformationFrameTests.ts | 88 ++++++++++---------- test/id3v2/uniqueFileIdentifierFrameTests.ts | 34 ++++---- test/id3v2/unknownFrameTests.ts | 36 ++++---- test/id3v2/unsynchronizedLyricsFrameTests.ts | 28 +++---- test/id3v2/urlLinkFrameTests.ts | 80 ++++++++---------- test/id3v2/userTextInformationFrameTests.ts | 16 ++-- test/id3v2/userUrlLinkFrameTests.ts | 44 ++++------ 21 files changed, 312 insertions(+), 324 deletions(-) diff --git a/src/id3v2/frameIdentifiers.ts b/src/id3v2/frameIdentifiers.ts index c1fd3cd0..210789bc 100644 --- a/src/id3v2/frameIdentifiers.ts +++ b/src/id3v2/frameIdentifiers.ts @@ -27,7 +27,7 @@ export class FrameIdentifier { this.versionTable[4] = v4 ? ByteVector.fromString(v4, StringType.Latin1, undefined, true) : undefined; - this.versionTable[4] = v3 + this.versionTable[3] = v3 ? (v3 === v4 ? this.versionTable[4] : ByteVector.fromString(v3, StringType.Latin1, undefined, true)) : undefined; this.versionTable[2] = v2 @@ -57,7 +57,7 @@ export class FrameIdentifier { throw new Error(`Frame ${newest} is not supported in ID3v2 version ${version}`); } - return this.versionTable[version]; + return ByteVector.fromByteVector(this.versionTable[version]); } } @@ -192,6 +192,7 @@ export const FrameIdentifiers: {[key: string]: FrameIdentifier} = { REV: uniqueFrameIdentifiers.RVRB, RVA: uniqueFrameIdentifiers.RVA2, RVA2: uniqueFrameIdentifiers.RVA2, + RVAD: uniqueFrameIdentifiers.RVA2, RVRB: uniqueFrameIdentifiers.RVRB, SEEK: uniqueFrameIdentifiers.SEEK, SIGN: uniqueFrameIdentifiers.SIGN, diff --git a/src/id3v2/frames/frameFactory.ts b/src/id3v2/frames/frameFactory.ts index 19925f3e..2369a4ce 100644 --- a/src/id3v2/frames/frameFactory.ts +++ b/src/id3v2/frames/frameFactory.ts @@ -194,6 +194,4 @@ export default { offset: offset }; } - - }; diff --git a/src/id3v2/frames/urlLinkFrame.ts b/src/id3v2/frames/urlLinkFrame.ts index af7df830..1b87f7b6 100644 --- a/src/id3v2/frames/urlLinkFrame.ts +++ b/src/id3v2/frames/urlLinkFrame.ts @@ -180,30 +180,38 @@ export class UrlLinkFrame extends Frame { this._rawData = undefined; const fieldList = []; - const delim = ByteVector.getTextDelimiter(this._encoding); - if (this.frameId !== FrameIdentifiers.WXXX) { - fieldList.push(... data.toStrings(StringType.Latin1, 0)); - } else if (data.length > 1 && !ByteVector.equal(data.mid(0, delim.length), delim)) { - let value = data.toString(StringType.Latin1, 1, data.length - 1); - - // Do a fast removal of end bytes - if (value.length > 1 && value[value.length - 1] === "\0") { - for (let i = value.length - 1; i >= 0; i--) { - if (value[i] !== "\0") { - value = value.substr(0, i + 1); - break; - } - } + let index = 0; + if (this.frameId === FrameIdentifiers.WXXX) { + // Text Encoding $xx + // Description $00 (00) + // URL + const encoding = data.get(0); + const delim = ByteVector.getTextDelimiter(encoding); + const delimIndex = data.find(delim, 1, delim.length); + + if (delimIndex >= 0) { + const description = data.toString(delimIndex - 1, encoding, 1); + fieldList.push(description); + index += delimIndex - 1; } - fieldList.push(value); + index += 1; } - // Bad tags may have one or more null characters at the end of a string, resulting in empty - // strings at the end of the field list. Strip them off. - while (fieldList.length !== 0 && !fieldList[fieldList.length - 1]) { - fieldList.pop(); + // Read the url from the data + let url = data.toString(data.length - index, StringType.Latin1, index); + + // Do a fast removal of end bytes + if (url.length > 1 && url[url.length - 1] === "\0") { + for (let i = url.length - 1; i >= 0; i--) { + if (url[i] !== "\0") { + url = url.substr(0, i + 1); + break; + } + } } + + fieldList.push(url); this._textFields = fieldList; } diff --git a/test/id3v2/attachmentsFrameTests.ts b/test/id3v2/attachmentsFrameTests.ts index 63600308..7c1491ec 100644 --- a/test/id3v2/attachmentsFrameTests.ts +++ b/test/id3v2/attachmentsFrameTests.ts @@ -7,10 +7,11 @@ import AttachmentFrame from "../../src/id3v2/frames/attachmentFrame"; import {IPicture, PictureType} from "../../src/picture"; import TestConstants from "../testConstants"; import {FrameClassType} from "../../src/id3v2/frames/frame"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; + import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import {ByteVector} from "../../src/byteVector"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; import {IFileAbstraction} from "../../src/fileAbstraction"; // Setup chai diff --git a/test/id3v2/commentsFrameTests.ts b/test/id3v2/commentsFrameTests.ts index 2b89610d..3f100df5 100644 --- a/test/id3v2/commentsFrameTests.ts +++ b/test/id3v2/commentsFrameTests.ts @@ -5,10 +5,10 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import CommentsFrame from "../../src/id3v2/frames/commentsFrame"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; // Setup Chai @@ -16,7 +16,7 @@ Chai.use(ChaiAsPromised); const assert = Chai.assert; function getTestFrame(): CommentsFrame { - const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.COMM); header.frameSize = 11; const data = ByteVector.concatenate( header.render(4), @@ -32,7 +32,7 @@ function getTestFrame(): CommentsFrame { @suite(timeout(3000), slow(1000)) class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return CommentsFrame.fromOffsetRawData; } @@ -89,7 +89,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { @test public fromOffsetRawData_tooFewBytes_throws() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.COMM); header.frameSize = 1; const data = ByteVector.concatenate( header.render(4), @@ -98,13 +98,13 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { ); // Act/Assert - assert.throws(() => { CommentsFrame.fromOffsetRawData(data, 2, header); }); + assert.throws(() => { CommentsFrame.fromOffsetRawData(data, 2, header, 4); }); } @test public fromOffsetRawData_noData_returnsEmptyFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.COMM); header.frameSize = 4; const data = ByteVector.concatenate( header.render(4), @@ -114,7 +114,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { ); // Act - const frame = CommentsFrame.fromOffsetRawData(data, 2, header); + const frame = CommentsFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.validateFrame(frame, "", "eng", StringType.Latin1, ""); @@ -123,7 +123,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { @test public fromOffsetRawData_oneData_returnsFrameWithoutDescription() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.COMM); header.frameSize = 7; const data = ByteVector.concatenate( header.render(4), @@ -134,7 +134,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { ); // Act - const frame = CommentsFrame.fromOffsetRawData(data, 2, header); + const frame = CommentsFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.validateFrame(frame, "", "eng", StringType.Latin1, "fux"); @@ -143,7 +143,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { @test public fromOffsetRawData_twoData_returnsFrameWithDescriptionAndText() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.COMM); header.frameSize = 11; const data = ByteVector.concatenate( header.render(4), @@ -156,7 +156,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { ); // Act - const frame = CommentsFrame.fromOffsetRawData(data, 2, header); + const frame = CommentsFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.validateFrame(frame, "fux", "eng", StringType.Latin1, "bux"); @@ -164,7 +164,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { @test public fromOffsetRawData_threeData_returnsFrameWithDescriptionAndText() { - const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.COMM); header.frameSize = 12; const data = ByteVector.concatenate( header.render(4), @@ -178,7 +178,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { ); // Act - const frame = CommentsFrame.fromOffsetRawData(data, 2, header); + const frame = CommentsFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.validateFrame(frame, "fux", "eng", StringType.Latin1, "bux"); @@ -187,7 +187,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_tooFewBytes_throws() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.COMM); header.frameSize = 1; const data = ByteVector.concatenate( header.render(4), @@ -201,7 +201,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_noData_returnsEmptyFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.COMM); header.frameSize = 4; const data = ByteVector.concatenate( header.render(4), @@ -219,7 +219,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_oneData_returnsFrameWithoutDescription() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.COMM); header.frameSize = 7; const data = ByteVector.concatenate( header.render(4), @@ -237,7 +237,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_twoData_returnsFrameWithDescriptionAndText() { - const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.COMM); header.frameSize = 11; const data = ByteVector.concatenate( header.render(4), @@ -257,7 +257,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_threeData_returnsFrameWithDescriptionAndText() { - const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.COMM); header.frameSize = 12; const data = ByteVector.concatenate( header.render(4), @@ -285,7 +285,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { ) { assert.isOk(frame); assert.equal(frame.frameClassType, FrameClassType.CommentsFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.COMM)); + assert.strictEqual(frame.frameId, FrameIdentifiers.COMM); assert.strictEqual(frame.description, expectedDesc); assert.strictEqual(frame.language, expectedLang); @@ -553,7 +553,7 @@ class Id3v2_CommentsFrame_MethodTests { // Assert assert.isOk(output); assert.equal(output.frameClassType, FrameClassType.CommentsFrame); - assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.COMM)); + assert.strictEqual(output.frameId, FrameIdentifiers.COMM); assert.strictEqual(output.description, frame.description); assert.strictEqual(output.language, frame.language); @@ -564,7 +564,7 @@ class Id3v2_CommentsFrame_MethodTests { @test public render() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.COMM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.COMM); header.frameSize = 11; const data = ByteVector.concatenate( header.render(4), diff --git a/test/id3v2/eventTimeCodeFrameTests.ts b/test/id3v2/eventTimeCodeFrameTests.ts index 4279a4b6..a622f91d 100644 --- a/test/id3v2/eventTimeCodeFrameTests.ts +++ b/test/id3v2/eventTimeCodeFrameTests.ts @@ -4,11 +4,11 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; import {ByteVector} from "../../src/byteVector"; import {EventTimeCode, EventTimeCodeFrame} from "../../src/id3v2/frames/eventTimeCodeFrame"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameFlags, Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; import {EventType, TimestampFormat} from "../../src/id3v2/utilTypes"; import framePropertyTests from "./framePropertyTests"; @@ -96,7 +96,7 @@ class Id3v2_EventTimeCodeTests { @suite(timeout(3000), slow(1000)) class Id3v2_EventTimeCodeFrame_ConstructorTests extends FrameConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return EventTimeCodeFrame.fromOffsetRawData; } @@ -125,7 +125,7 @@ class Id3v2_EventTimeCodeFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_noEvents() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.ETCO, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.ETCO); header.frameSize = 1; const data = ByteVector.concatenate( header.render(4), @@ -142,7 +142,7 @@ class Id3v2_EventTimeCodeFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_withEvents() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.ETCO, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.ETCO); header.frameSize = 11; const event1 = new EventTimeCode(EventType.Profanity, 123); const event2 = new EventTimeCode(EventType.KeyChange, 456); @@ -163,7 +163,7 @@ class Id3v2_EventTimeCodeFrame_ConstructorTests extends FrameConstructorTests { @test public fromOffsetRawData_noEvents() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.ETCO, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.ETCO); header.frameSize = 1; const data = ByteVector.concatenate( header.render(4), @@ -172,7 +172,7 @@ class Id3v2_EventTimeCodeFrame_ConstructorTests extends FrameConstructorTests { ); // Act - const frame = EventTimeCodeFrame.fromOffsetRawData(data, 2, header); + const frame = EventTimeCodeFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.assertFrame(frame, [], TimestampFormat.AbsoluteMilliseconds); @@ -181,7 +181,7 @@ class Id3v2_EventTimeCodeFrame_ConstructorTests extends FrameConstructorTests { @test public fromOffsetRawData_withEvents() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.ETCO, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.ETCO); header.frameSize = 11; const event1 = new EventTimeCode(EventType.Profanity, 123); const event2 = new EventTimeCode(EventType.KeyChange, 456); @@ -194,7 +194,7 @@ class Id3v2_EventTimeCodeFrame_ConstructorTests extends FrameConstructorTests { ); // Act - const frame = EventTimeCodeFrame.fromOffsetRawData(data, 2, header); + const frame = EventTimeCodeFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.assertFrame(frame, [event1, event2], TimestampFormat.AbsoluteMilliseconds); @@ -203,7 +203,7 @@ class Id3v2_EventTimeCodeFrame_ConstructorTests extends FrameConstructorTests { private assertFrame(frame: EventTimeCodeFrame, e: EventTimeCode[], t: TimestampFormat) { assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.EventTimeCodeFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.ETCO)); + assert.strictEqual(frame.frameId, FrameIdentifiers.ETCO); assert.isTrue((frame.flags | Id3v2FrameFlags.FileAlterPreservation) > 0); assert.deepStrictEqual(frame.events, e); @@ -257,7 +257,7 @@ class Id3v2_EventTimeCodeFrame_MethodTests { // Assert assert.isOk(output); assert.strictEqual(output.frameClassType, FrameClassType.EventTimeCodeFrame); - assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.ETCO)); + assert.strictEqual(output.frameId, FrameIdentifiers.ETCO); assert.isTrue((output.flags | Id3v2FrameFlags.FileAlterPreservation) > 0); assert.deepStrictEqual(output.events, frame.events); @@ -267,7 +267,7 @@ class Id3v2_EventTimeCodeFrame_MethodTests { @test public render() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.ETCO, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.ETCO); header.frameSize = 11; const event2 = new EventTimeCode(EventType.KeyChange, 456); const event1 = new EventTimeCode(EventType.Profanity, 123); diff --git a/test/id3v2/frameConstructorTests.ts b/test/id3v2/frameConstructorTests.ts index 5d45410e..9be664bb 100644 --- a/test/id3v2/frameConstructorTests.ts +++ b/test/id3v2/frameConstructorTests.ts @@ -5,7 +5,7 @@ import {test} from "mocha-typescript"; import {ByteVector} from "../../src/byteVector"; import {Frame} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; // Setup chai Chai.use(ChaiAsPromised); @@ -13,29 +13,29 @@ const assert = Chai.assert; export default abstract class FrameConstructorTests { - public abstract get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame; + public abstract get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame; public abstract get fromRawData(): (d: ByteVector, v: number) => Frame; @test public fromOffsetRawData_falsyData_throws() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WCOM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.WCOM); // Act/Assert - assert.throws(() => { this.fromOffsetRawData(null, 2, header); }); - assert.throws(() => { this.fromOffsetRawData(undefined, 2, header); }); + assert.throws(() => { this.fromOffsetRawData(null, 2, header, 4); }); + assert.throws(() => { this.fromOffsetRawData(undefined, 2, header, 4); }); } @test public fromOffsetRawData_invalidVersion_throws() { // Arrange const data = ByteVector.empty(); - const header = new Id3v2FrameHeader(FrameTypes.WCOM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.WCOM); // Act/Assert - assert.throws(() => { this.fromOffsetRawData(data, -1, header); }); - assert.throws(() => { this.fromOffsetRawData(data, 1.5, header); }); - assert.throws(() => { this.fromOffsetRawData(data, 0x100, header); }); + assert.throws(() => { this.fromOffsetRawData(data, -1, header, 4); }); + assert.throws(() => { this.fromOffsetRawData(data, 1.5, header, 4); }); + assert.throws(() => { this.fromOffsetRawData(data, 0x100, header, 4); }); } @test @@ -44,8 +44,8 @@ export default abstract class FrameConstructorTests { const data = ByteVector.empty(); // Act/Assert - assert.throws(() => { this.fromOffsetRawData(data, 2, undefined); }); - assert.throws(() => { this.fromOffsetRawData(data, 2, null); }); + assert.throws(() => { this.fromOffsetRawData(data, 2, undefined, 4); }); + assert.throws(() => { this.fromOffsetRawData(data, 2, null, 4); }); } @test diff --git a/test/id3v2/musicCdIdentifierFrameTests.ts b/test/id3v2/musicCdIdentifierFrameTests.ts index 8d29570a..5652c956 100644 --- a/test/id3v2/musicCdIdentifierFrameTests.ts +++ b/test/id3v2/musicCdIdentifierFrameTests.ts @@ -4,11 +4,11 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; import MusicCdIdentifierFrame from "../../src/id3v2/frames/musicCdIdentifierFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; // Setup Chai Chai.use(ChaiAsPromised); @@ -16,7 +16,7 @@ const assert = Chai.assert; @suite(timeout(3000), slow(1000)) class Id3v2_MusicCdIdentifierFrameTests extends FrameConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return MusicCdIdentifierFrame.fromOffsetRawData; } @@ -27,7 +27,7 @@ class Id3v2_MusicCdIdentifierFrameTests extends FrameConstructorTests { @test public fromRawData_validParameters() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.MCDI, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.MCDI); header.frameSize = 9; const data = ByteVector.concatenate( header.render(4), @@ -44,7 +44,7 @@ class Id3v2_MusicCdIdentifierFrameTests extends FrameConstructorTests { @test public fromOffsetRawData_validParameters() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.MCDI, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.MCDI); header.frameSize = 9; const data = ByteVector.concatenate( header.render(4), @@ -53,7 +53,7 @@ class Id3v2_MusicCdIdentifierFrameTests extends FrameConstructorTests { ); // Act - const frame = MusicCdIdentifierFrame.fromOffsetRawData(data, 2, header); + const frame = MusicCdIdentifierFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.assertFrame(frame, ByteVector.fromString("12345abcd", StringType.Latin1)); @@ -85,7 +85,7 @@ class Id3v2_MusicCdIdentifierFrameTests extends FrameConstructorTests { @test public clone_withoutData() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.MCDI, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.MCDI); header.frameSize = 0; const frame = MusicCdIdentifierFrame.fromRawData(header.render(4), 4); @@ -99,7 +99,7 @@ class Id3v2_MusicCdIdentifierFrameTests extends FrameConstructorTests { @test public render_withData() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.MCDI, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.MCDI); header.frameSize = 9; const data = ByteVector.concatenate( header.render(4), @@ -117,7 +117,7 @@ class Id3v2_MusicCdIdentifierFrameTests extends FrameConstructorTests { @test public render_withoutData() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.MCDI, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.MCDI); header.frameSize = 0; const frame = MusicCdIdentifierFrame.fromRawData(header.render(4), 4); @@ -131,13 +131,13 @@ class Id3v2_MusicCdIdentifierFrameTests extends FrameConstructorTests { private assertFrame(frame: MusicCdIdentifierFrame, d: ByteVector) { assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.MusicCdIdentiferFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.MCDI)); + assert.strictEqual(frame.frameId, FrameIdentifiers.MCDI); assert.isTrue(ByteVector.equal(frame.data, d)); } private getTestFrame() { - const header = new Id3v2FrameHeader(FrameTypes.MCDI, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.MCDI); header.frameSize = 9; const data = ByteVector.concatenate( header.render(4), diff --git a/test/id3v2/playCountFrameTests.ts b/test/id3v2/playCountFrameTests.ts index 39f8d605..8451b5ab 100644 --- a/test/id3v2/playCountFrameTests.ts +++ b/test/id3v2/playCountFrameTests.ts @@ -5,11 +5,11 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; import PlayCountFrame from "../../src/id3v2/frames/playCountFrame"; -import {ByteVector, StringType} from "../../src/byteVector"; +import {ByteVector} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; // Setup chai Chai.use(ChaiAsPromised); @@ -17,7 +17,7 @@ const assert = Chai.assert; @suite(timeout(3000), slow(1000)) class Id3v2_PlayCountFrame_ConstructorTests extends FrameConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return PlayCountFrame.fromOffsetRawData; } @@ -37,7 +37,7 @@ class Id3v2_PlayCountFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_fourBytePlayCount() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.PCNT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.PCNT); header.frameSize = 4; const data = ByteVector.concatenate( header.render(4), @@ -51,9 +51,10 @@ class Id3v2_PlayCountFrame_ConstructorTests extends FrameConstructorTests { this.assertFrame(frame, 1234); } + @test public fromRawData_sixBytePlayCount() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.PCNT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.PCNT); header.frameSize = 6; const data = ByteVector.concatenate( header.render(4), @@ -70,7 +71,7 @@ class Id3v2_PlayCountFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_eightBytePlayCount() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.PCNT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.PCNT); header.frameSize = 8; const data = ByteVector.concatenate( header.render(4), @@ -87,7 +88,7 @@ class Id3v2_PlayCountFrame_ConstructorTests extends FrameConstructorTests { @test public fromOffsetRawData_twoBytePlayCount() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.PCNT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.PCNT); header.frameSize = 8; const data = ByteVector.concatenate( header.render(4), @@ -96,7 +97,7 @@ class Id3v2_PlayCountFrame_ConstructorTests extends FrameConstructorTests { ); // Act - const frame = PlayCountFrame.fromOffsetRawData(data, 2, header); + const frame = PlayCountFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.assertFrame(frame, BigInt("4294967296")); @@ -105,7 +106,7 @@ class Id3v2_PlayCountFrame_ConstructorTests extends FrameConstructorTests { private assertFrame(frame: PlayCountFrame, p: BigInt.BigNumber) { assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.PlayCountFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.PCNT)); + assert.strictEqual(frame.frameId, FrameIdentifiers.PCNT); assert.isTrue(frame.playCount.eq(p)); } @@ -142,7 +143,7 @@ class Id3v2_PlayCountFrame_MethodTests { // Assert assert.isOk(output); assert.strictEqual(output.frameClassType, FrameClassType.PlayCountFrame); - assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.PCNT)); + assert.strictEqual(output.frameId, FrameIdentifiers.PCNT); assert.isTrue(output.playCount.eq(frame.playCount)); } @@ -150,7 +151,7 @@ class Id3v2_PlayCountFrame_MethodTests { @test public render_intPlayCount() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.PCNT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.PCNT); header.frameSize = 4; const data = ByteVector.concatenate( header.render(4), @@ -169,7 +170,7 @@ class Id3v2_PlayCountFrame_MethodTests { @test public render_sixBytePlayCount() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.PCNT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.PCNT); header.frameSize = 6; const data = ByteVector.concatenate( header.render(4), @@ -188,7 +189,7 @@ class Id3v2_PlayCountFrame_MethodTests { @test public render_longBytePlayCount() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.PCNT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.PCNT); header.frameSize = 8; const data = ByteVector.concatenate( header.render(4), diff --git a/test/id3v2/popularimeterFrameTests.ts b/test/id3v2/popularimeterFrameTests.ts index 6df97113..801bae10 100644 --- a/test/id3v2/popularimeterFrameTests.ts +++ b/test/id3v2/popularimeterFrameTests.ts @@ -5,11 +5,11 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; import PopularimeterFrame from "../../src/id3v2/frames/popularimeterFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; // Setup chai Chai.use(ChaiAsPromised); @@ -17,7 +17,7 @@ const assert = Chai.assert; @suite(timeout(3000), slow(1000)) class Id3v2_PopularimeterFrame_ConstructorTests extends FrameConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return PopularimeterFrame.fromOffsetRawData; } @@ -37,7 +37,7 @@ class Id3v2_PopularimeterFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_noDelimiter() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.POPM); header.frameSize = 3; const data = ByteVector.concatenate( header.render(4), @@ -51,7 +51,7 @@ class Id3v2_PopularimeterFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_tooShort() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.POPM); header.frameSize = 3; const data = ByteVector.concatenate( header.render(4), @@ -66,7 +66,7 @@ class Id3v2_PopularimeterFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_noPlayCount() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.POPM); header.frameSize = 9; const data = ByteVector.concatenate( header.render(4), @@ -85,7 +85,7 @@ class Id3v2_PopularimeterFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_invalidPlayCountBytes() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.POPM); header.frameSize = 9; const data = ByteVector.concatenate( header.render(4), @@ -102,7 +102,7 @@ class Id3v2_PopularimeterFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_intSizedPlayCount() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.POPM); header.frameSize = 9; const data = ByteVector.concatenate( header.render(4), @@ -122,7 +122,7 @@ class Id3v2_PopularimeterFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_longSizedPlayCount() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.POPM); header.frameSize = 13; const data = ByteVector.concatenate( header.render(4), @@ -142,7 +142,7 @@ class Id3v2_PopularimeterFrame_ConstructorTests extends FrameConstructorTests { @test public fromOffsetRawData_longSizedPlayCount() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.POPM); header.frameSize = 13; const data = ByteVector.concatenate( header.render(4), @@ -154,7 +154,7 @@ class Id3v2_PopularimeterFrame_ConstructorTests extends FrameConstructorTests { ); // Act - const frame = PopularimeterFrame.fromOffsetRawData(data, 2, header); + const frame = PopularimeterFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.assertFrame(frame, "fux", BigInt(1234), 0x05); @@ -163,7 +163,7 @@ class Id3v2_PopularimeterFrame_ConstructorTests extends FrameConstructorTests { private assertFrame(frame: PopularimeterFrame, u: string, p: BigInt.BigInteger, r: number) { assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.PopularimeterFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.POPM)); + assert.strictEqual(frame.frameId, FrameIdentifiers.POPM); if (p === undefined) { assert.isUndefined(frame.playCount); @@ -282,7 +282,7 @@ class Id3v2_PopularimeterFrame_MethodTests { // Assert assert.isOk(output); assert.strictEqual(output.frameClassType, FrameClassType.PopularimeterFrame); - assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.POPM)); + assert.strictEqual(output.frameId, FrameIdentifiers.POPM); assert.isOk(output.playCount); assert.isTrue(frame.playCount.equals(output.playCount)); @@ -294,7 +294,7 @@ class Id3v2_PopularimeterFrame_MethodTests { @test public render_noPlayCount() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.POPM); header.frameSize = 5; const data = ByteVector.concatenate( header.render(4), @@ -315,7 +315,7 @@ class Id3v2_PopularimeterFrame_MethodTests { @test public render_intPlayCount() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.POPM); header.frameSize = 9; const data = ByteVector.concatenate( header.render(4), @@ -337,7 +337,7 @@ class Id3v2_PopularimeterFrame_MethodTests { @test public render_intermediateSizePlayCount() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.POPM); header.frameSize = 10; const data = ByteVector.concatenate( header.render(4), @@ -359,7 +359,7 @@ class Id3v2_PopularimeterFrame_MethodTests { @test public render_longPlayCount() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.POPM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.POPM); header.frameSize = 13; const data = ByteVector.concatenate( header.render(4), diff --git a/test/id3v2/privateFrameTests.ts b/test/id3v2/privateFrameTests.ts index 36432582..d9809135 100644 --- a/test/id3v2/privateFrameTests.ts +++ b/test/id3v2/privateFrameTests.ts @@ -4,11 +4,11 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; import PrivateFrame from "../../src/id3v2/frames/privateFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; // Setup chai Chai.use(ChaiAsPromised); @@ -16,7 +16,7 @@ const assert = Chai.assert; @suite(timeout(3000), slow(1000)) class Id3v2_PrivateFrame_ConstructorTests extends FrameConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return PrivateFrame.fromOffsetRawData; } @@ -36,7 +36,7 @@ class Id3v2_PrivateFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_tooFewBytes() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.PRIV, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.PRIV); header.frameSize = 0; const data = ByteVector.concatenate( header.render(4) @@ -49,7 +49,7 @@ class Id3v2_PrivateFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_owner() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.PRIV, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.PRIV); header.frameSize = 4; const data = ByteVector.concatenate( header.render(4), @@ -66,7 +66,7 @@ class Id3v2_PrivateFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_ownerAndData() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.PRIV, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.PRIV); header.frameSize = 9; const data = ByteVector.concatenate( header.render(4), @@ -85,7 +85,7 @@ class Id3v2_PrivateFrame_ConstructorTests extends FrameConstructorTests { @test public fromOffsetRawData_ownerAndData() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.PRIV, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.PRIV); header.frameSize = 8; const data = ByteVector.concatenate( header.render(4), @@ -96,7 +96,7 @@ class Id3v2_PrivateFrame_ConstructorTests extends FrameConstructorTests { ); // Act - const frame = PrivateFrame.fromOffsetRawData(data, 2, header); + const frame = PrivateFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.assertFrame(frame, "fux", ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04]))); @@ -105,7 +105,7 @@ class Id3v2_PrivateFrame_ConstructorTests extends FrameConstructorTests { private assertFrame(frame: PrivateFrame, o: string, d: ByteVector) { assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.PrivateFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.PRIV)); + assert.strictEqual(frame.frameId, FrameIdentifiers.PRIV); assert.strictEqual(frame.owner, o); assert.isTrue(ByteVector.equal(frame.privateData, d)); @@ -184,7 +184,7 @@ class Id3v2_PrivateFrame_MethodTests { assert.notEqual(frame, output); assert.strictEqual(output.frameClassType, FrameClassType.PrivateFrame); - assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.PRIV)); + assert.strictEqual(output.frameId, FrameIdentifiers.PRIV); assert.strictEqual(output.owner, frame.owner); assert.notEqual(output.privateData, frame.privateData); @@ -194,7 +194,7 @@ class Id3v2_PrivateFrame_MethodTests { @test public render_v4() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.PRIV, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.PRIV); header.frameSize = 8; const data = ByteVector.concatenate( header.render(4), @@ -215,7 +215,7 @@ class Id3v2_PrivateFrame_MethodTests { @test public render_v2_throws() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.PRIV, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.PRIV); header.frameSize = 9; const data = ByteVector.concatenate( header.render(4), diff --git a/test/id3v2/relativeVolumeFrameTests.ts b/test/id3v2/relativeVolumeFrameTests.ts index 7ba7672f..b0ef43fc 100644 --- a/test/id3v2/relativeVolumeFrameTests.ts +++ b/test/id3v2/relativeVolumeFrameTests.ts @@ -5,11 +5,11 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import ConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; import {ChannelData, ChannelType, RelativeVolumeFrame} from "../../src/id3v2/frames/relativeVolumeFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; // Setup chai Chai.use(ChaiAsPromised); @@ -283,7 +283,7 @@ class Id3v2_RelativeVolumeChannelData { @suite(timeout(3000), slow(1000)) class Id3v2_RelativeVolumeFrame_ConstructorTests extends ConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return RelativeVolumeFrame.fromOffsetRawData; } @@ -303,7 +303,7 @@ class Id3v2_RelativeVolumeFrame_ConstructorTests extends ConstructorTests { @test public fromRawData_noDelimiter_emptyFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.RVA2, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.RVA2); header.frameSize = 3; const data = ByteVector.concatenate( header.render(4), @@ -320,7 +320,7 @@ class Id3v2_RelativeVolumeFrame_ConstructorTests extends ConstructorTests { @test public fromRawData_withChannelData_hasChannelData() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.RVA2, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.RVA2); header.frameSize = 15; const channel1 = new ChannelData(ChannelType.Subwoofer); @@ -351,7 +351,7 @@ class Id3v2_RelativeVolumeFrame_ConstructorTests extends ConstructorTests { @test public fromOffsetRawData_withChannelData_hasChannelData() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.RVA2, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.RVA2); header.frameSize = 15; const channel1 = new ChannelData(ChannelType.Subwoofer); @@ -374,7 +374,7 @@ class Id3v2_RelativeVolumeFrame_ConstructorTests extends ConstructorTests { ); // Act - const frame = RelativeVolumeFrame.fromOffsetRawData(data, 2, header); + const frame = RelativeVolumeFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.assertFrame(frame, [channel2, channel1], "foo"); @@ -383,7 +383,7 @@ class Id3v2_RelativeVolumeFrame_ConstructorTests extends ConstructorTests { private assertFrame(frame: RelativeVolumeFrame, c: ChannelData[], i: string) { assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.RelativeVolumeFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.RVA2)); + assert.strictEqual(frame.frameId, FrameIdentifiers.RVA2); assert.deepStrictEqual(frame.channels, c); assert.strictEqual(frame.identification, i); @@ -474,7 +474,7 @@ class Id3v2_RelativeVolumeFrameMethodTests { @test public render() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.RVA2, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.RVA2); header.frameSize = 15; const channel1 = new ChannelData(ChannelType.Subwoofer); diff --git a/test/id3v2/synchronizedLyricsFrameTests.ts b/test/id3v2/synchronizedLyricsFrameTests.ts index fd2a970c..d22d0dae 100644 --- a/test/id3v2/synchronizedLyricsFrameTests.ts +++ b/test/id3v2/synchronizedLyricsFrameTests.ts @@ -4,11 +4,11 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; import {SynchronizedLyricsFrame, SynchronizedText} from "../../src/id3v2/frames/synchronizedLyricsFrame"; import {SynchronizedTextType, TimestampFormat} from "../../src/id3v2/utilTypes"; @@ -48,7 +48,7 @@ class Id3v2_SynchronizedTextTests { @suite(timeout(3000), slow(1000)) class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return SynchronizedLyricsFrame.fromOffsetRawData; } @@ -104,7 +104,7 @@ class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTes @test public fromRawData_notEnoughBytes() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.SYLT); header.frameSize = 5; const data = ByteVector.concatenate( header.render(4), @@ -118,7 +118,7 @@ class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTes @test public fromRawData_missingDelimiter() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.SYLT); header.frameSize = 10; const data = ByteVector.concatenate( header.render(4), @@ -133,7 +133,7 @@ class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTes @test public fromRawData_noDelimiterForSynchronizedText() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.SYLT); header.frameSize = 16; const data = ByteVector.concatenate( header.render(4), @@ -152,7 +152,7 @@ class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTes @test public fromRawData_incompleteSynchronizedText() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.SYLT); header.frameSize = 34; const content1 = new SynchronizedText(123, "qux"); const content2 = new SynchronizedText(456, "zux"); @@ -186,7 +186,7 @@ class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTes @test public fromRawData_noData() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.SYLT); header.frameSize = 14; const data = ByteVector.concatenate( header.render(4), @@ -216,7 +216,7 @@ class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTes @test public fromRawData_singleData() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.SYLT); header.frameSize = 26; const content1 = new SynchronizedText(123, "qux"); const data = ByteVector.concatenate( @@ -248,7 +248,7 @@ class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTes @test public fromRawData_multipleData() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.SYLT); header.frameSize = 38; const content1 = new SynchronizedText(123, "qux"); const content2 = new SynchronizedText(456, "zux"); @@ -282,7 +282,7 @@ class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTes @test public fromOffsetRawData_multipleData() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.SYLT); header.frameSize = 38; const content1 = new SynchronizedText(123, "qux"); const content2 = new SynchronizedText(456, "zux"); @@ -300,7 +300,7 @@ class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTes ); // Act - const frame = SynchronizedLyricsFrame.fromOffsetRawData(data, 2, header); + const frame = SynchronizedLyricsFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.assertFrame( @@ -325,7 +325,7 @@ class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTes ) { assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.SynchronizedLyricsFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.SYLT)); + assert.strictEqual(frame.frameId, FrameIdentifiers.SYLT); assert.strictEqual(frame.description, d); assert.strictEqual(frame.format, f); @@ -617,7 +617,7 @@ class Id3v2_SynchronizedLyricsFrame_MethodTests { assert.isOk(output); assert.notStrictEqual(output, frame); assert.strictEqual(output.frameClassType, FrameClassType.SynchronizedLyricsFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.SYLT)); + assert.strictEqual(frame.frameId, FrameIdentifiers.SYLT); assert.strictEqual(output.description, frame.description); assert.strictEqual(output.format, frame.format); @@ -632,7 +632,7 @@ class Id3v2_SynchronizedLyricsFrame_MethodTests { @test public render() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.SYLT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.SYLT); header.frameSize = 38; const content1 = new SynchronizedText(123, "qux"); const content2 = new SynchronizedText(456, "zux"); diff --git a/test/id3v2/termsOfUseFrameTests.ts b/test/id3v2/termsOfUseFrameTests.ts index 3f2934dc..27af4732 100644 --- a/test/id3v2/termsOfUseFrameTests.ts +++ b/test/id3v2/termsOfUseFrameTests.ts @@ -4,13 +4,12 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import TermsOfUseFrame from "../../src/id3v2/frames/termsOfUseFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; - +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; // Setup chai Chai.use(ChaiAsPromised); @@ -18,7 +17,7 @@ const assert = Chai.assert; @suite(timeout(3000), slow(1000)) class Id3v2_TermsOfUseFrame_ConstructorTests extends FrameConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return TermsOfUseFrame.fromOffsetRawData; } @@ -47,7 +46,7 @@ class Id3v2_TermsOfUseFrame_ConstructorTests extends FrameConstructorTests { @test public fromOffsetRawData_notEnoughBytes() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.USER, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.USER); header.frameSize = 2; const data = ByteVector.concatenate( header.render(4), @@ -56,13 +55,13 @@ class Id3v2_TermsOfUseFrame_ConstructorTests extends FrameConstructorTests { ); // Act / Assert - assert.throws(() => { TermsOfUseFrame.fromOffsetRawData(data, 2, header); }); + assert.throws(() => { TermsOfUseFrame.fromOffsetRawData(data, 2, header, 4); }); } @test public fromOffsetRawData_enoughData() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.USER, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.USER); header.frameSize = 10; const data = ByteVector.concatenate( header.render(4), @@ -73,7 +72,7 @@ class Id3v2_TermsOfUseFrame_ConstructorTests extends FrameConstructorTests { ); // Act - const output = TermsOfUseFrame.fromOffsetRawData(data, 2, header); + const output = TermsOfUseFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.assertFrame(output, "fux", "buxqux", StringType.Latin1); @@ -82,7 +81,7 @@ class Id3v2_TermsOfUseFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_notEnoughBytes() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.USER, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.USER); header.frameSize = 2; const data = ByteVector.concatenate( header.render(4), @@ -96,7 +95,7 @@ class Id3v2_TermsOfUseFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_enoughData() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.USER, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.USER); header.frameSize = 10; const data = ByteVector.concatenate( header.render(4), @@ -115,7 +114,7 @@ class Id3v2_TermsOfUseFrame_ConstructorTests extends FrameConstructorTests { private assertFrame(frame: TermsOfUseFrame, language: string, text: string, textEncoding: StringType) { assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.TermsOfUseFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.USER)); + assert.strictEqual(frame.frameId, FrameIdentifiers.USER); assert.strictEqual(frame.language, language); assert.strictEqual(frame.text, text); @@ -266,7 +265,7 @@ class Id3v2_TermsOfUseFrame_MethodTests { // Assert assert.isOk(output); assert.strictEqual(output.frameClassType, frame.frameClassType); - assert.isTrue(ByteVector.equal(output.frameId, frame.frameId)); + assert.strictEqual(output.frameId, frame.frameId); assert.strictEqual(output.language, frame.language); assert.strictEqual(output.text, frame.text); @@ -276,7 +275,7 @@ class Id3v2_TermsOfUseFrame_MethodTests { @test public render() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.USER, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.SYLT); header.frameSize = 10; const data = ByteVector.concatenate( header.render(4), diff --git a/test/id3v2/textInformationFrameTests.ts b/test/id3v2/textInformationFrameTests.ts index 8542f7eb..14b51fbe 100644 --- a/test/id3v2/textInformationFrameTests.ts +++ b/test/id3v2/textInformationFrameTests.ts @@ -4,19 +4,19 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import {TextInformationFrame} from "../../src/id3v2/frames/textInformationFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifier, FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; // Setup chai Chai.use(ChaiAsPromised); const assert = Chai.assert; function getTestFrame(): TextInformationFrame { - const header = new Id3v2FrameHeader(FrameTypes.TCOP, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.TCOP); header.frameSize = 8; const data = ByteVector.concatenate( header.render(4), @@ -31,7 +31,7 @@ function getTestFrame(): TextInformationFrame { @suite(timeout(3000), slow(1000)) class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return TextInformationFrame.fromOffsetRawData; } @@ -42,12 +42,12 @@ class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests @test public fromIdentifier_noEncoding_returnsFrameWithDefaultEncoding() { // Act - const frame = TextInformationFrame.fromIdentifier(FrameTypes.TCOP); + const frame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOP); // Assert assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.TextInformationFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.TCOP)); + assert.strictEqual(frame.frameId, FrameIdentifiers.TCOP); assert.isOk(frame.text); assert.isArray(frame.text); @@ -58,16 +58,16 @@ class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests @test public fromIdentifier_withEncoding_returnsFrameWithProvidedEncoding() { // Act - const frame = TextInformationFrame.fromIdentifier(FrameTypes.TCOP, StringType.Latin1); + const frame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOP, StringType.Latin1); // Assert - this.assertFrame(frame, FrameTypes.TCOP, []); + this.assertFrame(frame, FrameIdentifiers.TCOP, []); } @test public fromRawData_emptyFrameByLength_returnsEmptyFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.TCOP, 3); + const header = new Id3v2FrameHeader(FrameIdentifiers.TCOP); header.frameSize = 1; const data = ByteVector.concatenate( header.render(3), @@ -78,13 +78,13 @@ class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests const frame = TextInformationFrame.fromRawData(data, 3); // Assert - this.assertFrame(frame, FrameTypes.TCOP, []); + this.assertFrame(frame, FrameIdentifiers.TCOP, []); } @test public fromRawData_emptyFrameByContents_returnsEmptyFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.TCOP, 3); + const header = new Id3v2FrameHeader(FrameIdentifiers.TCOP); header.frameSize = 3; const data = ByteVector.concatenate( header.render(3), @@ -96,13 +96,13 @@ class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests const frame = TextInformationFrame.fromRawData(data, 3); // Assert - this.assertFrame(frame, FrameTypes.TCOP, []); + this.assertFrame(frame, FrameIdentifiers.TCOP, []); } @test public fromRawData_v4NotTxxx_returnsFrameSplitByDelimiter() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.TCOP, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.TCOP); header.frameSize = 11; const data = ByteVector.concatenate( header.render(4), @@ -117,13 +117,13 @@ class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests const frame = TextInformationFrame.fromRawData(data, 4); // Assert - this.assertFrame(frame, FrameTypes.TCOP, ["fux", "bux"]); + this.assertFrame(frame, FrameIdentifiers.TCOP, ["fux", "bux"]); } @test public fromRawData_txxx_returnsFrameSplitByDelimiter() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.TXXX, 3); + const header = new Id3v2FrameHeader(FrameIdentifiers.TXXX); header.frameSize = 11; const data = ByteVector.concatenate( header.render(4), @@ -138,13 +138,13 @@ class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests const frame = TextInformationFrame.fromRawData(data, 4); // Assert - this.assertFrame(frame, FrameTypes.TXXX, ["fux", "bux"]); + this.assertFrame(frame, FrameIdentifiers.TXXX, ["fux", "bux"]); } @test public fromRawData_v3SplitType_returnsFrameSplitBySlash() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.TCOM, 3); + const header = new Id3v2FrameHeader(FrameIdentifiers.TCOM); header.frameSize = 8; const data = ByteVector.concatenate( header.render(3), @@ -156,14 +156,14 @@ class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests const frame = TextInformationFrame.fromRawData(data, 3); // Assert - this.assertFrame(frame, FrameTypes.TCOM, ["fux", "bux"]); + this.assertFrame(frame, FrameIdentifiers.TCOM, ["fux", "bux"]); } @test public fromRawData_v3Tcon_returnsListOfStrings() { // Arrange // - Let's get crazy and try to handle all the cases at once - const header = new Id3v2FrameHeader(FrameTypes.TCON, 3); + const header = new Id3v2FrameHeader(FrameIdentifiers.TCON); header.frameSize = 67; const data = ByteVector.concatenate( header.render(3), @@ -175,7 +175,7 @@ class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests const frame = TextInformationFrame.fromRawData(data, 3); // Assert - this.assertFrame(frame, FrameTypes.TCON, [ + this.assertFrame(frame, FrameIdentifiers.TCON, [ "SomeGenre", "32", "32", @@ -188,7 +188,7 @@ class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests @test public fromOffsetRawData_v4_returnsFrameSplitByDelimiter() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.TCOP, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.TCOP); header.frameSize = 11; const data = ByteVector.concatenate( header.render(4), @@ -201,16 +201,16 @@ class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests ); // Act - const frame = TextInformationFrame.fromOffsetRawData(data, 2, header); + const frame = TextInformationFrame.fromOffsetRawData(data, 2, header, 4); // Assert - this.assertFrame(frame, FrameTypes.TCOP, ["fux", "bux"]); + this.assertFrame(frame, FrameIdentifiers.TCOP, ["fux", "bux"]); } - private assertFrame(frame: TextInformationFrame, frameId: ByteVector, text: string[]): void { + private assertFrame(frame: TextInformationFrame, frameId: FrameIdentifier, text: string[]): void { assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.TextInformationFrame); - assert.isTrue(ByteVector.equal(frame.frameId, frameId)); + assert.strictEqual(frame.frameId, frameId); assert.isOk(frame.text); assert.deepStrictEqual(frame.text, text); @@ -284,7 +284,7 @@ class Id3v2_TextInformationFrame_MethodTests { // Assert assert.ok(output); assert.strictEqual(output.frameClassType, FrameClassType.TextInformationFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.TCOP)); + assert.strictEqual(frame.frameId, FrameIdentifiers.TCOP); assert.deepStrictEqual(frame.text, output.text); assert.strictEqual(frame.textEncoding, output.textEncoding); @@ -293,8 +293,8 @@ class Id3v2_TextInformationFrame_MethodTests { @test public find_falsyFrames() { // Act - assert.throws(() => { TextInformationFrame.findTextInformationFrame(null, FrameTypes.TCOP); }); - assert.throws(() => { TextInformationFrame.findTextInformationFrame(undefined, FrameTypes.TCOP); }); + assert.throws(() => { TextInformationFrame.findTextInformationFrame(null, FrameIdentifiers.TCOP); }); + assert.throws(() => { TextInformationFrame.findTextInformationFrame(undefined, FrameIdentifiers.TCOP); }); } @test @@ -302,20 +302,18 @@ class Id3v2_TextInformationFrame_MethodTests { // Act assert.throws(() => { TextInformationFrame.findTextInformationFrame([], null); }); assert.throws(() => { TextInformationFrame.findTextInformationFrame([], undefined); }); - assert.throws(() => { TextInformationFrame.findTextInformationFrame([], ByteVector.empty()); }); - assert.throws(() => { TextInformationFrame.findTextInformationFrame([], ByteVector.fromSize(5)); }); } @test public find_frameExists() { // Arrange const frames = [ - TextInformationFrame.fromIdentifier(FrameTypes.TCOP), - TextInformationFrame.fromIdentifier(FrameTypes.TCOM) + TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOP), + TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM) ]; // Act - const output = TextInformationFrame.findTextInformationFrame(frames, FrameTypes.TCOM); + const output = TextInformationFrame.findTextInformationFrame(frames, FrameIdentifiers.TCOM); // Assert assert.isOk(output); @@ -326,12 +324,12 @@ class Id3v2_TextInformationFrame_MethodTests { public find_frameDoesNotExist() { // Arrange const frames = [ - TextInformationFrame.fromIdentifier(FrameTypes.TCOP), - TextInformationFrame.fromIdentifier(FrameTypes.TCOM) + TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOP), + TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM) ]; // Act - const output = TextInformationFrame.findTextInformationFrame(frames, FrameTypes.TALB); + const output = TextInformationFrame.findTextInformationFrame(frames, FrameIdentifiers.TALB); // Assert assert.isUndefined(output); @@ -351,7 +349,7 @@ class Id3v2_TextInformationFrame_MethodTests { @test public render_notRead_returnsRawData() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.TCOP, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.TCOP); header.frameSize = 8; const data = ByteVector.concatenate( header.render(4), @@ -373,7 +371,7 @@ class Id3v2_TextInformationFrame_MethodTests { @test public render_readV4NotTxxx() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.TCOP, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.TCOP); header.frameSize = 8; const data = ByteVector.concatenate( header.render(4), @@ -396,7 +394,7 @@ class Id3v2_TextInformationFrame_MethodTests { @test public render_readV3IsTxxxEmpty() { // Arrange - let header = new Id3v2FrameHeader(FrameTypes.TXXX, 3); + let header = new Id3v2FrameHeader(FrameIdentifiers.TXXX); header.frameSize = 1; const data = ByteVector.concatenate( header.render(3), @@ -411,7 +409,7 @@ class Id3v2_TextInformationFrame_MethodTests { // Assert assert.ok(output); - header = new Id3v2FrameHeader(FrameTypes.TXXX, 3); + header = new Id3v2FrameHeader(FrameIdentifiers.TXXX); header.frameSize = 3; const expected = ByteVector.concatenate( header.render(3), @@ -425,7 +423,7 @@ class Id3v2_TextInformationFrame_MethodTests { @test public render_readV3IsTxxxSingleValue() { // Arrange - let header = new Id3v2FrameHeader(FrameTypes.TXXX, 3); + let header = new Id3v2FrameHeader(FrameIdentifiers.TXXX); header.frameSize = 4; const data = ByteVector.concatenate( header.render(3), @@ -441,7 +439,7 @@ class Id3v2_TextInformationFrame_MethodTests { // Assert assert.ok(output); - header = new Id3v2FrameHeader(FrameTypes.TXXX, 3); + header = new Id3v2FrameHeader(FrameIdentifiers.TXXX); header.frameSize = 11; const expected = ByteVector.concatenate( header.render(3), @@ -456,7 +454,7 @@ class Id3v2_TextInformationFrame_MethodTests { @test public render_readV3Tcon() { // Arrange - let header = new Id3v2FrameHeader(FrameTypes.TCON, 3); + let header = new Id3v2FrameHeader(FrameIdentifiers.TCON); header.frameSize = 67; const data = ByteVector.concatenate( header.render(3), @@ -475,7 +473,7 @@ class Id3v2_TextInformationFrame_MethodTests { // Assert assert.ok(output); - header = new Id3v2FrameHeader(FrameTypes.TCON, 3); + header = new Id3v2FrameHeader(FrameIdentifiers.TCON); header.frameSize = 58; const expected = ByteVector.concatenate( header.render(3), @@ -489,7 +487,7 @@ class Id3v2_TextInformationFrame_MethodTests { @test public render_readV3WithSplit() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.TCOP, 3); + const header = new Id3v2FrameHeader(FrameIdentifiers.TCOP); header.frameSize = 8; const data = ByteVector.concatenate( header.render(3), @@ -510,7 +508,7 @@ class Id3v2_TextInformationFrame_MethodTests { @test public render_readV3WithoutSplit() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.TCOP, 3); + const header = new Id3v2FrameHeader(FrameIdentifiers.TCOP); header.frameSize = 4; const data = ByteVector.concatenate( header.render(3), diff --git a/test/id3v2/uniqueFileIdentifierFrameTests.ts b/test/id3v2/uniqueFileIdentifierFrameTests.ts index ef11641c..8a288b13 100644 --- a/test/id3v2/uniqueFileIdentifierFrameTests.ts +++ b/test/id3v2/uniqueFileIdentifierFrameTests.ts @@ -4,11 +4,11 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; import UniqueFileIdentifierFrame from "../../src/id3v2/frames/uniqueFileIdentifierFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; // Setup chai Chai.use(ChaiAsPromised); @@ -20,7 +20,7 @@ const testOwner = "http://github.com/benrr101/node-taglib-sharp"; @suite(timeout(3000), slow(1000)) class Id3v2_UniqueFileIdentifierFrame_ConstructorTests extends FrameConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return UniqueFileIdentifierFrame.fromOffsetRawData; } @@ -63,7 +63,7 @@ class Id3v2_UniqueFileIdentifierFrame_ConstructorTests extends FrameConstructorT @test public fromOffsetRawData_tooFewFields_returnsEmptyFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.UFID, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.UFID); header.frameSize = 9; const data = ByteVector.concatenate( header.render(4), @@ -72,7 +72,7 @@ class Id3v2_UniqueFileIdentifierFrame_ConstructorTests extends FrameConstructorT ); // Act - const frame = UniqueFileIdentifierFrame.fromOffsetRawData(data, 2, header); + const frame = UniqueFileIdentifierFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.assertFrame(frame, undefined, undefined); @@ -81,7 +81,7 @@ class Id3v2_UniqueFileIdentifierFrame_ConstructorTests extends FrameConstructorT @test public fromOffsetRawData_tooManyFields_returnsEmptyFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.UFID, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.UFID); header.frameSize = 29; const data = ByteVector.concatenate( header.render(4), @@ -95,7 +95,7 @@ class Id3v2_UniqueFileIdentifierFrame_ConstructorTests extends FrameConstructorT ); // Act - const frame = UniqueFileIdentifierFrame.fromOffsetRawData(data, 2, header); + const frame = UniqueFileIdentifierFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.assertFrame(frame, undefined, undefined); @@ -104,7 +104,7 @@ class Id3v2_UniqueFileIdentifierFrame_ConstructorTests extends FrameConstructorT @test public fromOffsetRawData_validData_returnsFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.UFID, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.UFID); header.frameSize = 54; const data = ByteVector.concatenate( header.render(4), @@ -115,7 +115,7 @@ class Id3v2_UniqueFileIdentifierFrame_ConstructorTests extends FrameConstructorT ); // Act - const frame = UniqueFileIdentifierFrame.fromOffsetRawData(data, 2, header); + const frame = UniqueFileIdentifierFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.assertFrame(frame, testOwner, testIdentifier); @@ -124,7 +124,7 @@ class Id3v2_UniqueFileIdentifierFrame_ConstructorTests extends FrameConstructorT @test public fromRawData_tooFewFields_returnsEmptyFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.UFID, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.UFID); header.frameSize = 9; const data = ByteVector.concatenate( header.render(4), @@ -141,7 +141,7 @@ class Id3v2_UniqueFileIdentifierFrame_ConstructorTests extends FrameConstructorT @test public fromRawData_tooManyFields_returnsEmptyFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.UFID, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.UFID); header.frameSize = 29; const data = ByteVector.concatenate( header.render(4), @@ -163,7 +163,7 @@ class Id3v2_UniqueFileIdentifierFrame_ConstructorTests extends FrameConstructorT @test public fromRawData_validData_returnsFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.UFID, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.UFID); header.frameSize = 54; const data = ByteVector.concatenate( header.render(4), @@ -182,7 +182,7 @@ class Id3v2_UniqueFileIdentifierFrame_ConstructorTests extends FrameConstructorT private assertFrame(frame: UniqueFileIdentifierFrame, o: string, i: ByteVector) { assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.UniqueFileIdentifierFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.UFID)); + assert.strictEqual(frame.frameId, FrameIdentifiers.UFID); assert.strictEqual(frame.owner, o); @@ -260,8 +260,8 @@ class Id3v2_UniqueFileIdentifierFrame_MethodTests { // Assert assert.isOk(clone); - assert.equal(clone.frameClassType, FrameClassType.UniqueFileIdentifierFrame); - assert.isTrue(ByteVector.equal(clone.frameId, FrameTypes.UFID)); + assert.strictEqual(clone.frameClassType, FrameClassType.UniqueFileIdentifierFrame); + assert.strictEqual(clone.frameId, FrameIdentifiers.UFID); assert.strictEqual(clone.identifier, frame.identifier); assert.strictEqual(clone.owner, frame.owner); @@ -277,8 +277,8 @@ class Id3v2_UniqueFileIdentifierFrame_MethodTests { // Assert assert.isOk(clone); - assert.equal(clone.frameClassType, FrameClassType.UniqueFileIdentifierFrame); - assert.isTrue(ByteVector.equal(clone.frameId, FrameTypes.UFID)); + assert.strictEqual(clone.frameClassType, FrameClassType.UniqueFileIdentifierFrame); + assert.strictEqual(clone.frameId, FrameIdentifiers.UFID); assert.isTrue(ByteVector.equal(clone.identifier, frame.identifier)); assert.strictEqual(clone.owner, frame.owner); @@ -287,7 +287,7 @@ class Id3v2_UniqueFileIdentifierFrame_MethodTests { @test public render_returnsByteVector() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.UFID, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.UFID); header.frameSize = 54; const data = ByteVector.concatenate( header.render(4), diff --git a/test/id3v2/unknownFrameTests.ts b/test/id3v2/unknownFrameTests.ts index 58dbcfa2..ffc4f314 100644 --- a/test/id3v2/unknownFrameTests.ts +++ b/test/id3v2/unknownFrameTests.ts @@ -3,11 +3,11 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; import UnknownFrame from "../../src/id3v2/frames/unknownFrame"; import {ByteVector} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifier, FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; // Setup chai Chai.use(ChaiAsPromised); @@ -15,7 +15,7 @@ const assert = Chai.assert; @suite(timeout(3000), slow(1000)) class Id3v2_UnknownFrame_ConstructorTests extends FrameConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return UnknownFrame.fromOffsetRawData; } @@ -33,44 +33,44 @@ class Id3v2_UnknownFrame_ConstructorTests extends FrameConstructorTests { @test public fromData_undefinedData_frameHasNoData() { // Arrange - const frameType = FrameTypes.WXXX; + const frameType = FrameIdentifiers.WXXX; // Act const frame = UnknownFrame.fromData(frameType, undefined); // Assert - this.assertFrame(frame, FrameTypes.WXXX, undefined); + this.assertFrame(frame, FrameIdentifiers.WXXX, undefined); } @test public fromData_nullData_frameHasNoData() { // Arrange - const frameType = FrameTypes.WXXX; + const frameType = FrameIdentifiers.WXXX; // Act const frame = UnknownFrame.fromData(frameType, null); // Assert - this.assertFrame(frame, FrameTypes.WXXX, undefined); + this.assertFrame(frame, FrameIdentifiers.WXXX, undefined); } @test public fromData_withData_frameHasData() { // Arrange - const frameType = FrameTypes.WXXX; + const frameType = FrameIdentifiers.WXXX; const data = ByteVector.fromString("fux qux quxx"); // Act const frame = UnknownFrame.fromData(frameType, data); // Assert - this.assertFrame(frame, FrameTypes.WXXX, data); + this.assertFrame(frame, FrameIdentifiers.WXXX, data); } @test public fromOffsetData_validParams_returnsFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); header.frameSize = 11; const data = ByteVector.concatenate( header.render(4), @@ -79,16 +79,16 @@ class Id3v2_UnknownFrame_ConstructorTests extends FrameConstructorTests { ); // Act - const frame = UnknownFrame.fromOffsetRawData(data, 2, header); + const frame = UnknownFrame.fromOffsetRawData(data, 2, header, 4); // Assert - this.assertFrame(frame, FrameTypes.WXXX, ByteVector.fromString("foo bar baz")); + this.assertFrame(frame, FrameIdentifiers.WXXX, ByteVector.fromString("foo bar baz")); } @test public fromRawData_validParams_returnsFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); header.frameSize = 11; const data = ByteVector.concatenate( header.render(4), @@ -99,13 +99,13 @@ class Id3v2_UnknownFrame_ConstructorTests extends FrameConstructorTests { const frame = UnknownFrame.fromRawData(data, 4); // Assert - this.assertFrame(frame, FrameTypes.WXXX, ByteVector.fromString("foo bar baz")); + this.assertFrame(frame, FrameIdentifiers.WXXX, ByteVector.fromString("foo bar baz")); } - private assertFrame(frame: UnknownFrame, ft: ByteVector, d: ByteVector) { + private assertFrame(frame: UnknownFrame, fi: FrameIdentifier, d: ByteVector) { assert.ok(frame); assert.strictEqual(frame.frameClassType, FrameClassType.UnknownFrame); - assert.isTrue(ByteVector.equal(frame.frameId, ft)); + assert.strictEqual(frame.frameId, fi); if (d !== undefined) { assert.isTrue(ByteVector.equal(frame.data, d)); @@ -120,7 +120,7 @@ class Id3v2_UnknownFrame_MethodTests { @test public clone_returnsCopy() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); header.frameSize = 11; const data = ByteVector.concatenate( header.render(4), @@ -134,7 +134,7 @@ class Id3v2_UnknownFrame_MethodTests { // Assert assert.ok(result); assert.strictEqual(result.frameClassType, FrameClassType.UnknownFrame); - assert.isTrue(ByteVector.equal(result.frameId, FrameTypes.WXXX)); + assert.strictEqual(result.frameId, FrameIdentifiers.WXXX); assert.isTrue(ByteVector.equal(result.data, result.data)); } @@ -142,7 +142,7 @@ class Id3v2_UnknownFrame_MethodTests { @test public render_returnsByteVector() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); header.frameSize = 11; const data = ByteVector.concatenate( header.render(4), diff --git a/test/id3v2/unsynchronizedLyricsFrameTests.ts b/test/id3v2/unsynchronizedLyricsFrameTests.ts index 36af3538..5fc02fa0 100644 --- a/test/id3v2/unsynchronizedLyricsFrameTests.ts +++ b/test/id3v2/unsynchronizedLyricsFrameTests.ts @@ -3,19 +3,19 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; +import FramePropertyTests from "./framePropertyTests"; import UnsynchronizedLyricsFrame from "../../src/id3v2/frames/unsynchronizedLyricsFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; -import FramePropertyTests from "./framePropertyTests"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; // Setup chai Chai.use(ChaiAsPromised); const assert = Chai.assert; const getTestFrameData = (): ByteVector => { - const header = new Id3v2FrameHeader(FrameTypes.USLT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.USLT); header.frameSize = 11; return ByteVector.concatenate( @@ -35,7 +35,7 @@ const getTestUnsynchronizedLyricsFrame = (): UnsynchronizedLyricsFrame => { @suite(timeout(3000), slow(1000)) class Id3v2_UnsynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return UnsynchronizedLyricsFrame.fromOffsetRawData; } @@ -60,7 +60,7 @@ class Id3v2_UnsynchronizedLyricsFrame_ConstructorTests extends FrameConstructorT @test public fromOffsetRawData_missingDescription_returnsValidFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.USLT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.USLT); header.frameSize = 7; const data = ByteVector.concatenate( 0x00, 0x00, // Offset bytes @@ -71,7 +71,7 @@ class Id3v2_UnsynchronizedLyricsFrame_ConstructorTests extends FrameConstructorT ); // Act - const frame = UnsynchronizedLyricsFrame.fromOffsetRawData(data, 2, header); + const frame = UnsynchronizedLyricsFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.assertFrame(frame, "", "eng", "foo", StringType.Latin1); @@ -80,7 +80,7 @@ class Id3v2_UnsynchronizedLyricsFrame_ConstructorTests extends FrameConstructorT @test public fromOffsetRawData_withDescription_returnsValidFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.USLT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.USLT); header.frameSize = 11; const data = ByteVector.concatenate( 0x00, 0x00, // Offset bytes @@ -93,7 +93,7 @@ class Id3v2_UnsynchronizedLyricsFrame_ConstructorTests extends FrameConstructorT ); // Act - const frame = UnsynchronizedLyricsFrame.fromOffsetRawData(data, 2, header); + const frame = UnsynchronizedLyricsFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.assertFrame(frame, "foo", "eng", "bar", StringType.Latin1); @@ -102,7 +102,7 @@ class Id3v2_UnsynchronizedLyricsFrame_ConstructorTests extends FrameConstructorT @test public fromRawData_missingDescription_returnsValidFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.USLT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.USLT); header.frameSize = 7; const data = ByteVector.concatenate( header.render(4), // Header @@ -121,7 +121,7 @@ class Id3v2_UnsynchronizedLyricsFrame_ConstructorTests extends FrameConstructorT @test public fromRawData_withDescription_returnsValidFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.USLT, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.USLT); header.frameSize = 11; const data = ByteVector.concatenate( header.render(4), // Header @@ -141,8 +141,8 @@ class Id3v2_UnsynchronizedLyricsFrame_ConstructorTests extends FrameConstructorT private assertFrame(frame: UnsynchronizedLyricsFrame, d: string, l: string, t: string, te: StringType) { assert.isOk(frame); - assert.equal(frame.frameClassType, FrameClassType.UnsynchronizedLyricsFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.USLT)); + assert.strictEqual(frame.frameClassType, FrameClassType.UnsynchronizedLyricsFrame); + assert.strictEqual(frame.frameId, FrameIdentifiers.USLT); assert.strictEqual(frame.description, d); assert.strictEqual(frame.language, l); @@ -491,8 +491,8 @@ class Id3v2_UnsynchronizedLyricsFrame_MethodTests { // Assert assert.ok(result); - assert.equal(result.frameClassType, FrameClassType.UnsynchronizedLyricsFrame); - assert.isTrue(ByteVector.equal(result.frameId, FrameTypes.USLT)); + assert.strictEqual(result.frameClassType, FrameClassType.UnsynchronizedLyricsFrame); + assert.strictEqual(result.frameId, FrameIdentifiers.USLT); assert.strictEqual(result.description, frame.description); assert.strictEqual(result.language, frame.language); diff --git a/test/id3v2/urlLinkFrameTests.ts b/test/id3v2/urlLinkFrameTests.ts index 34589428..0155e72f 100644 --- a/test/id3v2/urlLinkFrameTests.ts +++ b/test/id3v2/urlLinkFrameTests.ts @@ -3,20 +3,20 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; +import FramePropertyTests from "./framePropertyTests"; import TestConstants from "../testConstants"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifier, FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; import {UrlLinkFrame} from "../../src/id3v2/frames/urlLinkFrame"; -import FramePropertyTests from "./framePropertyTests"; // Setup chai Chai.use(ChaiAsPromised); const assert = Chai.assert; const getTestFrameData = (): ByteVector => { - const header = new Id3v2FrameHeader(FrameTypes.WCOM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.WCOM); header.frameSize = 7; return ByteVector.concatenate( @@ -34,7 +34,7 @@ const getTestUrlLinkFrame = (): UrlLinkFrame => { @suite(timeout(3000), slow(1000)) class Id3v2_UrlLinkFrame_ConstructorTests extends FrameConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return UrlLinkFrame.fromOffsetRawData; } @@ -52,16 +52,16 @@ class Id3v2_UrlLinkFrame_ConstructorTests extends FrameConstructorTests { @test public fromIdentity_validIdentity() { // Act - const output = UrlLinkFrame.fromIdentity(FrameTypes.WCOM); + const output = UrlLinkFrame.fromIdentity(FrameIdentifiers.WCOM); // Assert - this.assertFrame(output, FrameTypes.WCOM, [], StringType.Latin1); + this.assertFrame(output, FrameIdentifiers.WCOM, [], StringType.Latin1); } @test public fromOffsetRawData_notUserFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WCOM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.WCOM); header.frameSize = TestConstants.syncedUint; const data = ByteVector.concatenate( header.render(4), @@ -71,36 +71,37 @@ class Id3v2_UrlLinkFrame_ConstructorTests extends FrameConstructorTests { ); // Act - const output = UrlLinkFrame.fromOffsetRawData(data, 2, header); + const output = UrlLinkFrame.fromOffsetRawData(data, 2, header, 4); // Assert - this.assertFrame(output, FrameTypes.WCOM, ["foobar"], StringType.Latin1); + this.assertFrame(output, FrameIdentifiers.WCOM, ["foobar"], StringType.Latin1); } @test public fromOffsetRawData_userFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); - header.frameSize = 7; + const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); + header.frameSize = 4; const data = ByteVector.concatenate( header.render(4), 0x00, 0x00, - ByteVector.fromString("foo", StringType.Latin1, undefined, true), - ByteVector.getTextDelimiter(StringType.Latin1), - ByteVector.fromString("bar", StringType.Latin1, undefined, true) + StringType.UTF16BE, + ByteVector.fromString("foo", StringType.UTF16BE, undefined, true), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("bar", StringType.Latin1) ); // Act - const output = UrlLinkFrame.fromOffsetRawData(data, 2, header); + const output = UrlLinkFrame.fromOffsetRawData(data, 2, header, 4); // Assert - this.assertFrame(output, FrameTypes.WXXX, ["foo", "bar"], StringType.Latin1); + this.assertFrame(output, FrameIdentifiers.WXXX, ["foo", "bar"], StringType.Latin1); } @test public fromRawData_notUserFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WCOM, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.WCOM); header.frameSize = 8; const data = ByteVector.concatenate( header.render(4), @@ -113,32 +114,31 @@ class Id3v2_UrlLinkFrame_ConstructorTests extends FrameConstructorTests { const output = UrlLinkFrame.fromRawData(data, 4); // Assert - this.assertFrame(output, FrameTypes.WCOM, ["foobar"], StringType.Latin1); + this.assertFrame(output, FrameIdentifiers.WCOM, ["foobar"], StringType.Latin1); } @test public fromRawData_userFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); - header.frameSize = 7; + const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); + header.frameSize = 4; const data = ByteVector.concatenate( header.render(4), - ByteVector.fromString("foo", StringType.Latin1, undefined, true), - ByteVector.getTextDelimiter(StringType.Latin1), - ByteVector.fromString("bar", StringType.Latin1, undefined, true) + StringType.UTF16BE, + ByteVector.fromString("foo", StringType.UTF16BE, undefined, true) ); // Act const output = UrlLinkFrame.fromRawData(data, 4); // Assert - this.assertFrame(output, FrameTypes.WXXX, ["foo", "bar"], StringType.Latin1); + this.assertFrame(output, FrameIdentifiers.WXXX, ["foo", "bar"], StringType.Latin1); } - private assertFrame(frame: UrlLinkFrame, ft: ByteVector, t: string[], te: StringType) { + private assertFrame(frame: UrlLinkFrame, ft: FrameIdentifier, t: string[], te: StringType) { assert.ok(frame); - assert.equal(frame.frameClassType, FrameClassType.UrlLinkFrame); - assert.isTrue(ByteVector.equal(frame.frameId, ft)); + assert.strictEqual(frame.frameClassType, FrameClassType.UrlLinkFrame); + assert.strictEqual(frame.frameId, ft); assert.deepEqual(frame.text, t); assert.equal(frame.textEncoding, te); @@ -210,8 +210,8 @@ class Id3v2_UrlLinkFrame_MethodTests { @test public findUrlLinkFrame_falsyFrames_throws(): void { // Act/Assert - assert.throws(() => { UrlLinkFrame.findUrlLinkFrame(null, FrameTypes.WCOM); }); - assert.throws(() => { UrlLinkFrame.findUrlLinkFrame(undefined, FrameTypes.WCOM); }); + assert.throws(() => { UrlLinkFrame.findUrlLinkFrame(null, FrameIdentifiers.WCOM); }); + assert.throws(() => { UrlLinkFrame.findUrlLinkFrame(undefined, FrameIdentifiers.WCOM); }); } @test @@ -224,23 +224,13 @@ class Id3v2_UrlLinkFrame_MethodTests { assert.throws(() => { UrlLinkFrame.findUrlLinkFrame(frames, undefined); }); } - @test - public findUrlLinkFrame_invalidIdentity_throws() { - // Arrange - const frames = [getTestUrlLinkFrame()]; - - // Act/Assert - assert.throws(() => { UrlLinkFrame.findUrlLinkFrame(frames, ByteVector.empty()); }); - assert.throws(() => { UrlLinkFrame.findUrlLinkFrame(frames, ByteVector.fromSize(5)); }); - } - @test public findUrlLinkFrame_emptyFrames_returnsUndefined() { // Arrange const frames: UrlLinkFrame[] = []; // Act - const result = UrlLinkFrame.findUrlLinkFrame(frames, FrameTypes.WCOM); + const result = UrlLinkFrame.findUrlLinkFrame(frames, FrameIdentifiers.WCOM); // Assert assert.isUndefined(result); @@ -252,7 +242,7 @@ class Id3v2_UrlLinkFrame_MethodTests { const frames = [getTestUrlLinkFrame(), getTestUrlLinkFrame()]; // Type is WCOM // Act - const result = UrlLinkFrame.findUrlLinkFrame(frames, FrameTypes.WXXX); + const result = UrlLinkFrame.findUrlLinkFrame(frames, FrameIdentifiers.WXXX); // Assert assert.isUndefined(result); @@ -266,7 +256,7 @@ class Id3v2_UrlLinkFrame_MethodTests { const frames = [frame1, frame2]; // Act - const result = UrlLinkFrame.findUrlLinkFrame(frames, FrameTypes.WCOM); + const result = UrlLinkFrame.findUrlLinkFrame(frames, FrameIdentifiers.WCOM); // Assert assert.equal(result, frame1); @@ -282,7 +272,7 @@ class Id3v2_UrlLinkFrame_MethodTests { // Assert assert.isOk(result); - assert.isTrue(ByteVector.equal(result.frameId, frame.frameId)); + assert.strictEqual(result.frameId, frame.frameId); assert.deepEqual(result.text, frame.text); assert.deepEqual(result.textEncoding, frame.textEncoding); } @@ -291,14 +281,14 @@ class Id3v2_UrlLinkFrame_MethodTests { public clone_withoutRawData_returnsClone() { // Arrange const frame = getTestUrlLinkFrame(); - const text = frame.text; // Force reading raw data, and trashing it + const _ = frame.text; // Force reading raw data, and trashing it // Act const result = frame.clone(); // Assert assert.isOk(result); - assert.isTrue(ByteVector.equal(result.frameId, frame.frameId)); + assert.strictEqual(result.frameId, frame.frameId); assert.deepEqual(result.text, frame.text); assert.deepEqual(result.textEncoding, frame.textEncoding); } diff --git a/test/id3v2/userTextInformationFrameTests.ts b/test/id3v2/userTextInformationFrameTests.ts index 6ba5f439..26486b96 100644 --- a/test/id3v2/userTextInformationFrameTests.ts +++ b/test/id3v2/userTextInformationFrameTests.ts @@ -3,19 +3,19 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; import {UserTextInformationFrame} from "../../src/id3v2/frames/textInformationFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; // Setup chai Chai.use(ChaiAsPromised); const assert = Chai.assert; function getTestFrame(): UserTextInformationFrame { - const header = new Id3v2FrameHeader(FrameTypes.TXXX, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.TXXX); header.frameSize = 8; const data = ByteVector.concatenate( header.render(4), @@ -30,7 +30,7 @@ function getTestFrame(): UserTextInformationFrame { @suite(timeout(3000), slow(1000)) class Id3v2_UserInformationFrame_ConstructorTests extends FrameConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return UserTextInformationFrame.fromOffsetRawData; } @@ -59,7 +59,7 @@ class Id3v2_UserInformationFrame_ConstructorTests extends FrameConstructorTests @test public fromOffsetRawData_returnsFrame() { // Assert - const header = new Id3v2FrameHeader(FrameTypes.TXXX, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.TXXX); header.frameSize = 8; const data = ByteVector.concatenate( header.render(4), @@ -71,7 +71,7 @@ class Id3v2_UserInformationFrame_ConstructorTests extends FrameConstructorTests ); // Act - const frame = UserTextInformationFrame.fromOffsetRawData(data, 2, header); + const frame = UserTextInformationFrame.fromOffsetRawData(data, 2, header, 4); // Assert this.assertFrame(frame, "foo", ["bar"], StringType.Latin1); @@ -80,7 +80,7 @@ class Id3v2_UserInformationFrame_ConstructorTests extends FrameConstructorTests @test public fromRawData_returnsFrame() { // Assert - const header = new Id3v2FrameHeader(FrameTypes.TXXX, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.TXXX); header.frameSize = 8; const data = ByteVector.concatenate( header.render(4), @@ -105,7 +105,7 @@ class Id3v2_UserInformationFrame_ConstructorTests extends FrameConstructorTests ) { assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.UserTextInformationFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.TXXX)); + assert.strictEqual(frame.frameId, FrameIdentifiers.TXXX); assert.isOk(frame.text); assert.strictEqual(frame.description, description); @@ -239,7 +239,7 @@ class Id3v2_UserTextInformationFrame_MethodTests { // Assert assert.ok(output); assert.strictEqual(output.frameClassType, FrameClassType.UserTextInformationFrame); - assert.isTrue(ByteVector.equal(frame.frameId, FrameTypes.TXXX)); + assert.strictEqual(frame.frameId, FrameIdentifiers.TXXX); assert.strictEqual(frame.description, output.description); assert.deepStrictEqual(frame.text, output.text); diff --git a/test/id3v2/userUrlLinkFrameTests.ts b/test/id3v2/userUrlLinkFrameTests.ts index 50d41456..42e456a4 100644 --- a/test/id3v2/userUrlLinkFrameTests.ts +++ b/test/id3v2/userUrlLinkFrameTests.ts @@ -3,11 +3,11 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FrameTypes from "../../src/id3v2/frameIdentifiers"; import TestConstants from "../testConstants"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameFlags, Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; import {UserUrlLinkFrame} from "../../src/id3v2/frames/urlLinkFrame"; // Setup chai @@ -15,7 +15,7 @@ Chai.use(ChaiAsPromised); const assert = Chai.assert; const getTestFrameData = (): ByteVector => { - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); + const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); header.frameSize = 7; return ByteVector.concatenate( @@ -33,7 +33,7 @@ const getTestUserUrlLinkFrame = (): UserUrlLinkFrame => { @suite(timeout(3000), slow(1000)) class Id3v2_UserUrlLinkFrame_ConstructorTests extends FrameConstructorTests { - public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader) => Frame { + public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return UserUrlLinkFrame.fromOffsetRawData; } @@ -44,8 +44,8 @@ class Id3v2_UserUrlLinkFrame_ConstructorTests extends FrameConstructorTests { @test public fromOffsetRawData_userFrame() { // Arrange - const header = new Id3v2FrameHeader(FrameTypes.WXXX, 4); - header.frameSize = TestConstants.syncedUint; + const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); + header.frameSize = 7; // Offset bytes // Header @@ -59,17 +59,12 @@ class Id3v2_UserUrlLinkFrame_ConstructorTests extends FrameConstructorTests { ); // Act - const output = UserUrlLinkFrame.fromOffsetRawData(data, 2, header); + const output = UserUrlLinkFrame.fromOffsetRawData(data, 2, header, 4); // Assert assert.ok(output); assert.equal(output.frameClassType, FrameClassType.UserUrlLinkFrame); - - assert.equal(output.encryptionId, -1); - assert.equal(output.flags, Id3v2FrameFlags.None); - assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.WXXX)); - assert.equal(output.groupId, -1); - assert.equal(output.size, TestConstants.syncedUint); + assert.strictEqual(output.frameId, FrameIdentifiers.WXXX); assert.strictEqual(output.description, "foo"); assert.deepEqual(output.text, ["bar"]); @@ -79,9 +74,10 @@ class Id3v2_UserUrlLinkFrame_ConstructorTests extends FrameConstructorTests { @test public fromRawData_userFrame() { // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); + header.frameSize = 7; const data = ByteVector.concatenate( - FrameTypes.WXXX, // - Frame ID - TestConstants.syncedUintBytes, // - Frame size + header.render(4), 0x00, 0x00, // - Frame flags ByteVector.fromString("foo", StringType.Latin1, undefined, true), // - String 1 ByteVector.getTextDelimiter(StringType.Latin1), // - String separator @@ -94,12 +90,7 @@ class Id3v2_UserUrlLinkFrame_ConstructorTests extends FrameConstructorTests { // Assert assert.ok(output); assert.equal(output.frameClassType, FrameClassType.UserUrlLinkFrame); - - assert.equal(output.encryptionId, -1); - assert.equal(output.flags, Id3v2FrameFlags.None); - assert.isTrue(ByteVector.equal(output.frameId, FrameTypes.WXXX)); - assert.equal(output.groupId, -1); - assert.equal(output.size, TestConstants.syncedUint); + assert.strictEqual(output.frameId, FrameIdentifiers.WXXX); assert.strictEqual(output.description, "foo"); assert.deepEqual(output.text, ["bar"]); @@ -112,7 +103,7 @@ class Id3v2_UserUrlLinkFrame_PropertyTests { @test public getDescription_emptyText_returnsUndefined() { // Arrange - const data = new Id3v2FrameHeader(FrameTypes.WXXX, 4).render(4); + const data = new Id3v2FrameHeader(FrameIdentifiers.WXXX).render(4); const frame = UserUrlLinkFrame.fromRawData(data, 4); // Act @@ -164,9 +155,10 @@ class Id3v2_UserUrlLinkFrame_PropertyTests { @test public setDescription_noDescription_value() { // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); + header.frameSize = 0; const data = ByteVector.concatenate( - FrameTypes.WXXX, // - Frame ID - 0x00, 0x00, 0x00, 0x00, // - Frame size + header.render(4), 0x00, 0x00, // - Frame flags ); const frame = UserUrlLinkFrame.fromRawData(data, 4); @@ -321,7 +313,7 @@ class Id3v2_UserUrlLink_MethodTests { // Assert assert.isOk(result); - assert.isTrue(ByteVector.equal(result.frameId, frame.frameId)); + assert.strictEqual(result.frameId, frame.frameId); assert.strictEqual(result.description, frame.description); assert.deepEqual(result.text, frame.text); assert.deepEqual(result.textEncoding, frame.textEncoding); @@ -331,14 +323,14 @@ class Id3v2_UserUrlLink_MethodTests { public clone_withoutRawData_returnsClone() { // Arrange const frame = getTestUserUrlLinkFrame(); - const text = frame.text; // Force reading raw data, and trashing it + const _ = frame.text; // Force reading raw data, and trashing it // Act const result = frame.clone(); // Assert assert.isOk(result); - assert.isTrue(ByteVector.equal(result.frameId, frame.frameId)); + assert.strictEqual(result.frameId, frame.frameId); assert.strictEqual(result.description, frame.description); assert.deepEqual(result.text, frame.text); assert.strictEqual(result.textEncoding, frame.textEncoding); From 666cbc3994290bd69f1696f0c27cddbdb7160f61 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 8 Feb 2020 14:02:52 -0500 Subject: [PATCH 40/71] Patching up the last round of unit tests. --- src/id3v2/frames/urlLinkFrame.ts | 27 +++++++++++++++------------ test/id3v2/urlLinkFrameTests.ts | 19 +++++++++++-------- test/id3v2/userUrlLinkFrameTests.ts | 18 ++++++++---------- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/id3v2/frames/urlLinkFrame.ts b/src/id3v2/frames/urlLinkFrame.ts index 1b87f7b6..cec8d009 100644 --- a/src/id3v2/frames/urlLinkFrame.ts +++ b/src/id3v2/frames/urlLinkFrame.ts @@ -181,7 +181,7 @@ export class UrlLinkFrame extends Frame { const fieldList = []; let index = 0; - if (this.frameId === FrameIdentifiers.WXXX) { + if (this.frameId === FrameIdentifiers.WXXX && data.length > 0) { // Text Encoding $xx // Description $00 (00) // URL @@ -192,26 +192,29 @@ export class UrlLinkFrame extends Frame { if (delimIndex >= 0) { const description = data.toString(delimIndex - 1, encoding, 1); fieldList.push(description); - index += delimIndex - 1; + index += delimIndex - 1 + delim.length; } index += 1; } - // Read the url from the data - let url = data.toString(data.length - index, StringType.Latin1, index); + if (index < data.length) { - // Do a fast removal of end bytes - if (url.length > 1 && url[url.length - 1] === "\0") { - for (let i = url.length - 1; i >= 0; i--) { - if (url[i] !== "\0") { - url = url.substr(0, i + 1); - break; + // Read the url from the data + let url = data.toString(data.length - index, StringType.Latin1, index); + + // Do a fast removal of end bytes + if (url.length > 1 && url[url.length - 1] === "\0") { + for (let i = url.length - 1; i >= 0; i--) { + if (url[i] !== "\0") { + url = url.substr(0, i + 1); + break; + } } } - } - fieldList.push(url); + fieldList.push(url); + } this._textFields = fieldList; } diff --git a/test/id3v2/urlLinkFrameTests.ts b/test/id3v2/urlLinkFrameTests.ts index 0155e72f..58d0097e 100644 --- a/test/id3v2/urlLinkFrameTests.ts +++ b/test/id3v2/urlLinkFrameTests.ts @@ -16,11 +16,12 @@ Chai.use(ChaiAsPromised); const assert = Chai.assert; const getTestFrameData = (): ByteVector => { - const header = new Id3v2FrameHeader(FrameIdentifiers.WCOM); - header.frameSize = 7; + const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); + header.frameSize = 8; return ByteVector.concatenate( header.render(4), + StringType.Latin1, ByteVector.fromString("foo", StringType.Latin1, undefined, true), ByteVector.getTextDelimiter(StringType.Latin1), ByteVector.fromString("bar", StringType.Latin1, undefined, true) @@ -81,7 +82,7 @@ class Id3v2_UrlLinkFrame_ConstructorTests extends FrameConstructorTests { public fromOffsetRawData_userFrame() { // Arrange const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); - header.frameSize = 4; + header.frameSize = 12; const data = ByteVector.concatenate( header.render(4), 0x00, 0x00, @@ -121,11 +122,13 @@ class Id3v2_UrlLinkFrame_ConstructorTests extends FrameConstructorTests { public fromRawData_userFrame() { // Arrange const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); - header.frameSize = 4; + header.frameSize = 12; const data = ByteVector.concatenate( header.render(4), StringType.UTF16BE, - ByteVector.fromString("foo", StringType.UTF16BE, undefined, true) + ByteVector.fromString("foo", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("bar", StringType.Latin1) ); // Act @@ -239,10 +242,10 @@ class Id3v2_UrlLinkFrame_MethodTests { @test public findUrlLinkFrame_noMatch_returnsUndefined() { // Arrange - const frames = [getTestUrlLinkFrame(), getTestUrlLinkFrame()]; // Type is WCOM + const frames = [getTestUrlLinkFrame(), getTestUrlLinkFrame()]; // Type is WXXX // Act - const result = UrlLinkFrame.findUrlLinkFrame(frames, FrameIdentifiers.WXXX); + const result = UrlLinkFrame.findUrlLinkFrame(frames, FrameIdentifiers.WCOM); // Assert assert.isUndefined(result); @@ -256,7 +259,7 @@ class Id3v2_UrlLinkFrame_MethodTests { const frames = [frame1, frame2]; // Act - const result = UrlLinkFrame.findUrlLinkFrame(frames, FrameIdentifiers.WCOM); + const result = UrlLinkFrame.findUrlLinkFrame(frames, FrameIdentifiers.WXXX); // Assert assert.equal(result, frame1); diff --git a/test/id3v2/userUrlLinkFrameTests.ts b/test/id3v2/userUrlLinkFrameTests.ts index 42e456a4..17ae0f01 100644 --- a/test/id3v2/userUrlLinkFrameTests.ts +++ b/test/id3v2/userUrlLinkFrameTests.ts @@ -16,12 +16,13 @@ const assert = Chai.assert; const getTestFrameData = (): ByteVector => { const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); - header.frameSize = 7; + header.frameSize = 12; return ByteVector.concatenate( header.render(4), - ByteVector.fromString("foo", StringType.Latin1, undefined, true), - ByteVector.getTextDelimiter(StringType.Latin1), + StringType.UTF16BE, + ByteVector.fromString("foo", StringType.UTF16BE, undefined, true), + ByteVector.getTextDelimiter(StringType.UTF16BE), ByteVector.fromString("bar", StringType.Latin1, undefined, true) ); }; @@ -45,14 +46,11 @@ class Id3v2_UserUrlLinkFrame_ConstructorTests extends FrameConstructorTests { public fromOffsetRawData_userFrame() { // Arrange const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); - header.frameSize = 7; - - // Offset bytes - // Header - // Data (2x strings w/divider) + header.frameSize = 8; const data = ByteVector.concatenate( 0x00, 0x00, header.render(4), + StringType.Latin1, ByteVector.fromString("foo", StringType.Latin1, undefined, true), ByteVector.getTextDelimiter(StringType.Latin1), ByteVector.fromString("bar", StringType.Latin1, undefined, true) @@ -75,10 +73,10 @@ class Id3v2_UserUrlLinkFrame_ConstructorTests extends FrameConstructorTests { public fromRawData_userFrame() { // Arrange const header = new Id3v2FrameHeader(FrameIdentifiers.WXXX); - header.frameSize = 7; + header.frameSize = 8; const data = ByteVector.concatenate( header.render(4), - 0x00, 0x00, // - Frame flags + StringType.Latin1, ByteVector.fromString("foo", StringType.Latin1, undefined, true), // - String 1 ByteVector.getTextDelimiter(StringType.Latin1), // - String separator ByteVector.fromString("bar", StringType.Latin1, undefined, true) // - String 2 From 2643ed2737efc67e4708bfbc0b1dee5aa821f234 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 8 Feb 2020 14:31:07 -0500 Subject: [PATCH 41/71] Adding tests for frame identifiers --- test/id3v2/frameIdentifiersTests.ts | 152 ++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 test/id3v2/frameIdentifiersTests.ts diff --git a/test/id3v2/frameIdentifiersTests.ts b/test/id3v2/frameIdentifiersTests.ts new file mode 100644 index 00000000..de61a582 --- /dev/null +++ b/test/id3v2/frameIdentifiersTests.ts @@ -0,0 +1,152 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import {ByteVector} from "../../src/byteVector"; +import {FrameIdentifier, FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; + +// Setup Chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(timeout(3000), slow(1000)) +class FrameIdentifierTests { + @test + public isTextFrame_v2StartsWithT() { + // Arrange + const identifier = new FrameIdentifier(undefined, "XYZZ", "TCH"); + + // Act + const output = identifier.isTextFrame; + + // Assert + assert.isTrue(output); + } + + @test + public isTextFrame_v3StartsWithT() { + // Arrange + const identifier = new FrameIdentifier(undefined, "TCCH", "XYZ"); + + // Act + const output = identifier.isTextFrame; + + // Assert + assert.isTrue(output); + } + + @test + public isTextFrame_v4StartsWithT() { + // Arrange + const identifier = new FrameIdentifier("TCCH", "XYZZ", undefined); + + // Act + const output = identifier.isTextFrame; + + // Assert + assert.isTrue(output); + } + + @test + public isTextFrame_noVersionStartsWithT() { + // Arrange + const identifier = new FrameIdentifier("XYZZ", undefined, "XYZ"); + + // Act + const output = identifier.isTextFrame; + + // Assert + assert.isFalse(output); + } + + @test + public isUrlFrame_v2StartsWithW() { + // Arrange + const identifier = new FrameIdentifier(undefined, undefined, "WCH"); + + // Act + const output = identifier.isUrlFrame; + + // Assert + assert.isTrue(output); + } + + @test + public isUrlFrame_v3StartsWithW() { + // Arrange + const identifier = new FrameIdentifier(undefined, "WCCH", undefined); + + // Act + const output = identifier.isUrlFrame; + + // Assert + assert.isTrue(output); + } + + @test + public isUrlFrame_v4StartsWithW() { + // Arrange + const identifier = new FrameIdentifier("WCCH", undefined, undefined); + + // Act + const output = identifier.isUrlFrame; + + // Assert + assert.isTrue(output); + } + + @test + public render_invalidVersion() { + // Act / Assert + assert.throws(() => { FrameIdentifiers.RVA2.render(-1); }); + assert.throws(() => { FrameIdentifiers.RVA2.render(1.23); }); + assert.throws(() => { FrameIdentifiers.RVA2.render(256); }); + assert.throws(() => { FrameIdentifiers.RVA2.render(1); }); + assert.throws(() => { FrameIdentifiers.RVA2.render(5); }); + } + + @test + public render_v2() { + // Arrange + const identifier = new FrameIdentifier("ABCD", "DEFG", "HIJ"); + + // Act + const output = identifier.render(2); + + // Assert + assert.isTrue(ByteVector.equal(output, ByteVector.fromString("HIJ"))); + } + + @test + public render_v3() { + // Arrange + const identifier = new FrameIdentifier("ABCD", "DEFG", "HIJ"); + + // Act + const output = identifier.render(3); + + // Assert + assert.isTrue(ByteVector.equal(output, ByteVector.fromString("DEFG"))); + } + + @test + public render_v4() { + // Arrange + const identifier = new FrameIdentifier("ABCD", "DEFG", "HIJ"); + + // Act + const output = identifier.render(4); + + // Assert + assert.isTrue(ByteVector.equal(output, ByteVector.fromString("ABCD"))); + } + + @test + public render_unsupported() { + // Arrange + const identifier = new FrameIdentifier("ABCD", undefined, undefined); + + // Act / Assert + assert.throws(() => { const _ = identifier.render(2); }); + } +} From d45007fcf6a849104d11c71f5f01a7e2f26f1d36 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 8 Feb 2020 16:20:05 -0500 Subject: [PATCH 42/71] Constructor tests for attachment frame. They don't work yet. --- src/id3v2/frames/attachmentFrame.ts | 4 +- test/id3v2/attachmentsFrameTests.ts | 252 +++++++++++++++++++++++++++- 2 files changed, 247 insertions(+), 9 deletions(-) diff --git a/src/id3v2/frames/attachmentFrame.ts b/src/id3v2/frames/attachmentFrame.ts index 9b1157ec..32c703a4 100644 --- a/src/id3v2/frames/attachmentFrame.ts +++ b/src/id3v2/frames/attachmentFrame.ts @@ -355,7 +355,7 @@ export default class AttachmentFrame extends Frame { this._type = data.get(mimeTypeEndIndex + 1); - descriptionEndIndex = data.find(delim, mimeTypeEndIndex + 1); + descriptionEndIndex = data.find(delim, mimeTypeEndIndex + 1, delim.length); const descriptionLength = descriptionEndIndex - mimeTypeLength - 1; this._description = data.toString( descriptionLength, @@ -371,7 +371,7 @@ export default class AttachmentFrame extends Frame { const imageFormat = data.toString(3, StringType.Latin1, 1); this._mimeType = Picture.getMimeTypeFromFilename(imageFormat); - descriptionEndIndex = data.find(delim, 5); + descriptionEndIndex = data.find(delim, 5, delim.length); const descriptionLength = descriptionEndIndex - 5; this._description = data.toString(descriptionLength, this._encoding, 5); } diff --git a/test/id3v2/attachmentsFrameTests.ts b/test/id3v2/attachmentsFrameTests.ts index 7c1491ec..c74c704e 100644 --- a/test/id3v2/attachmentsFrameTests.ts +++ b/test/id3v2/attachmentsFrameTests.ts @@ -4,18 +4,256 @@ import * as TypeMoq from "typemoq"; import {slow, suite, test, timeout} from "mocha-typescript"; import AttachmentFrame from "../../src/id3v2/frames/attachmentFrame"; +import FrameConstructorTests from "./frameConstructorTests"; +import {ByteVector, StringType} from "../../src/byteVector"; +import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; +import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifier, FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; import {IPicture, PictureType} from "../../src/picture"; -import TestConstants from "../testConstants"; -import {FrameClassType} from "../../src/id3v2/frames/frame"; - import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; -import {ByteVector} from "../../src/byteVector"; -import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; -import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; -import {IFileAbstraction} from "../../src/fileAbstraction"; // Setup chai Chai.use(ChaiAsPromised); const assert = Chai.assert; +@suite(timeout(3000), slow(1000)) +class Id3v2_AttachmentFrame_ConstructorTests extends FrameConstructorTests { + get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { + return AttachmentFrame.fromOffsetRawData; + } + + get fromRawData(): (d: ByteVector, v: number) => Frame { + return AttachmentFrame.fromRawData; + } + + @test + public fromPicture_falsyPicture() { + // Act / Assert + assert.throws(() => { const _ = AttachmentFrame.fromPicture(null); }); + assert.throws(() => { const _ = AttachmentFrame.fromPicture(undefined); }); + } + + @test + public fromPicture_validPicture() { + // Arrange + const data = ByteVector.fromString("foobarbaz"); + const mockPicture = TypeMoq.Mock.ofType(); + mockPicture.setup((p) => p.data).returns(() => data); + mockPicture.setup((p) => p.description).returns(() => "fux"); + mockPicture.setup((p) => p.filename).returns(() => "bux"); + mockPicture.setup((p) => p.mimeType).returns(() => "application/octet-stream"); + mockPicture.setup((p) => p.type).returns(() => PictureType.FrontCover); + + // Act + const frame = AttachmentFrame.fromPicture(mockPicture.object); + + // Assert + this.verifyFrame( + frame, + FrameIdentifiers.APIC, + data, + "fux", + "bux", + "application/octet-stream", + Id3v2TagSettings.defaultEncoding, + PictureType.FrontCover + ); + } + + @test + public fromPicture_notAPicture() { + // Arrange + const data = ByteVector.fromString("foobarbaz"); + const mockPicture = TypeMoq.Mock.ofType(); + mockPicture.setup((p) => p.data).returns(() => data); + mockPicture.setup((p) => p.description).returns(() => "fux"); + mockPicture.setup((p) => p.filename).returns(() => "bux"); + mockPicture.setup((p) => p.mimeType).returns(() => "application/octet-stream"); + mockPicture.setup((p) => p.type).returns(() => PictureType.NotAPicture); + + // Act + const frame = AttachmentFrame.fromPicture(mockPicture.object); + + // Assert + this.verifyFrame( + frame, + FrameIdentifiers.GEOB, + data, + "fux", + "bux", + "application/octet-stream", + Id3v2TagSettings.defaultEncoding, + PictureType.NotAPicture + ); + } + + @test + public fromRawData_invalidFrameIdentifier() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.TXXX); + header.frameSize = 10; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromSize(10) + ); + + // Act / Assert + const frame = AttachmentFrame.fromRawData(data, 4); + assert.throws(() => { const _ = frame.type; }); + } + + @test + public fromRawData_apicV4() { + // Arrange + const testData = ByteVector.fromString("fuxbuxqux", StringType.Latin1); + const header = new Id3v2FrameHeader(FrameIdentifiers.APIC); + header.frameSize = 40; + const data = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("image/gif", StringType.Latin1), + PictureType.Artist, + ByteVector.fromString("foobarbaz", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + testData + ); + + // Act + const frame = AttachmentFrame.fromRawData(data, 4); + + // Assert + this.verifyFrame( + frame, + FrameIdentifiers.APIC, + testData, + "foobarbaz", + undefined, + "image/gif", + StringType.UTF16BE, + PictureType.Artist + ); + } + + @test + public fromRawData_apicV2() { + // Arrange + const testData = ByteVector.fromString("fuxbuxqux", StringType.Latin1); + const header = new Id3v2FrameHeader(FrameIdentifiers.APIC); + header.frameSize = 34; + const data = ByteVector.concatenate( + header.render(2), + StringType.UTF16BE, + ByteVector.fromString("GIF", StringType.Latin1), + PictureType.Artist, + ByteVector.fromString("foobarbaz", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + testData + ); + + // Act + const frame = AttachmentFrame.fromRawData(data, 2); + + // Assert + this.verifyFrame( + frame, + FrameIdentifiers.APIC, + testData, + "foobarbaz", + undefined, + "image/gif", + StringType.UTF16BE, + PictureType.Artist + ); + } + + @test + public fromRawData_geob() { + // Arrange + const testData = ByteVector.fromString("fuxbuxqux", StringType.Latin1); + const header = new Id3v2FrameHeader(FrameIdentifiers.GEOB); + header.frameSize = 60; + const data = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("image/gif", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + ByteVector.fromString("image.gif", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("foobarbaz", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + testData + ); + + // Act + const frame = AttachmentFrame.fromRawData(data, 4); + + // Assert + this.verifyFrame( + frame, + FrameIdentifiers.GEOB, + testData, + "foobarbaz", + "image.gif", + "image/gif", + StringType.UTF16BE, + PictureType.Artist + ); + } + + @test + public fromOffsetRawData_apicV4() { + // Arrange + const testData = ByteVector.fromString("fuxbuxqux", StringType.Latin1); + const header = new Id3v2FrameHeader(FrameIdentifiers.APIC); + header.frameSize = 40; + const data = ByteVector.concatenate( + header.render(4), + 0x00, 0x00, + StringType.UTF16BE, + ByteVector.fromString("image/gif", StringType.Latin1), + PictureType.Artist, + ByteVector.fromString("foobarbaz", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + testData + ); + + // Act + const frame = AttachmentFrame.fromOffsetRawData(data, 2, header, 4); + + // Assert + this.verifyFrame( + frame, + FrameIdentifiers.APIC, + testData, + "foobarbaz", + undefined, + "image/gif", + StringType.UTF16BE, + PictureType.Artist + ); + } + + private verifyFrame( + frame: AttachmentFrame, + ft: FrameIdentifier, + d: ByteVector, + desc: string, + fn: string, + mt: string, + te: StringType, + t: PictureType + ) { + assert.isOk(frame); + assert.equal(frame.frameClassType, FrameClassType.AttachmentFrame); + assert.strictEqual(frame.frameId, ft); + + assert.isTrue(ByteVector.equal(frame.data, d)); + assert.strictEqual(frame.description, desc); + assert.strictEqual(frame.filename, fn); + assert.strictEqual(frame.mimeType, mt); + assert.strictEqual(frame.textEncoding, te); + assert.strictEqual(frame.type, t); + } +} + From f680e2050a6a9f5e48845ab02f7b8f1e63cca5c9 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sun, 9 Feb 2020 22:53:49 -0500 Subject: [PATCH 43/71] Fixing test bugs in attachment frame constructor tests --- src/id3v2/frames/attachmentFrame.ts | 8 +++++--- src/picture.ts | 2 ++ test/id3v2/attachmentsFrameTests.ts | 11 +++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/id3v2/frames/attachmentFrame.ts b/src/id3v2/frames/attachmentFrame.ts index 32c703a4..da2b2146 100644 --- a/src/id3v2/frames/attachmentFrame.ts +++ b/src/id3v2/frames/attachmentFrame.ts @@ -355,12 +355,12 @@ export default class AttachmentFrame extends Frame { this._type = data.get(mimeTypeEndIndex + 1); - descriptionEndIndex = data.find(delim, mimeTypeEndIndex + 1, delim.length); - const descriptionLength = descriptionEndIndex - mimeTypeLength - 1; + descriptionEndIndex = data.find(delim, mimeTypeEndIndex + 2, delim.length); + const descriptionLength = descriptionEndIndex - mimeTypeEndIndex - 1; this._description = data.toString( descriptionLength, this._encoding, - mimeTypeEndIndex + 1 + mimeTypeEndIndex + 2 ); } else { // Text encoding $xx @@ -371,6 +371,8 @@ export default class AttachmentFrame extends Frame { const imageFormat = data.toString(3, StringType.Latin1, 1); this._mimeType = Picture.getMimeTypeFromFilename(imageFormat); + this._type = data.get(4); + descriptionEndIndex = data.find(delim, 5, delim.length); const descriptionLength = descriptionEndIndex - 5; this._description = data.toString(descriptionLength, this._encoding, 5); diff --git a/src/picture.ts b/src/picture.ts index c9580d9f..82107dba 100644 --- a/src/picture.ts +++ b/src/picture.ts @@ -365,6 +365,8 @@ export class Picture implements IPicture { ext = ext.substring(1); } + ext = ext.toLowerCase(); + for (let i = 0; i < this._lutExtensionMime.length; i += 2) { if (this._lutExtensionMime[i] === ext) { mimeType = this._lutExtensionMime[i + 1]; diff --git a/test/id3v2/attachmentsFrameTests.ts b/test/id3v2/attachmentsFrameTests.ts index c74c704e..09268ed6 100644 --- a/test/id3v2/attachmentsFrameTests.ts +++ b/test/id3v2/attachmentsFrameTests.ts @@ -107,11 +107,12 @@ class Id3v2_AttachmentFrame_ConstructorTests extends FrameConstructorTests { // Arrange const testData = ByteVector.fromString("fuxbuxqux", StringType.Latin1); const header = new Id3v2FrameHeader(FrameIdentifiers.APIC); - header.frameSize = 40; + header.frameSize = 41; const data = ByteVector.concatenate( header.render(4), StringType.UTF16BE, ByteVector.fromString("image/gif", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), PictureType.Artist, ByteVector.fromString("foobarbaz", StringType.UTF16BE), ByteVector.getTextDelimiter(StringType.UTF16BE), @@ -196,7 +197,7 @@ class Id3v2_AttachmentFrame_ConstructorTests extends FrameConstructorTests { "image.gif", "image/gif", StringType.UTF16BE, - PictureType.Artist + undefined ); } @@ -205,12 +206,13 @@ class Id3v2_AttachmentFrame_ConstructorTests extends FrameConstructorTests { // Arrange const testData = ByteVector.fromString("fuxbuxqux", StringType.Latin1); const header = new Id3v2FrameHeader(FrameIdentifiers.APIC); - header.frameSize = 40; + header.frameSize = 41; const data = ByteVector.concatenate( header.render(4), 0x00, 0x00, StringType.UTF16BE, ByteVector.fromString("image/gif", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), PictureType.Artist, ByteVector.fromString("foobarbaz", StringType.UTF16BE), ByteVector.getTextDelimiter(StringType.UTF16BE), @@ -245,7 +247,6 @@ class Id3v2_AttachmentFrame_ConstructorTests extends FrameConstructorTests { ) { assert.isOk(frame); assert.equal(frame.frameClassType, FrameClassType.AttachmentFrame); - assert.strictEqual(frame.frameId, ft); assert.isTrue(ByteVector.equal(frame.data, d)); assert.strictEqual(frame.description, desc); @@ -253,6 +254,8 @@ class Id3v2_AttachmentFrame_ConstructorTests extends FrameConstructorTests { assert.strictEqual(frame.mimeType, mt); assert.strictEqual(frame.textEncoding, te); assert.strictEqual(frame.type, t); + + assert.strictEqual(frame.frameId, ft); } } From cb57a86aeedd658b852c15179813fd6377284170 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Mon, 10 Feb 2020 23:06:07 -0500 Subject: [PATCH 44/71] Finished tests for attachment frame --- src/id3v2/frames/attachmentFrame.ts | 5 +- test/id3v2/attachmentsFrameTests.ts | 317 ++++++++++++++++++++++++++++ 2 files changed, 320 insertions(+), 2 deletions(-) diff --git a/src/id3v2/frames/attachmentFrame.ts b/src/id3v2/frames/attachmentFrame.ts index da2b2146..0647aa55 100644 --- a/src/id3v2/frames/attachmentFrame.ts +++ b/src/id3v2/frames/attachmentFrame.ts @@ -191,7 +191,7 @@ export default class AttachmentFrame extends Frame { ? FrameIdentifiers.GEOB : FrameIdentifiers.APIC; if (this._header.frameId !== frameId) { - this._header = new Id3v2FrameHeader(frameId, 4); + this._header = new Id3v2FrameHeader(frameId); } this._type = value; @@ -211,6 +211,7 @@ export default class AttachmentFrame extends Frame { frame._rawVersion = this._rawVersion; } else { frame._data = ByteVector.fromByteVector(this._data); + frame._description = this._description; frame._encoding = this._encoding; frame._filename = this._filename; frame._mimeType = this._mimeType; @@ -237,7 +238,7 @@ export default class AttachmentFrame extends Frame { ): AttachmentFrame { Guards.truthy(frames, "frames"); return frames.find((f) => { - if (f.description && f.description !== description) { return false; } + if (description && f.description !== description) { return false; } if (type !== PictureType.Other && f.type !== type) { return false; } return true; }); diff --git a/test/id3v2/attachmentsFrameTests.ts b/test/id3v2/attachmentsFrameTests.ts index 09268ed6..bfcec4e3 100644 --- a/test/id3v2/attachmentsFrameTests.ts +++ b/test/id3v2/attachmentsFrameTests.ts @@ -11,11 +11,24 @@ import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; import {FrameIdentifier, FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; import {IPicture, PictureType} from "../../src/picture"; import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; +import FramePropertyTests from "./framePropertyTests"; // Setup chai Chai.use(ChaiAsPromised); const assert = Chai.assert; +function getTestFrame() { + const data = ByteVector.fromString("foobarbaz"); + const mockPicture = TypeMoq.Mock.ofType(); + mockPicture.setup((p) => p.data).returns(() => data); + mockPicture.setup((p) => p.description).returns(() => "fux"); + mockPicture.setup((p) => p.filename).returns(() => "bux"); + mockPicture.setup((p) => p.mimeType).returns(() => "application/octet-stream"); + mockPicture.setup((p) => p.type).returns(() => PictureType.FrontCover); + + return AttachmentFrame.fromPicture(mockPicture.object); +} + @suite(timeout(3000), slow(1000)) class Id3v2_AttachmentFrame_ConstructorTests extends FrameConstructorTests { get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { @@ -259,4 +272,308 @@ class Id3v2_AttachmentFrame_ConstructorTests extends FrameConstructorTests { } } +@suite(slow(1000), timeout(3000)) +class Id3v2_AttachmentFrame_PropertyTests { + @test + public data() { + // Arrange + const frame = getTestFrame(); + const get = () => frame.data; + const set = (v: ByteVector) => { frame.data = v; }; + + // Act / Assert + FramePropertyTests.propertyRoundTrip(set, get, ByteVector.fromString("xyz", StringType.Latin1)); + FramePropertyTests.propertyNormalized(set, get, undefined, ByteVector.empty()); + FramePropertyTests.propertyNormalized(set, get, null, ByteVector.empty()); + } + + @test + public description() { + // Arrange + const frame = getTestFrame(); + const get = () => frame.description; + const set = (v: string) => { frame.description = v; }; + + // Act / Assert + FramePropertyTests.propertyRoundTrip(set, get, "its funky enough"); + FramePropertyTests.propertyNormalized(set, get, undefined, ""); + FramePropertyTests.propertyNormalized(set, get, null, ""); + } + + @test + public filename() { + // Arrange + const frame = getTestFrame(); + const get = () => frame.filename; + const set = (v: string) => { frame.filename = v; }; + + // Act / Assert + FramePropertyTests.propertyRoundTrip(set, get, "the choice is yours (revisited)"); + FramePropertyTests.propertyRoundTrip(set, get, undefined); + FramePropertyTests.propertyRoundTrip(set, get, null); + } + + @test + public mimeType() { + // Arrange + const frame = getTestFrame(); + const get = () => frame.mimeType; + const set = (v: string) => { frame.mimeType = v; }; + + // Act / Assert + FramePropertyTests.propertyRoundTrip(set, get, "chief rocka"); + FramePropertyTests.propertyNormalized(set, get, undefined, ""); + FramePropertyTests.propertyNormalized(set, get, null, ""); + } + + @test + public textEncoding() { + // Arrange + const frame = getTestFrame(); + + // Act / Assert + FramePropertyTests.propertyRoundTrip( + (v) => { frame.textEncoding = v; }, + () => frame.textEncoding, + StringType.UTF8 + ); + } + + @test + public type_setToAPicture_frameIdDoesNotChange() { + // Arrange + const frame = getTestFrame(); + + // Act / Assert + FramePropertyTests.propertyRoundTrip( + (v) => { frame.type = v; }, + () => frame.type, + PictureType.BackCover + ); + assert.strictEqual(frame.frameId, FrameIdentifiers.APIC); + } + @test + public type_setToNotAPicture_frameIdChanges() { + // Arrange + const frame = getTestFrame(); + + // Act / Assert + FramePropertyTests.propertyRoundTrip( + (v) => { frame.type = v; }, + () => frame.type, + PictureType.NotAPicture + ); + assert.strictEqual(frame.frameId, FrameIdentifiers.GEOB); + } + + @test + public type_setToNotAPictureAndBack_frameIdChanges() { + // Arrange + const frame = getTestFrame(); + const set = (v: PictureType) => { frame.type = v; }; + const get = () => frame.type; + + // Act + FramePropertyTests.propertyRoundTrip(set, get, PictureType.NotAPicture); + assert.strictEqual(frame.frameId, FrameIdentifiers.GEOB); + FramePropertyTests.propertyRoundTrip(set, get, PictureType.BackCover); + assert.strictEqual(frame.frameId, FrameIdentifiers.APIC); + } +} + +@suite(slow(1000), timeout(3000)) +class Id3v2_AttachmentFrame_MethodTests { + @test + public clone_fromPictureUnread() { + // Arrange + const data = ByteVector.fromString("foobarbaz"); + const mockPicture = TypeMoq.Mock.ofType(); + mockPicture.setup((p) => p.data).returns(() => data); + mockPicture.setup((p) => p.description).returns(() => "fux"); + mockPicture.setup((p) => p.filename).returns(() => "bux"); + mockPicture.setup((p) => p.mimeType).returns(() => "application/octet-stream"); + mockPicture.setup((p) => p.type).returns(() => PictureType.FrontCover); + + const frame = AttachmentFrame.fromPicture(mockPicture.object); + + // Act + const output = frame.clone(); + + // Assert + assert.isOk(output); + assert.equal(output.frameClassType, FrameClassType.AttachmentFrame); + + assert.isTrue(ByteVector.equal(output.data, frame.data)); + assert.strictEqual(output.description, frame.description); + assert.strictEqual(output.filename, frame.filename); + assert.strictEqual(output.mimeType, frame.mimeType); + assert.strictEqual(output.textEncoding, frame.textEncoding); + assert.strictEqual(output.type, frame.type); + assert.strictEqual(frame.frameId, frame.frameId); + } + + @test + public clone_fromRawDataUnread() { + // Arrange + const testData = ByteVector.fromString("fuxbuxqux", StringType.Latin1); + const header = new Id3v2FrameHeader(FrameIdentifiers.APIC); + header.frameSize = 41; + const data = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("image/gif", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + PictureType.Artist, + ByteVector.fromString("foobarbaz", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + testData + ); + const frame = AttachmentFrame.fromRawData(data, 4); + + // Act + const output = frame.clone(); + + // Assert + assert.isOk(output); + assert.equal(output.frameClassType, FrameClassType.AttachmentFrame); + + assert.isTrue(ByteVector.equal(output.data, frame.data)); + assert.strictEqual(output.description, frame.description); + assert.strictEqual(output.filename, frame.filename); + assert.strictEqual(output.mimeType, frame.mimeType); + assert.strictEqual(output.textEncoding, frame.textEncoding); + assert.strictEqual(output.type, frame.type); + assert.strictEqual(frame.frameId, frame.frameId); + } + + @test + public clone_alreadyRead() { + // Arrange + const frame = getTestFrame(); + const _ = frame.data; // force a raw load + + // Act + const output = frame.clone(); + + // Assert + assert.isOk(output); + assert.equal(output.frameClassType, FrameClassType.AttachmentFrame); + + assert.isTrue(ByteVector.equal(output.data, frame.data)); + assert.strictEqual(output.description, frame.description); + assert.strictEqual(output.filename, frame.filename); + assert.strictEqual(output.mimeType, frame.mimeType); + assert.strictEqual(output.textEncoding, frame.textEncoding); + assert.strictEqual(output.type, frame.type); + assert.strictEqual(frame.frameId, frame.frameId); + } + + @test + public find_falsyFrames() { + // Act / Assert + assert.throws(() => { AttachmentFrame.find(undefined); }); + assert.throws(() => { AttachmentFrame.find(null); }); + } + + @test + public find_noFrames() { + // Arrange + const frames: AttachmentFrame[] = []; + + // Act + const output = AttachmentFrame.find(frames); + + // Assert + assert.isUndefined(output); + } + + @test + public find_noMatchByDescription() { + // Arrange + const frame1 = getTestFrame(); + frame1.description = "fux"; + frame1.type = PictureType.Artist; + const frame2 = getTestFrame(); + frame2.description = "bux"; + frame2.type = PictureType.Artist; + + const frames = [frame1, frame2]; + + // Act + const output = AttachmentFrame.find(frames, "qux", PictureType.Artist); + + // Assert + assert.isUndefined(output); + } + + @test + public find_noMatchByType() { + const frame1 = getTestFrame(); + frame1.description = "qux"; + frame1.type = PictureType.FrontCover; + const frame2 = getTestFrame(); + frame2.description = "qux"; + frame2.type = PictureType.BackCover; + + const frames = [frame1, frame2]; + + // Act + const output = AttachmentFrame.find(frames, "qux", PictureType.Artist); + + // Assert + assert.isUndefined(output); + } + + @test + public find_matchWithoutDescription() { + const frame1 = getTestFrame(); + frame1.type = PictureType.FrontCover; + const frame2 = getTestFrame(); + frame2.type = PictureType.BackCover; + + const frames = [frame1, frame2]; + + // Act + const output = AttachmentFrame.find(frames, undefined, PictureType.BackCover); + + // Assert + assert.strictEqual(output, frame2); + } + + @test + public find_matchWithoutType() { + const frame1 = getTestFrame(); + frame1.description = "bux"; + frame1.type = PictureType.FrontCover; + const frame2 = getTestFrame(); + frame2.description = "qux"; + frame2.type = PictureType.BackCover; + + const frames = [frame1, frame2]; + + // Act + const output = AttachmentFrame.find(frames, "qux"); + + // Assert + assert.strictEqual(output, frame2); + } + + @test + public find_matchWithoutEither() { + const frame1 = getTestFrame(); + frame1.description = "bux"; + frame1.type = PictureType.FrontCover; + const frame2 = getTestFrame(); + frame2.description = "qux"; + frame2.type = PictureType.BackCover; + + const frames = [frame1, frame2]; + + // Act + const output = AttachmentFrame.find(frames); + + // Assert + assert.strictEqual(output, frame1); + } +} From a40672b22e51c63a0dd5e7dec3adc6a31493c82d Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Fri, 14 Feb 2020 22:09:45 -0500 Subject: [PATCH 45/71] Start of tests for frame factory --- src/id3v2/frames/frameFactory.ts | 1 + test/id3v2/frameFactoryTests.ts | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 test/id3v2/frameFactoryTests.ts diff --git a/src/id3v2/frames/frameFactory.ts b/src/id3v2/frames/frameFactory.ts index 2369a4ce..b5fe43bc 100644 --- a/src/id3v2/frames/frameFactory.ts +++ b/src/id3v2/frames/frameFactory.ts @@ -66,6 +66,7 @@ export default { */ createFrame: (data: ByteVector, file: File, offset: number, version: number, alreadyUnsynced: boolean): {frame: Frame, offset: number} => { + Guards.uint(offset, "offset"); Guards.byte(version, "version"); let position = 0; diff --git a/test/id3v2/frameFactoryTests.ts b/test/id3v2/frameFactoryTests.ts new file mode 100644 index 00000000..28cd45c0 --- /dev/null +++ b/test/id3v2/frameFactoryTests.ts @@ -0,0 +1,25 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import * as TypeMoq from "typemoq"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import FrameFactory from "../../src/id3v2/frames/frameFactory"; +import {ByteVector} from "../../src/byteVector"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(slow(1000), timeout(3000)) +class FrameFactoryTests { + @test + public createFrame_invalidVersion() { + // Arrange + const data = ByteVector.empty(); + + // Act / Assert + assert.throws(() => { FrameFactory.createFrame(data, undefined, 0, -1, false); }); + assert.throws(() => { FrameFactory.createFrame(data, undefined, 0, 1.23, false); }); + assert.throws(() => { FrameFactory.createFrame(data, undefined, 0, 0x100, false); }); + } +} From 666228f19be6cd81f15c9c575d510e9c5f734db9 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 18 Feb 2020 20:59:44 -0500 Subject: [PATCH 46/71] Some more tests! --- src/id3v2/frames/commentsFrame.ts | 2 +- src/id3v2/frames/frameFactory.ts | 11 +- src/id3v2/frames/popularimeterFrame.ts | 4 +- src/id3v2/frames/termsOfUseFrame.ts | 2 +- src/id3v2/frames/unsynchronizedLyricsFrame.ts | 4 +- src/id3v2/frames/urlLinkFrame.ts | 1 + test/id3v2/frameFactoryTests.ts | 431 +++++++++++++++++- test/utilities/testFile.ts | 21 + 8 files changed, 465 insertions(+), 11 deletions(-) create mode 100644 test/utilities/testFile.ts diff --git a/src/id3v2/frames/commentsFrame.ts b/src/id3v2/frames/commentsFrame.ts index c15edae6..35dbf152 100644 --- a/src/id3v2/frames/commentsFrame.ts +++ b/src/id3v2/frames/commentsFrame.ts @@ -280,7 +280,7 @@ export default class CommentsFrame extends Frame { v.addByteVector(ByteVector.fromString(this.language, StringType.Latin1)); v.addByteVector(ByteVector.fromString(this.description, encoding)); v.addByteVector(ByteVector.getTextDelimiter(encoding)); - v.addByteVector(ByteVector.fromString(this._text, encoding)); + v.addByteVector(ByteVector.fromString(this.text, encoding)); return v; } } diff --git a/src/id3v2/frames/frameFactory.ts b/src/id3v2/frames/frameFactory.ts index b5fe43bc..87fa400f 100644 --- a/src/id3v2/frames/frameFactory.ts +++ b/src/id3v2/frames/frameFactory.ts @@ -72,6 +72,10 @@ export default { let position = 0; const frameHeaderSize = Id3v2FrameHeader.getSize(version); + if (!data && !file) { + throw new Error("Argument exception: data or file must be provided"); + } + if (!data) { file.seek(offset); data = file.readBlock(frameHeaderSize); @@ -103,7 +107,7 @@ export default { } // TODO: Support encryption - if ((header.flags & Id3v2FrameFlags.Encryption) > 0) { + if ((header.flags & Id3v2FrameFlags.Encryption) !== 0) { throw new NotImplementedError("Encryption is not supported"); } @@ -139,7 +143,7 @@ export default { data.addByteVector(file.readBlock(offset - filePosition)); } - let func: any = UnknownFrame.fromOffsetRawData; + let func; if (header.frameId === FrameIdentifiers.TXXX) { // User text identification frame func = UserTextInformationFrame.fromOffsetRawData; @@ -188,6 +192,9 @@ export default { } else if (header.frameId === FrameIdentifiers.ETCO) { // Event timing codes (frames 4.6) func = EventTimeCodeFrame.fromOffsetRawData; + } else { + // Return unknown + func = UnknownFrame.fromOffsetRawData; } return { diff --git a/src/id3v2/frames/popularimeterFrame.ts b/src/id3v2/frames/popularimeterFrame.ts index 2a3caf88..06f8d2c3 100644 --- a/src/id3v2/frames/popularimeterFrame.ts +++ b/src/id3v2/frames/popularimeterFrame.ts @@ -67,7 +67,7 @@ export default class PopularimeterFrame extends Frame { */ public static fromUser(user: string): PopularimeterFrame { const frame = new PopularimeterFrame(new Id3v2FrameHeader(FrameIdentifiers.POPM)); - frame.user = user; + frame._user = user; return frame; } @@ -96,7 +96,7 @@ export default class PopularimeterFrame extends Frame { /** * Gets the rating of the current instance */ - public get rating(): number { return this._rating; } + public get rating(): number { return this._rating || 0; } /** * Sets the rating of the current instance * @param value Rating of the current instance, must be a 8-bit unsigned integer. diff --git a/src/id3v2/frames/termsOfUseFrame.ts b/src/id3v2/frames/termsOfUseFrame.ts index 1f5fd003..34e33a5d 100644 --- a/src/id3v2/frames/termsOfUseFrame.ts +++ b/src/id3v2/frames/termsOfUseFrame.ts @@ -97,7 +97,7 @@ export default class TermsOfUseFrame extends Frame { /** * Gets the text of the terms of use */ - public get text(): string { return this._text; } + public get text(): string { return this._text || ""; } /** * Sets the text of the terms of use */ diff --git a/src/id3v2/frames/unsynchronizedLyricsFrame.ts b/src/id3v2/frames/unsynchronizedLyricsFrame.ts index 326dbc2c..f8dca684 100644 --- a/src/id3v2/frames/unsynchronizedLyricsFrame.ts +++ b/src/id3v2/frames/unsynchronizedLyricsFrame.ts @@ -246,9 +246,9 @@ export default class UnsynchronizedLyricsFrame extends Frame { return ByteVector.concatenate( encoding, ByteVector.fromString(this.language, StringType.Latin1), - ByteVector.fromString(this._description, encoding), + ByteVector.fromString(this.description, encoding), ByteVector.getTextDelimiter(encoding), - ByteVector.fromString(this._text, encoding) + ByteVector.fromString(this.text, encoding) ); } diff --git a/src/id3v2/frames/urlLinkFrame.ts b/src/id3v2/frames/urlLinkFrame.ts index cec8d009..24e14327 100644 --- a/src/id3v2/frames/urlLinkFrame.ts +++ b/src/id3v2/frames/urlLinkFrame.ts @@ -240,6 +240,7 @@ export class UrlLinkFrame extends Frame { } } } + // @TODO: is this correct formatting? v.addByteVector(ByteVector.fromString(text.join("/"), StringType.Latin1)); return v; diff --git a/test/id3v2/frameFactoryTests.ts b/test/id3v2/frameFactoryTests.ts index 28cd45c0..1edd0afb 100644 --- a/test/id3v2/frameFactoryTests.ts +++ b/test/id3v2/frameFactoryTests.ts @@ -1,10 +1,27 @@ import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; -import * as TypeMoq from "typemoq"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameFactory from "../../src/id3v2/frames/frameFactory"; -import {ByteVector} from "../../src/byteVector"; +import {ByteVector, StringType} from "../../src/byteVector"; +import TestFile from "../utilities/testFile"; +import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; +import {Id3v2FrameFlags, Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; +import {TextInformationFrame, UserTextInformationFrame} from "../../src/id3v2/frames/textInformationFrame"; +import UniqueFileIdentifierFrame from "../../src/id3v2/frames/uniqueFileIdentifierFrame"; +import UnsynchronizedLyricsFrame from "../../src/id3v2/frames/unsynchronizedLyricsFrame"; +import {SynchronizedLyricsFrame} from "../../src/id3v2/frames/synchronizedLyricsFrame"; +import {SynchronizedTextType, TimestampFormat} from "../../src/id3v2/utilTypes"; +import CommentsFrame from "../../src/id3v2/frames/commentsFrame"; +import {RelativeVolumeFrame} from "../../src/id3v2/frames/relativeVolumeFrame"; +import {PictureType} from "../../src/picture"; +import PlayCountFrame from "../../src/id3v2/frames/playCountFrame"; +import PopularimeterFrame from "../../src/id3v2/frames/popularimeterFrame"; +import TermsOfUseFrame from "../../src/id3v2/frames/termsOfUseFrame"; +import PrivateFrame from "../../src/id3v2/frames/privateFrame"; +import {UrlLinkFrame, UserUrlLinkFrame} from "../../src/id3v2/frames/urlLinkFrame"; +import {EventTimeCodeFrame} from "../../src/id3v2/frames/eventTimeCodeFrame"; // Setup chai Chai.use(ChaiAsPromised); @@ -13,7 +30,7 @@ const assert = Chai.assert; @suite(slow(1000), timeout(3000)) class FrameFactoryTests { @test - public createFrame_invalidVersion() { + public createFrame_invalidVersion() { // Arrange const data = ByteVector.empty(); @@ -22,4 +39,412 @@ class FrameFactoryTests { assert.throws(() => { FrameFactory.createFrame(data, undefined, 0, 1.23, false); }); assert.throws(() => { FrameFactory.createFrame(data, undefined, 0, 0x100, false); }); } + + @test + public createFrame_invalidOffset() { + // Arrange + const data = ByteVector.empty(); + + // Act / Assert + assert.throws(() => { FrameFactory.createFrame(data, undefined, -1, 4, false); }); + assert.throws(() => { FrameFactory.createFrame(data, undefined, 1.23, 4, false); }); + assert.throws(() => { FrameFactory.createFrame(data, undefined, Number.MAX_SAFE_INTEGER + 1, 4, false); }); + } + + @test + public createFrame_missingSource() { + // Act / Assert + assert.throws(() => { FrameFactory.createFrame(undefined, undefined, 0, 4, false); }); + assert.throws(() => { FrameFactory.createFrame(null, null, 0, 4, false); }); + } + + @test + public createFrame_allZero() { + // Arrange + const data = ByteVector.fromSize(10, 0x00); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + assert.isUndefined(output); + } + + @test + public createFrame_compressed() { + // Arrange + const data = ByteVector.concatenate( + FrameIdentifiers.TXXX.render(4), + 0x00, 0x00, 0x00, 0x0A, + (Id3v2FrameFlags.Compression & 0xFF >> 16), (Id3v2FrameFlags.Compression & 0xFF) + ); + + // Act / Assert + assert.throws(() => { FrameFactory.createFrame(data, undefined, 0, 4, false); }); + } + + @test + public createFrame_encrypted() { + // Arrange + const data = ByteVector.concatenate( + FrameIdentifiers.TXXX.render(4), + 0x00, 0x00, 0x00, 0x0A, + (Id3v2FrameFlags.Encryption & 0xFF >> 16), (Id3v2FrameFlags.Encryption & 0xFF) + ); + + // Act / Assert + assert.throws(() => { FrameFactory.createFrame(data, undefined, 0, 4, false); }); + } + + @test + public createFrame_fromData_txxx() { + // Arrange + const data = UserTextInformationFrame.fromDescription("foo").render(4); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.UserTextInformationFrame, data.length); + } + + @test + public createFrame_fromData_textFrame() { + // Arrange + const data = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM).render(4); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.TextInformationFrame, data.length); + } + + @test + public createFrame_fromData_ufid() { + // Arrange + const data = UniqueFileIdentifierFrame.fromData("foo", ByteVector.empty()).render(4); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.UniqueFileIdentifierFrame, data.length); + } + + @test + public createFrame_fromData_mcdi() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.MCDI); + header.frameSize = 9; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("12345abcd", StringType.Latin1) + ); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.MusicCdIdentiferFrame, data.length); + } + + @test + public createFrame_fromData_uslt() { + // Arrange + const data = UnsynchronizedLyricsFrame.fromData("foo").render(4); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.UnsynchronizedLyricsFrame, data.length); + } + + @test + public createFrame_fromData_sylt() { + // Arrange + const data = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Lyrics).render(4); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.SynchronizedLyricsFrame, data.length); + } + + @test + public createFrame_fromData_comm() { + // Arrange + const data = CommentsFrame.fromDescription("foo").render(4); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.CommentsFrame, data.length); + } + + @test + public createFrame_fromData_rva2() { + // Arrange + const data = RelativeVolumeFrame.fromIdentification("foo").render(4); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.RelativeVolumeFrame, data.length); + } + + @test + public createFrame_fromData_apic() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.APIC); + header.frameSize = 41; + const data = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("image/gif", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + PictureType.Artist, + ByteVector.fromString("foobarbaz", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("fuxbuxqux", StringType.Latin1) + ); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.AttachmentFrame, data.length); + } + + @test + public createFrame_fromData_geob() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.GEOB); + header.frameSize = 60; + const data = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("image/gif", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + ByteVector.fromString("image.gif", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("foobarbaz", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("fuxbuxqux", StringType.Latin1) + ); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.AttachmentFrame, data.length); + } + + @test + public createFrame_fromData_pcnt() { + // Arrange + const data = PlayCountFrame.fromEmpty().render(4); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.PlayCountFrame, data.length); + } + + @test + public createFrame_fromData_popm() { + // Arrange + const data = PopularimeterFrame.fromUser("foo").render(4); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.PopularimeterFrame, data.length); + } + + @test + public createFrame_fromData_user() { + // Arrange + const data = TermsOfUseFrame.fromFields("foo").render(4); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.TermsOfUseFrame, data.length); + } + + @test + public createFrame_fromData_priv() { + // Arrange + const data = PrivateFrame.fromOwner("foo").render(4); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.PrivateFrame, data.length); + } + + @test + public createFrame_fromData_wxxx() { + // Arrange + const data = UserUrlLinkFrame.fromDescription("foo").render(4); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.UserUrlLinkFrame, data.length); + } + + @test + public createFrame_fromData_urlFrame() { + // Arrange + const frame = UrlLinkFrame.fromIdentity(FrameIdentifiers.WCOM); + frame.text = ["foo"]; + const data = frame.render(4); + + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.UrlLinkFrame, data.length); + } + + @test + public createFrame_fromData_etco() { + // Arrange + const data = EventTimeCodeFrame.fromTimestampFormat(TimestampFormat.AbsoluteMilliseconds).render(4); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.EventTimeCodeFrame, data.length); + } + + @test + public createFrame_fromData_unknown() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.RVRB); + header.frameSize = 3; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromString("foo") + ); + + // Act + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.UnknownFrame, data.length); + } + + @test + public createFrame_fromOffsetData() { + // Arrange + const data = ByteVector.concatenate( + 0x00, 0x00, + UniqueFileIdentifierFrame.fromData("foo", ByteVector.empty()).render(4) + ); + + // Act + const output = FrameFactory.createFrame(data, undefined, 2, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.UniqueFileIdentifierFrame, data.length); + } + + @test + public createFrame_fromFileNoOffset_ufid() { + // Arrange + const data = UniqueFileIdentifierFrame.fromData("foo", ByteVector.empty()).render(4); + const file = TestFile.getFile(data); + + // Act + const output = FrameFactory.createFrame(undefined, file, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.UniqueFileIdentifierFrame, data.length); + } + + @test + public createFrame_fromFileWithOffset_ufid() { + // Arrange + const data = ByteVector.concatenate( + 0x00, 0x00, + UniqueFileIdentifierFrame.fromData("foo", ByteVector.empty()).render(4) + ); + const file = TestFile.getFile(data); + + // Act + const output = FrameFactory.createFrame(undefined, file, 2, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.UniqueFileIdentifierFrame, data.length); + } + + @test + public createFrame_fromFile_apic() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.APIC); + header.frameSize = 41; + const data = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("image/gif", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + PictureType.Artist, + ByteVector.fromString("foobarbaz", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("fuxbuxqux", StringType.Latin1) + ); + const file = TestFile.getFile(data); + + // Act + const output = FrameFactory.createFrame(undefined, file, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.AttachmentFrame, data.length); + } + + @test + public createFrame_fromeFile_geob() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.GEOB); + header.frameSize = 60; + const data = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("image/gif", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + ByteVector.fromString("image.gif", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("foobarbaz", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("fuxbuxqux", StringType.Latin1) + ); + const file = TestFile.getFile(data); + + // Act + const output = FrameFactory.createFrame(undefined, file, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.AttachmentFrame, data.length); + + } + + private validateOutput(output: {frame: Frame, offset: number}, fct: FrameClassType, o: number) { + assert.ok(output); + assert.strictEqual(output.frame.frameClassType, fct); + assert.strictEqual(output.offset, o); + } } diff --git a/test/utilities/testFile.ts b/test/utilities/testFile.ts new file mode 100644 index 00000000..be214915 --- /dev/null +++ b/test/utilities/testFile.ts @@ -0,0 +1,21 @@ +import {ByteVector} from "../../src/byteVector"; +import * as TypeMoq from "typemoq"; +import {SeekOrigin} from "../../src/stream"; +import {File} from "../../src/file"; + +export default { + getFile: (data: ByteVector): File => { + const mockFile = TypeMoq.Mock.ofType(); + let position = 0; + mockFile.setup((f) => f.seek(TypeMoq.It.isAnyNumber(), TypeMoq.It.isAny())) + .returns((p) => { + position = p; + }); + mockFile.setup((f) => f.readBlock(TypeMoq.It.isAnyNumber())) + .returns((s) => { + return data.mid(position, s); + }); + + return mockFile.object; + } +}; From 3e1504017b9dd54488f6a7861a29e9269a093d77 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Thu, 20 Feb 2020 22:21:30 -0500 Subject: [PATCH 47/71] 98% coverage of frame creation --- src/id3v2/frames/frameFactory.ts | 12 ++++- test/id3v2/frameFactoryTests.ts | 83 +++++++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/id3v2/frames/frameFactory.ts b/src/id3v2/frames/frameFactory.ts index 87fa400f..249db91a 100644 --- a/src/id3v2/frames/frameFactory.ts +++ b/src/id3v2/frames/frameFactory.ts @@ -24,7 +24,7 @@ import {Guards} from "../../utils"; export type FrameCreator = (data: ByteVector, offset: number, header: Id3v2FrameHeader, version: number) => Frame; -const customFrameCreators: FrameCreator[] = []; +let customFrameCreators: FrameCreator[] = []; /** * Performs the necessary operations to determine and create the correct child classes of @@ -44,12 +44,19 @@ export default { * * version: number ID3v2 version the raw frame data is stored in (should be byte) * * returns Frame if method was able to match the frame, falsy otherwise */ - addFrameCreator: (creator: (data: ByteVector, offset: number, header: Id3v2FrameHeader, vrsion: number) => Frame): + addFrameCreator: (creator: FrameCreator): void => { Guards.truthy(creator, "creator"); customFrameCreators.unshift(creator); }, + /** + * Removes all custom frame creators + */ + clearFrameCreators: (): void => { + customFrameCreators = []; + }, + /** * Creates a {@see Frame} object by reading it from raw ID3v2 frame data. * @param data Raw ID3v2 frame @@ -113,6 +120,7 @@ export default { // Try to find a custom creator for (const creator of customFrameCreators) { + // @TODO: If we're reading from a file, data will only ever contain the header const frame = creator(data, position, header, version); if (frame) { return { diff --git a/test/id3v2/frameFactoryTests.ts b/test/id3v2/frameFactoryTests.ts index 1edd0afb..d206c0de 100644 --- a/test/id3v2/frameFactoryTests.ts +++ b/test/id3v2/frameFactoryTests.ts @@ -1,8 +1,9 @@ import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; +import * as TypeMoq from "typemoq"; import {slow, suite, test, timeout} from "mocha-typescript"; -import FrameFactory from "../../src/id3v2/frames/frameFactory"; +import FrameFactory, {FrameCreator} from "../../src/id3v2/frames/frameFactory"; import {ByteVector, StringType} from "../../src/byteVector"; import TestFile from "../utilities/testFile"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; @@ -442,6 +443,86 @@ class FrameFactoryTests { } + @test + public createFrame_fromeFile_nonLazy() { + // Arrange + const data = PlayCountFrame.fromEmpty().render(4); + const file = TestFile.getFile(data); + + // Act + const output = FrameFactory.createFrame(undefined, file, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.PlayCountFrame, data.length); + } + + @test + public createFrame_fromData_customFrameCreatorMatch() { + try { + // Arrange + const frame = PlayCountFrame.fromEmpty(); + const data = frame.render(4); + const mockCreator = TypeMoq.Mock.ofType(); + mockCreator.setup( + (c) => c(TypeMoq.It.isAny(), TypeMoq.It.isValue(0), TypeMoq.It.isAny(), TypeMoq.It.isValue(4)) + ).returns(() => frame); + + // Act + FrameFactory.addFrameCreator(mockCreator.object); + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + assert.isOk(output); + assert.strictEqual(output.frame, frame); + assert.strictEqual(output.offset, data.length); + + mockCreator.verify( + (c) => c( + TypeMoq.It.is((d) => ByteVector.equal(d, data)), + TypeMoq.It.isValue(0), + TypeMoq.It.isAny(), + TypeMoq.It.isValue(4) + ), + TypeMoq.Times.once() + ); + } finally { + FrameFactory.clearFrameCreators(); + } + } + + @test + public createFrame_fromData_customFrameCreatorNoMatch() { + try { + // Arrange + const frame = PlayCountFrame.fromEmpty(); + const data = frame.render(4); + const mockCreator = TypeMoq.Mock.ofType(); + mockCreator.setup( + (c) => c(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ).returns(() => undefined); + + // Act + FrameFactory.addFrameCreator(mockCreator.object); + const output = FrameFactory.createFrame(data, undefined, 0, 4, false); + + // Assert + this.validateOutput(output, FrameClassType.PlayCountFrame, data.length); + assert.notStrictEqual(output.frame, frame); + + mockCreator.verify( + (c) => c( + TypeMoq.It.is((d) => ByteVector.equal(d, data)), + TypeMoq.It.isValue(0), + TypeMoq.It.isAny(), + TypeMoq.It.isValue(4) + ), + TypeMoq.Times.once() + ); + } finally { + FrameFactory.clearFrameCreators(); + } + } + private validateOutput(output: {frame: Frame, offset: number}, fct: FrameClassType, o: number) { assert.ok(output); assert.strictEqual(output.frame.frameClassType, fct); From dfe923dd2d3f55ccf34ef7a7982c6dc66d1b99a6 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 22 Feb 2020 13:30:40 -0500 Subject: [PATCH 48/71] Patching up a few broken tests --- test/id3v2/popularimeterFrameTests.ts | 2 +- test/id3v2/termsOfUseFrameTests.ts | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/test/id3v2/popularimeterFrameTests.ts b/test/id3v2/popularimeterFrameTests.ts index 801bae10..a7cdf5bb 100644 --- a/test/id3v2/popularimeterFrameTests.ts +++ b/test/id3v2/popularimeterFrameTests.ts @@ -31,7 +31,7 @@ class Id3v2_PopularimeterFrame_ConstructorTests extends FrameConstructorTests { const frame = PopularimeterFrame.fromUser("fux"); // Assert - this.assertFrame(frame, "fux", undefined, undefined); + this.assertFrame(frame, "fux", undefined, 0); } @test diff --git a/test/id3v2/termsOfUseFrameTests.ts b/test/id3v2/termsOfUseFrameTests.ts index 27af4732..e0405228 100644 --- a/test/id3v2/termsOfUseFrameTests.ts +++ b/test/id3v2/termsOfUseFrameTests.ts @@ -31,7 +31,7 @@ class Id3v2_TermsOfUseFrame_ConstructorTests extends FrameConstructorTests { const output = TermsOfUseFrame.fromFields("fux"); // Assert - this.assertFrame(output, "fux", undefined, Id3v2TagSettings.defaultEncoding); + this.assertFrame(output, "fux", "", Id3v2TagSettings.defaultEncoding); } @test @@ -40,7 +40,7 @@ class Id3v2_TermsOfUseFrame_ConstructorTests extends FrameConstructorTests { const output = TermsOfUseFrame.fromFields("fux", StringType.UTF16BE); // Assert - this.assertFrame(output, "fux", undefined, StringType.UTF16BE); + this.assertFrame(output, "fux", "", StringType.UTF16BE); } @test @@ -141,9 +141,14 @@ class Id3v2_TermsOfUseFrame_PropertyTests { @test public text() { + // Arrange + const set = (v: string) => { frame.text = v; }; + const get = () => frame.text; + const frame = TermsOfUseFrame.fromFields("eng"); - FramePropertyTests.propertyRoundTrip((v) => { frame.text = v; }, () => frame.text, "fux"); - FramePropertyTests.propertyRoundTrip((v) => { frame.text = v; }, () => frame.text, undefined); + FramePropertyTests.propertyRoundTrip(set, get, "fux"); + FramePropertyTests.propertyNormalized(set, get, undefined, ""); + FramePropertyTests.propertyNormalized(set, get, null, ""); } @test From 28c0503a6622081237c9a97830a59f1113e51199 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 22 Feb 2020 14:39:29 -0500 Subject: [PATCH 49/71] Renaming a few files, porting tag constructors --- src/id3v2/frames/attachmentFrame.ts | 8 +- src/id3v2/frames/commentsFrame.ts | 6 +- src/id3v2/frames/frame.ts | 8 +- src/id3v2/frames/synchronizedLyricsFrame.ts | 6 +- src/id3v2/frames/termsOfUseFrame.ts | 8 +- src/id3v2/frames/textInformationFrame.ts | 8 +- src/id3v2/frames/unsynchronizedLyricsFrame.ts | 6 +- src/id3v2/headerFlags.ts | 26 ----- ...tendedHeader.ts => id3v2ExtendedHeader.ts} | 10 +- .../{id3v2TagSettings.ts => id3v2Settings.ts} | 10 +- src/id3v2/id3v2Tag.ts | 108 +++++++++++++----- src/id3v2/{footer.ts => id3v2TagFooter.ts} | 26 ++--- src/id3v2/{header.ts => id3v2TagHeader.ts} | 71 ++++++++---- test/id3v2/attachmentsFrameTests.ts | 6 +- test/id3v2/commentsFrameTests.ts | 6 +- test/id3v2/id3v2TagTests.ts | 0 test/id3v2/synchronizedLyricsFrameTests.ts | 4 +- test/id3v2/tagExtendedHeaderTests.ts | 20 ++-- test/id3v2/tagFooterTests.ts | 68 +++++------ test/id3v2/tagHeaderTests.ts | 105 ++++++++--------- test/id3v2/termsOfUseFrameTests.ts | 4 +- test/id3v2/textInformationFrameTests.ts | 8 +- test/id3v2/userTextInformationFrameTests.ts | 4 +- 23 files changed, 293 insertions(+), 233 deletions(-) delete mode 100644 src/id3v2/headerFlags.ts rename src/id3v2/{extendedHeader.ts => id3v2ExtendedHeader.ts} (76%) rename src/id3v2/{id3v2TagSettings.ts => id3v2Settings.ts} (93%) rename src/id3v2/{footer.ts => id3v2TagFooter.ts} (87%) rename src/id3v2/{header.ts => id3v2TagHeader.ts} (75%) create mode 100644 test/id3v2/id3v2TagTests.ts diff --git a/src/id3v2/frames/attachmentFrame.ts b/src/id3v2/frames/attachmentFrame.ts index 0647aa55..6534e8ad 100644 --- a/src/id3v2/frames/attachmentFrame.ts +++ b/src/id3v2/frames/attachmentFrame.ts @@ -1,4 +1,4 @@ -import Id3v2TagSettings from "../id3v2TagSettings"; +import Id3v2Settings from "../id3v2Settings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; import {Frame, FrameClassType} from "./frame"; @@ -10,7 +10,7 @@ import {Guards} from "../../utils"; export default class AttachmentFrame extends Frame { private _data: ByteVector; private _description: string; - private _encoding: StringType = Id3v2TagSettings.defaultEncoding; + private _encoding: StringType = Id3v2Settings.defaultEncoding; private _filename: string; private _mimeType: string; private _rawPicture: IPicture; @@ -163,7 +163,7 @@ export default class AttachmentFrame extends Frame { * Sets the text encoding to use when storing the current instance. * @param value Text encoding to use when storing the current instance. * This encoding is overridden when rendering if - * {@see Id3v2TagSettings.forceDefaultEncoding} is `true` or the render version does not + * {@see Id3v2Settings.forceDefaultEncoding} is `true` or the render version does not * support it. */ public set textEncoding(value: StringType) { @@ -421,7 +421,7 @@ export default class AttachmentFrame extends Frame { this._mimeType = picture.mimeType; this._type = picture.type; - this._encoding = Id3v2TagSettings.defaultEncoding; + this._encoding = Id3v2Settings.defaultEncoding; // Switch the frame ID if we discovered the attachment isn't an image if (this._type === PictureType.NotAPicture) { diff --git a/src/id3v2/frames/commentsFrame.ts b/src/id3v2/frames/commentsFrame.ts index 35dbf152..0b4e6d48 100644 --- a/src/id3v2/frames/commentsFrame.ts +++ b/src/id3v2/frames/commentsFrame.ts @@ -1,4 +1,4 @@ -import Id3v2TagSettings from "../id3v2TagSettings"; +import Id3v2Settings from "../id3v2Settings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; import {Frame, FrameClassType} from "./frame"; @@ -18,7 +18,7 @@ export default class CommentsFrame extends Frame { private _description: string; private _language: string; private _text: string; - private _textEncoding: StringType = Id3v2TagSettings.defaultEncoding; + private _textEncoding: StringType = Id3v2Settings.defaultEncoding; // #region @@ -35,7 +35,7 @@ export default class CommentsFrame extends Frame { public static fromDescription( description: string, language?: string, - encoding: StringType = Id3v2TagSettings.defaultEncoding + encoding: StringType = Id3v2Settings.defaultEncoding ): CommentsFrame { Guards.notNullOrUndefined(description, "description"); diff --git a/src/id3v2/frames/frame.ts b/src/id3v2/frames/frame.ts index 7ee88c52..04faea11 100644 --- a/src/id3v2/frames/frame.ts +++ b/src/id3v2/frames/frame.ts @@ -1,4 +1,4 @@ -import Id3v2TagSettings from "../id3v2TagSettings"; +import Id3v2Settings from "../id3v2Settings"; import SyncData from "../syncData"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; @@ -207,14 +207,14 @@ export abstract class Frame { * @param type Value containing the original encoding * @param version Value containing the ID3v2 version to be encoded. * @returns StringType Value containing the correct encoding to use, based on - * {@see Id3v2TagSettings.forceDefaultEncoding} and what is supported by + * {@see Id3v2Settings.forceDefaultEncoding} and what is supported by * {@paramref version} */ protected static correctEncoding(type: StringType, version: number): StringType { Guards.byte(version, "version"); - if (Id3v2TagSettings.forceDefaultEncoding) { - type = Id3v2TagSettings.defaultEncoding; + if (Id3v2Settings.forceDefaultEncoding) { + type = Id3v2Settings.defaultEncoding; } return version < 4 && type === StringType.UTF8 diff --git a/src/id3v2/frames/synchronizedLyricsFrame.ts b/src/id3v2/frames/synchronizedLyricsFrame.ts index 183bc2f4..17509712 100644 --- a/src/id3v2/frames/synchronizedLyricsFrame.ts +++ b/src/id3v2/frames/synchronizedLyricsFrame.ts @@ -1,4 +1,4 @@ -import Id3v2TagSettings from "../id3v2TagSettings"; +import Id3v2Settings from "../id3v2Settings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; import {Frame, FrameClassType} from "./frame"; @@ -59,7 +59,7 @@ export class SynchronizedLyricsFrame extends Frame { private _format: TimestampFormat = TimestampFormat.Unknown; private _language: string; private _text: SynchronizedText[] = []; - private _textEncoding: StringType = Id3v2TagSettings.defaultEncoding; + private _textEncoding: StringType = Id3v2Settings.defaultEncoding; private _textType: SynchronizedTextType = SynchronizedTextType.Other; // #region Constructors @@ -80,7 +80,7 @@ export class SynchronizedLyricsFrame extends Frame { description: string, language: string, textType: SynchronizedTextType, - encoding: StringType = Id3v2TagSettings.defaultEncoding + encoding: StringType = Id3v2Settings.defaultEncoding ): SynchronizedLyricsFrame { const frame = new SynchronizedLyricsFrame(new Id3v2FrameHeader(FrameIdentifiers.SYLT)); frame.textEncoding = encoding; diff --git a/src/id3v2/frames/termsOfUseFrame.ts b/src/id3v2/frames/termsOfUseFrame.ts index 34e33a5d..41582da4 100644 --- a/src/id3v2/frames/termsOfUseFrame.ts +++ b/src/id3v2/frames/termsOfUseFrame.ts @@ -1,4 +1,4 @@ -import Id3v2TagSettings from "../id3v2TagSettings"; +import Id3v2Settings from "../id3v2Settings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; import {Frame, FrameClassType} from "./frame"; @@ -9,7 +9,7 @@ import {Guards} from "../../utils"; export default class TermsOfUseFrame extends Frame { private _language: string; private _text: string; - private _textEncoding: StringType = Id3v2TagSettings.defaultEncoding; + private _textEncoding: StringType = Id3v2Settings.defaultEncoding; // #region Constructors @@ -21,11 +21,11 @@ export default class TermsOfUseFrame extends Frame { * Constructs and initializes a new instance with a specified language. * @param language ISO-639-2 language code for the new frame * @param textEncoding Optional, text encoding to use when rendering the new frame. If not - * provided defaults to {@see Id3v2TagSettings.defaultEncoding} + * provided defaults to {@see Id3v2Settings.defaultEncoding} */ public static fromFields( language: string, - textEncoding: StringType = Id3v2TagSettings.defaultEncoding + textEncoding: StringType = Id3v2Settings.defaultEncoding ): TermsOfUseFrame { const f = new TermsOfUseFrame(new Id3v2FrameHeader(FrameIdentifiers.USER)); f.textEncoding = textEncoding; diff --git a/src/id3v2/frames/textInformationFrame.ts b/src/id3v2/frames/textInformationFrame.ts index 9ca0bcb5..73459675 100644 --- a/src/id3v2/frames/textInformationFrame.ts +++ b/src/id3v2/frames/textInformationFrame.ts @@ -1,5 +1,5 @@ import Genres from "../../genres"; -import Id3v2TagSettings from "../id3v2TagSettings"; +import Id3v2Settings from "../id3v2Settings"; import {ByteVector, StringType} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; @@ -131,7 +131,7 @@ export class TextInformationFrame extends Frame { private static COVER_STRING = "Cover"; private static REMIX_STRING = "Remix"; - protected _encoding: StringType = Id3v2TagSettings.defaultEncoding; + protected _encoding: StringType = Id3v2Settings.defaultEncoding; protected _rawData: ByteVector; protected _rawVersion: number; protected _textFields: string[] = []; @@ -150,7 +150,7 @@ export class TextInformationFrame extends Frame { */ public static fromIdentifier( identifier: FrameIdentifier, - encoding: StringType = Id3v2TagSettings.defaultEncoding + encoding: StringType = Id3v2Settings.defaultEncoding ): TextInformationFrame { const frame = new TextInformationFrame(new Id3v2FrameHeader(identifier)); frame._encoding = encoding; @@ -540,7 +540,7 @@ export class UserTextInformationFrame extends TextInformationFrame { */ public static fromDescription( description: string, - encoding: StringType = Id3v2TagSettings.defaultEncoding + encoding: StringType = Id3v2Settings.defaultEncoding ): UserTextInformationFrame { const frame = new UserTextInformationFrame(new Id3v2FrameHeader(FrameIdentifiers.TXXX)); frame._encoding = encoding; diff --git a/src/id3v2/frames/unsynchronizedLyricsFrame.ts b/src/id3v2/frames/unsynchronizedLyricsFrame.ts index f8dca684..1506633c 100644 --- a/src/id3v2/frames/unsynchronizedLyricsFrame.ts +++ b/src/id3v2/frames/unsynchronizedLyricsFrame.ts @@ -1,4 +1,4 @@ -import Id3v2TagSettings from "../id3v2TagSettings"; +import Id3v2Settings from "../id3v2Settings"; import {ByteVector, StringType} from "../../byteVector"; import {CorruptFileError} from "../../errors"; import {Frame, FrameClassType} from "./frame"; @@ -10,7 +10,7 @@ export default class UnsynchronizedLyricsFrame extends Frame { private _description: string; private _language: string; private _text: string; - private _textEncoding: StringType = Id3v2TagSettings.defaultEncoding; + private _textEncoding: StringType = Id3v2Settings.defaultEncoding; // #region Constructors @@ -27,7 +27,7 @@ export default class UnsynchronizedLyricsFrame extends Frame { public static fromData( description: string, language?: string, - encoding: StringType = Id3v2TagSettings.defaultEncoding + encoding: StringType = Id3v2Settings.defaultEncoding ): UnsynchronizedLyricsFrame { const frame = new UnsynchronizedLyricsFrame(new Id3v2FrameHeader(FrameIdentifiers.USLT)); frame.textEncoding = encoding; diff --git a/src/id3v2/headerFlags.ts b/src/id3v2/headerFlags.ts deleted file mode 100644 index 7071db10..00000000 --- a/src/id3v2/headerFlags.ts +++ /dev/null @@ -1,26 +0,0 @@ -export enum HeaderFlags { - /** - * The header contains no flags. - */ - None = 0x0, - - /** - * The tag described by the header contains a footer. - */ - FooterPresent = 0x10, - - /** - * The tag described by the header is experimental. - */ - ExperimentalIndicator = 0x20, - - /** - * The tag described by the header contains an extended header. - */ - ExtendedHeader = 0x40, - - /** - * The tag described by the header has been desynchronized. - */ - Unsynchronication = 0x80, -} diff --git a/src/id3v2/extendedHeader.ts b/src/id3v2/id3v2ExtendedHeader.ts similarity index 76% rename from src/id3v2/extendedHeader.ts rename to src/id3v2/id3v2ExtendedHeader.ts index 80ec2215..4f3ec908 100644 --- a/src/id3v2/extendedHeader.ts +++ b/src/id3v2/id3v2ExtendedHeader.ts @@ -2,7 +2,7 @@ import SyncData from "./syncData"; import {ByteVector} from "../byteVector"; import {Guards} from "../utils"; -export default class ExtendedHeader { +export default class Id3v2ExtendedHeader { private _size: number; private constructor() {} @@ -12,11 +12,11 @@ export default class ExtendedHeader { * @param data Raw extended header structure * @param version ID3v2 version. Must be an unsigned 8-bit integer. */ - public static fromData(data: ByteVector, version: number): ExtendedHeader { + public static fromData(data: ByteVector, version: number): Id3v2ExtendedHeader { Guards.truthy(data, "data"); Guards.byte(version, "version"); - const header = new ExtendedHeader(); + const header = new Id3v2ExtendedHeader(); header.parse(data, version); return header; } @@ -24,8 +24,8 @@ export default class ExtendedHeader { /** * Constructs and initializes a new instance with no contents. */ - public static fromEmpty(): ExtendedHeader { - return new ExtendedHeader(); + public static fromEmpty(): Id3v2ExtendedHeader { + return new Id3v2ExtendedHeader(); } /** diff --git a/src/id3v2/id3v2TagSettings.ts b/src/id3v2/id3v2Settings.ts similarity index 93% rename from src/id3v2/id3v2TagSettings.ts rename to src/id3v2/id3v2Settings.ts index e905b439..0ceec5ce 100644 --- a/src/id3v2/id3v2TagSettings.ts +++ b/src/id3v2/id3v2Settings.ts @@ -1,7 +1,7 @@ import {StringType} from "../byteVector"; import {Guards} from "../utils"; -export default class Id3v2TagSettings { +export default class Id3v2Settings { private static _defaultEncoding: StringType = StringType.UTF8; private static _defaultVersion: number = 3; private static _forceDefaultEncoding: boolean = false; @@ -27,7 +27,7 @@ export default class Id3v2TagSettings { * Gets the default version to use when creating new tags. * If {@see forceDefaultEncoding} is `true` then all tags will be rendered with this version. */ - public static get defaultVersion(): number { return Id3v2TagSettings._defaultVersion; } + public static get defaultVersion(): number { return Id3v2Settings._defaultVersion; } /** * Sets the default version to use when creating new tags. * If {@see forceDefaultEncoding} is `true` then all tags will be rendered with this version. @@ -36,7 +36,7 @@ export default class Id3v2TagSettings { public static set defaultVersion(value: number) { Guards.byte(value, "value"); Guards.betweenInclusive(value, 2, 4, "value"); - Id3v2TagSettings._defaultVersion = value; + Id3v2Settings._defaultVersion = value; } /** @@ -48,14 +48,14 @@ export default class Id3v2TagSettings { * Gets whether or not to render all frames with the default encoding rather than their * original encoding. */ - public static get forceDefaultEncoding(): boolean { return Id3v2TagSettings._forceDefaultEncoding; } + public static get forceDefaultEncoding(): boolean { return Id3v2Settings._forceDefaultEncoding; } /** * Sets whether or not to render all frames with the default encoding rather than their * original encoding. * @param value If `true` frames will be rendered using {@see defaultEncoding} rather than * their original encoding. */ - public static set forceDefaultEncoding(value: boolean) { Id3v2TagSettings._forceDefaultEncoding = value; } + public static set forceDefaultEncoding(value: boolean) { Id3v2Settings._forceDefaultEncoding = value; } /** * Gets whether or not to save all tags in the default version rather than their original diff --git a/src/id3v2/id3v2Tag.ts b/src/id3v2/id3v2Tag.ts index cdddfc5a..8c825914 100644 --- a/src/id3v2/id3v2Tag.ts +++ b/src/id3v2/id3v2Tag.ts @@ -2,11 +2,10 @@ import * as DateFormat from "dateformat"; import AttachmentFrame from "./frames/attachmentFrame"; import CommentsFrame from "./frames/commentsFrame"; -import ExtendedHeader from "./extendedHeader"; +import Id3v2ExtendedHeader from "./id3v2ExtendedHeader"; import FrameFactory from "./frames/frameFactory"; import Genres from "../genres"; -import Header from "./header"; -import Id3v2TagSettings from "./id3v2TagSettings"; +import Id3v2Settings from "./id3v2Settings"; import SyncData from "./syncData"; import UniqueFileIdentifierFrame from "./frames/uniqueFileIdentifierFrame"; import UnsynchronizedLyricsFrame from "./frames/unsynchronizedLyricsFrame"; @@ -15,25 +14,80 @@ import {File, FileAccessMode, ReadStyle} from "../file"; import {Frame, FrameClassType} from "./frames/frame"; import {FrameIdentifier, FrameIdentifiers} from "./frameIdentifiers"; import {Id3v2FrameFlags, Id3v2FrameHeader} from "./frames/frameHeader"; -import {HeaderFlags} from "./headerFlags"; +import {Id3v2TagHeader, Id3v2TagHeaderFlags} from "./id3v2TagHeader"; import {IPicture} from "../picture"; import {Tag, TagTypes} from "../tag"; import {TextInformationFrame, UserTextInformationFrame} from "./frames/textInformationFrame"; import {UrlLinkFrame} from "./frames/urlLinkFrame"; import {Guards} from "../utils"; +import {CorruptFileError} from "../errors"; export default class Id3v2Tag extends Tag { private static _language: string = undefined; // @TODO: Use the os-locale module to supply a // lazily loaded "default" locale - private _extendedHeader: ExtendedHeader; + private _extendedHeader: Id3v2ExtendedHeader; private _frameList: Frame[] = []; - private _header: Header; + private _header: Id3v2TagHeader; private _performersRole: string[]; // #region Constructors + /** + * Constructs an empty ID3v2 tag + */ + public constructor() { + super(); + } + + /** + * Constructs and initializes a new Tag by reading the contents from a specified position in + * the provided file. + * @param file File from which the contents of the new instance is to be read + * @param position Offset into the file where the tag should be read from + * @param style How the data is to be read into the current instance + * @returns Id3v2Tag Tag with the data from the file read into it + */ + public static fromFile(file: File, position: number, style: ReadStyle): Id3v2Tag { + Guards.truthy(file, "file"); + Guards.uint(position, "position"); + if (position > file.length - Id3v2Settings.headerSize) { + throw new Error("Argument out of range: position must be within size of the file"); + } + + file.mode = FileAccessMode.Read; + const tag = new Id3v2Tag(); + tag.read(file, position, style); + return tag; + } + + /** + * Constructs and initializes a new Tag by reading the contents from a specified + * {@see ByteVector} object. + * @param data Tag data to read into a tag object + * @returns Id3v2Tag Tag with the data from the byte vector read into it + */ + public static fromData(data: ByteVector): Id3v2Tag { + Guards.truthy(data, "data"); + if (data.length < Id3v2Settings.headerSize) { + throw new CorruptFileError("Provided data does not contain enough bytes for an ID3v2 tag header"); + } + + const header = new Id3v2TagHeader(data); + // If the tag size is 0, then this is an invalid tag. Tags must contain at least one frame + const tag = new Id3v2Tag(); + if (header.tagSize === 0) { + return tag; + } + + if (data.length - Id3v2Settings.headerSize < header.tagSize) { + throw new CorruptFileError("Provided data does not enough tag data"); + } + + tag.parse(data.mid(Id3v2Settings.headerSize, header.tagSize), undefined, 0, ReadStyle.None); + return tag; + } // #endregion @@ -59,13 +113,13 @@ export default class Id3v2Tag extends Tag { /** * Gets the header flags applied to the current instance. */ - public get flags(): HeaderFlags { return this._header.flags; } + public get flags(): Id3v2TagHeaderFlags { return this._header.flags; } /** * Sets the header flags applied to the current instance - * @param value Bitwise combined {@see HeaderFlags} value contiaining flags applied to the + * @param value Bitwise combined {@see Id3v2TagHeaderFlags} value contiaining flags applied to the * current instance. */ - public set flags(value: HeaderFlags) { this._header.flags = value; } + public set flags(value: Id3v2TagHeaderFlags) { this._header.flags = value; } /** * Gets all frames contained in the current instance. @@ -95,8 +149,8 @@ export default class Id3v2Tag extends Tag { * Gets the ID3v2 version for the current instance. */ public get version(): number { - return Id3v2TagSettings.forceDefaultVersion - ? Id3v2TagSettings.defaultVersion + return Id3v2Settings.forceDefaultVersion + ? Id3v2Settings.defaultVersion : this._header.majorVersion; } /** @@ -269,7 +323,7 @@ export default class Id3v2Tag extends Tag { } /** @inheritDoc via TCON frame */ set genres(value: string[]) { - if (!value || !Id3v2TagSettings.useNumericGenres) { + if (!value || !Id3v2Settings.useNumericGenres) { this.setTextFrame(FrameIdentifiers.TCON, ...value); return; } @@ -359,7 +413,7 @@ export default class Id3v2Tag extends Tag { this.addFrame(frame); } frame.text = value; - frame.textEncoding = Id3v2TagSettings.defaultEncoding; + frame.textEncoding = Id3v2Settings.defaultEncoding; } /** @inheritDoc via TIT1 frame */ @@ -758,10 +812,10 @@ export default class Id3v2Tag extends Tag { // tag's header. The "tag data" (everything that is included in Header.tagSize) includes // the extended header, frames and padding, but does not include the tag's header or footer - const hasFooter = (this._header.flags & HeaderFlags.FooterPresent) !== 0; - const unsyncAtFrameLevel = (this._header.flags & HeaderFlags.Unsynchronication) !== 0 + const hasFooter = (this._header.flags & Id3v2TagHeaderFlags.FooterPresent) !== 0; + const unsyncAtFrameLevel = (this._header.flags & Id3v2TagHeaderFlags.Unsynchronication) !== 0 && this.version >= 4; - const unsyncAtTagLevel = (this._header.flags & HeaderFlags.Unsynchronication) !== 0 + const unsyncAtTagLevel = (this._header.flags & Id3v2TagHeaderFlags.Unsynchronication) !== 0 && this.version < 4; this._header.majorVersion = hasFooter ? 4 : this.version; @@ -769,7 +823,7 @@ export default class Id3v2Tag extends Tag { const tagData = ByteVector.empty(); // TODO: Render the extended header - this._header.flags &= ~HeaderFlags.ExtendedHeader; + this._header.flags &= ~Id3v2TagHeaderFlags.ExtendedHeader; // Loop through the frames rendering them and adding them to tag data for (const frame of this._frameList) { @@ -890,7 +944,7 @@ export default class Id3v2Tag extends Tag { } urlFrame.text = text; - urlFrame.textEncoding = Id3v2TagSettings.defaultEncoding; + urlFrame.textEncoding = Id3v2Settings.defaultEncoding; } else { const frames = this.getFramesByClassType(FrameClassType.TextInformationFrame); let frame = TextInformationFrame.findTextInformationFrame(frames, ident); @@ -900,7 +954,7 @@ export default class Id3v2Tag extends Tag { } frame.text = text; - frame.textEncoding = Id3v2TagSettings.defaultEncoding; + frame.textEncoding = Id3v2Settings.defaultEncoding; } } @@ -912,7 +966,7 @@ export default class Id3v2Tag extends Tag { // If the entire tag is marked as unsynchronized, and this tag is version ID3v2.3 or lower, // resynchronize it. const fullTagUnsync = this._header.majorVersion < 4 - && (this._header.flags & HeaderFlags.Unsynchronication) !== 0; + && (this._header.flags & Id3v2TagHeaderFlags.Unsynchronication) !== 0; // Avoid loading all the ID3 tag if PictureLazy is enabled and size is significant enough // (ID3v2.4 and later only) @@ -920,7 +974,7 @@ export default class Id3v2Tag extends Tag { fullTagUnsync || this._header.tagSize < 1024 || (style & ReadStyle.PictureLazy) !== 0 || - (this._header.flags & HeaderFlags.ExtendedHeader) !== 0 + (this._header.flags & Id3v2TagHeaderFlags.ExtendedHeader) !== 0 )) { file.seek(position); data = file.readBlock(this._header.tagSize); @@ -935,8 +989,8 @@ export default class Id3v2Tag extends Tag { + frameDataPosition - Id3v2FrameHeader.getSize(this._header.majorVersion); // Check for the extended header - if ((this._header.flags & HeaderFlags.ExtendedHeader) !== 0) { - this._extendedHeader = ExtendedHeader.fromData(data, this._header.majorVersion); + if ((this._header.flags & Id3v2TagHeaderFlags.ExtendedHeader) !== 0) { + this._extendedHeader = Id3v2ExtendedHeader.fromData(data, this._header.majorVersion); if (this._extendedHeader.size <= data.length) { frameDataPosition += this._extendedHeader.size; @@ -1037,20 +1091,20 @@ export default class Id3v2Tag extends Tag { file.mode = FileAccessMode.Read; - if (position > file.length - Id3v2TagSettings.headerSize) { + if (position > file.length - Id3v2Settings.headerSize) { throw new Error("Argument out of range: position must be less than the length of the file"); } file.seek(position); - this._header = new Header(file.readBlock(Id3v2TagSettings.headerSize)); + this._header = new Id3v2TagHeader(file.readBlock(Id3v2Settings.headerSize)); // If the tag size is 0, then this is an invalid tag. Tags must contain at least one frame. if (this._header.tagSize === 0) { return; } - position += Id3v2TagSettings.headerSize; + position += Id3v2Settings.headerSize; this.parse(undefined, file, position, style); } @@ -1151,7 +1205,7 @@ export default class Id3v2Tag extends Tag { if (!text && frame) { this.removeFrame(frame); } else { - frame = UserTextInformationFrame.fromDescription(description, Id3v2TagSettings.defaultEncoding); + frame = UserTextInformationFrame.fromDescription(description, Id3v2Settings.defaultEncoding); frame.text = text.split(";"); this.addFrame(frame); } diff --git a/src/id3v2/footer.ts b/src/id3v2/id3v2TagFooter.ts similarity index 87% rename from src/id3v2/footer.ts rename to src/id3v2/id3v2TagFooter.ts index d22888b7..09a2ad59 100644 --- a/src/id3v2/footer.ts +++ b/src/id3v2/id3v2TagFooter.ts @@ -1,13 +1,13 @@ -import Id3v2TagSettings from "./id3v2TagSettings"; +import Id3v2Settings from "./id3v2Settings"; import SyncData from "./syncData"; import {ByteVector} from "../byteVector"; import {CorruptFileError} from "../errors"; -import {HeaderFlags} from "./headerFlags"; +import {Id3v2TagHeaderFlags} from "./id3v2TagHeader"; import {Guards} from "../utils"; -export default class Footer { +export default class Id3v2TagFooter { private static readonly _fileIdentifier: ByteVector = ByteVector.fromString("3DI", undefined, undefined, true); - private _flags: HeaderFlags; + private _flags: Id3v2TagHeaderFlags; private _majorVersion: number; private _revisionNumber: number; private _tagSize: number; @@ -18,10 +18,10 @@ export default class Footer { */ public constructor(data: ByteVector) { Guards.truthy(data, "data"); - if (data.length < Id3v2TagSettings.footerSize) { + if (data.length < Id3v2Settings.footerSize) { throw new CorruptFileError("Provided data is smaller than object size."); } - if (!data.startsWith(Footer.fileIdentifier)) { + if (!data.startsWith(Id3v2TagFooter.fileIdentifier)) { throw new CorruptFileError("Provided data does not start with the file identifier"); } @@ -61,24 +61,24 @@ export default class Footer { * and footer. */ public get completeTagSize(): number { - return this.tagSize + Id3v2TagSettings.headerSize + Id3v2TagSettings.footerSize; + return this.tagSize + Id3v2Settings.headerSize + Id3v2Settings.footerSize; } /** * Gets the flags applied to the current instance. */ - public get flags(): HeaderFlags { return this._flags; } + public get flags(): Id3v2TagHeaderFlags { return this._flags; } /** * Sets the flags applied to the current instance. * @param value Bitwise combined {@see HeaderFlags} value containing the flags to apply to the * current instance. */ - public set flags(value: HeaderFlags) { - const version3Flags = HeaderFlags.ExtendedHeader | HeaderFlags.ExperimentalIndicator; + public set flags(value: Id3v2TagHeaderFlags) { + const version3Flags = Id3v2TagHeaderFlags.ExtendedHeader | Id3v2TagHeaderFlags.ExperimentalIndicator; if ((value & version3Flags) !== 0 && this.majorVersion < 3) { throw new Error("Feature only supported in version 2.3+"); } - const version4Flags = HeaderFlags.FooterPresent; + const version4Flags = Id3v2TagHeaderFlags.FooterPresent; if ((value & version4Flags) !== 0 && this.majorVersion < 4) { throw new Error("Feature only supported in version 2.4+"); } @@ -91,7 +91,7 @@ export default class Footer { */ public get majorVersion(): number { return this._majorVersion === 0 - ? Id3v2TagSettings.defaultVersion + ? Id3v2Settings.defaultVersion : this._majorVersion; } /** @@ -148,7 +148,7 @@ export default class Footer { public render(): ByteVector { return ByteVector.concatenate( - Footer.fileIdentifier, + Id3v2TagFooter.fileIdentifier, this.majorVersion, this.revisionNumber, this.flags, diff --git a/src/id3v2/header.ts b/src/id3v2/id3v2TagHeader.ts similarity index 75% rename from src/id3v2/header.ts rename to src/id3v2/id3v2TagHeader.ts index 6fda5768..41a7a3f5 100644 --- a/src/id3v2/header.ts +++ b/src/id3v2/id3v2TagHeader.ts @@ -1,13 +1,44 @@ -import Id3v2TagSettings from "./id3v2TagSettings"; +import Id3v2Settings from "./id3v2Settings"; import SyncData from "./syncData"; -import {ByteVector} from "../byteVector"; +import {ByteVector, StringType} from "../byteVector"; import {CorruptFileError} from "../errors"; -import {HeaderFlags} from "./headerFlags"; import {Guards} from "../utils"; -export default class Header { - private static readonly _fileIdentifier: ByteVector = ByteVector.fromString("ID3", undefined, undefined, true); - private _flags: HeaderFlags; +export enum Id3v2TagHeaderFlags { + /** + * The header contains no flags. + */ + None = 0x0, + + /** + * The tag described by the header contains a footer. + */ + FooterPresent = 0x10, + + /** + * The tag described by the header is experimental. + */ + ExperimentalIndicator = 0x20, + + /** + * The tag described by the header contains an extended header. + */ + ExtendedHeader = 0x40, + + /** + * The tag described by the header has been desynchronized. + */ + Unsynchronication = 0x80, +} + +export class Id3v2TagHeader { + private static readonly _fileIdentifier: ByteVector = ByteVector.fromString( + "ID3", + StringType.Latin1, + undefined, + true + ); + private _flags: Id3v2TagHeaderFlags; private _majorVersion: number; private _revisionNumber: number; private _tagSize: number; @@ -18,10 +49,10 @@ export default class Header { */ public constructor(data: ByteVector) { Guards.truthy(data, "data"); - if (data.length < Id3v2TagSettings.headerSize) { + if (data.length < Id3v2Settings.headerSize) { throw new CorruptFileError("Provided data is smaller than object size"); } - if (!data.startsWith(Header.fileIdentifier)) { + if (!data.startsWith(Id3v2TagHeader.fileIdentifier)) { throw new CorruptFileError("Provided data does not start with the file identifier"); } @@ -54,34 +85,34 @@ export default class Header { /** * The identifier used to recognize an ID3v2 header. */ - public static get fileIdentifier(): ByteVector { return Header._fileIdentifier; } + public static get fileIdentifier(): ByteVector { return Id3v2TagHeader._fileIdentifier; } /** * Gets the complete size of the tag described by the current instance including the header * and footer. */ public get completeTagSize(): number { - return (this._flags & HeaderFlags.FooterPresent) > 0 - ? this.tagSize + Id3v2TagSettings.headerSize + Id3v2TagSettings.footerSize - : this.tagSize + Id3v2TagSettings.headerSize; + return (this._flags & Id3v2TagHeaderFlags.FooterPresent) > 0 + ? this.tagSize + Id3v2Settings.headerSize + Id3v2Settings.footerSize + : this.tagSize + Id3v2Settings.headerSize; } /** * Gets the flags applied to the current instance. */ - public get flags(): HeaderFlags { return this._flags; } + public get flags(): Id3v2TagHeaderFlags { return this._flags; } /** * Sets the flags applied to the current instance. * @param value Bitwise combined {@see HeaderFlags} value containing the flags to apply to the * current instance. */ - public set flags(value: HeaderFlags) { + public set flags(value: Id3v2TagHeaderFlags) { // @TODO: Does it make sense to check for flags for major version <4? - const version3Flags = HeaderFlags.ExtendedHeader | HeaderFlags.ExperimentalIndicator; + const version3Flags = Id3v2TagHeaderFlags.ExtendedHeader | Id3v2TagHeaderFlags.ExperimentalIndicator; if ((value & version3Flags) > 0 && this.majorVersion < 3) { throw new Error("Feature only supported in version 2.3+"); } - const version4Flags = HeaderFlags.FooterPresent; + const version4Flags = Id3v2TagHeaderFlags.FooterPresent; if ((value & version4Flags) > 0 && this.majorVersion < 4) { throw new Error("Feature only supported in version 2.4+"); } @@ -94,7 +125,7 @@ export default class Header { */ public get majorVersion(): number { return this._majorVersion === 0 - ? Id3v2TagSettings.defaultVersion + ? Id3v2Settings.defaultVersion : this._majorVersion; } /** @@ -109,10 +140,10 @@ export default class Header { // @TODO: do we need to support setting to versions <4? if (value < 3) { - this._flags &= ~(HeaderFlags.ExtendedHeader | HeaderFlags.ExperimentalIndicator); + this._flags &= ~(Id3v2TagHeaderFlags.ExtendedHeader | Id3v2TagHeaderFlags.ExperimentalIndicator); } if (value < 4) { - this._flags &= ~HeaderFlags.FooterPresent; + this._flags &= ~Id3v2TagHeaderFlags.FooterPresent; } this._majorVersion = value; @@ -161,7 +192,7 @@ export default class Header { */ public render(): ByteVector { return ByteVector.concatenate( - Header.fileIdentifier, + Id3v2TagHeader.fileIdentifier, this.majorVersion, this.revisionNumber, this.flags, diff --git a/test/id3v2/attachmentsFrameTests.ts b/test/id3v2/attachmentsFrameTests.ts index bfcec4e3..ad43c0a5 100644 --- a/test/id3v2/attachmentsFrameTests.ts +++ b/test/id3v2/attachmentsFrameTests.ts @@ -10,7 +10,7 @@ import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; import {FrameIdentifier, FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; import {IPicture, PictureType} from "../../src/picture"; -import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; +import Id3v2Settings from "../../src/id3v2/id3v2Settings"; import FramePropertyTests from "./framePropertyTests"; // Setup chai @@ -68,7 +68,7 @@ class Id3v2_AttachmentFrame_ConstructorTests extends FrameConstructorTests { "fux", "bux", "application/octet-stream", - Id3v2TagSettings.defaultEncoding, + Id3v2Settings.defaultEncoding, PictureType.FrontCover ); } @@ -95,7 +95,7 @@ class Id3v2_AttachmentFrame_ConstructorTests extends FrameConstructorTests { "fux", "bux", "application/octet-stream", - Id3v2TagSettings.defaultEncoding, + Id3v2Settings.defaultEncoding, PictureType.NotAPicture ); } diff --git a/test/id3v2/commentsFrameTests.ts b/test/id3v2/commentsFrameTests.ts index 3f100df5..8252782e 100644 --- a/test/id3v2/commentsFrameTests.ts +++ b/test/id3v2/commentsFrameTests.ts @@ -5,7 +5,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import CommentsFrame from "../../src/id3v2/frames/commentsFrame"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; +import Id3v2Settings from "../../src/id3v2/id3v2Settings"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; @@ -56,7 +56,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { const frame = CommentsFrame.fromDescription(description); // Assert - this.validateFrame(frame, description, "XXX", Id3v2TagSettings.defaultEncoding, ""); + this.validateFrame(frame, description, "XXX", Id3v2Settings.defaultEncoding, ""); } @test @@ -69,7 +69,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { const frame = CommentsFrame.fromDescription(description, language); // Assert - this.validateFrame(frame, description, language, Id3v2TagSettings.defaultEncoding, ""); + this.validateFrame(frame, description, language, Id3v2Settings.defaultEncoding, ""); } @test diff --git a/test/id3v2/id3v2TagTests.ts b/test/id3v2/id3v2TagTests.ts new file mode 100644 index 00000000..e69de29b diff --git a/test/id3v2/synchronizedLyricsFrameTests.ts b/test/id3v2/synchronizedLyricsFrameTests.ts index d22d0dae..c4603539 100644 --- a/test/id3v2/synchronizedLyricsFrameTests.ts +++ b/test/id3v2/synchronizedLyricsFrameTests.ts @@ -4,7 +4,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; +import Id3v2Settings from "../../src/id3v2/id3v2Settings"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; @@ -73,7 +73,7 @@ class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTes TimestampFormat.Unknown, language, [], - Id3v2TagSettings.defaultEncoding, + Id3v2Settings.defaultEncoding, textType ); } diff --git a/test/id3v2/tagExtendedHeaderTests.ts b/test/id3v2/tagExtendedHeaderTests.ts index 3740d80f..247bda05 100644 --- a/test/id3v2/tagExtendedHeaderTests.ts +++ b/test/id3v2/tagExtendedHeaderTests.ts @@ -2,7 +2,7 @@ import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; -import ExtendedHeader from "../../src/id3v2/extendedHeader"; +import Id3v2ExtendedHeader from "../../src/id3v2/id3v2ExtendedHeader"; import {ByteVector} from "../../src/byteVector"; // Setup chai @@ -14,8 +14,8 @@ class Id3v2_TagExtendedHeaderTests { @test public fromData_falsyData() { // Act/Assert - assert.throws(() => { ExtendedHeader.fromData(null, 2); }); - assert.throws(() => { ExtendedHeader.fromData(undefined, 2); }); + assert.throws(() => { Id3v2ExtendedHeader.fromData(null, 2); }); + assert.throws(() => { Id3v2ExtendedHeader.fromData(undefined, 2); }); } @test @@ -24,9 +24,9 @@ class Id3v2_TagExtendedHeaderTests { const testData = ByteVector.empty(); // Act/Assert - assert.throws(() => { ExtendedHeader.fromData(testData, -1); }); - assert.throws(() => { ExtendedHeader.fromData(testData, 1.5); }); - assert.throws(() => { ExtendedHeader.fromData(testData, 0x100); }); + assert.throws(() => { Id3v2ExtendedHeader.fromData(testData, -1); }); + assert.throws(() => { Id3v2ExtendedHeader.fromData(testData, 1.5); }); + assert.throws(() => { Id3v2ExtendedHeader.fromData(testData, 0x100); }); } @test @@ -35,7 +35,7 @@ class Id3v2_TagExtendedHeaderTests { const testData = ByteVector.concatenate(0x10, 0x10, 0x10, 0x10); // Act - const output = ExtendedHeader.fromData(testData, 3); + const output = Id3v2ExtendedHeader.fromData(testData, 3); // Assert assert.equal(output.size, 4 + 0x2040810); @@ -47,7 +47,7 @@ class Id3v2_TagExtendedHeaderTests { const testData = ByteVector.concatenate(0x10, 0x10, 0x10, 0x10); // Act - const output = ExtendedHeader.fromData(testData, 2); + const output = Id3v2ExtendedHeader.fromData(testData, 2); // Assert assert.equal(output.size, 0x2040810); @@ -59,7 +59,7 @@ class Id3v2_TagExtendedHeaderTests { const testData = ByteVector.concatenate(0x10, 0x10, 0x10, 0x10); // Act - const output = ExtendedHeader.fromData(testData, 4); + const output = Id3v2ExtendedHeader.fromData(testData, 4); // Assert assert.equal(output.size, 0x2040810); @@ -67,7 +67,7 @@ class Id3v2_TagExtendedHeaderTests { public fromEmpty() { // Act - const output = ExtendedHeader.fromEmpty(); + const output = Id3v2ExtendedHeader.fromEmpty(); // Assert assert.equal(output.size, 0); diff --git a/test/id3v2/tagFooterTests.ts b/test/id3v2/tagFooterTests.ts index 8fcf8147..f7365f6b 100644 --- a/test/id3v2/tagFooterTests.ts +++ b/test/id3v2/tagFooterTests.ts @@ -2,25 +2,25 @@ import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; -import Footer from "../../src/id3v2/footer"; -import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; +import Id3v2TagFooter from "../../src/id3v2/id3v2TagFooter"; +import Id3v2Settings from "../../src/id3v2/id3v2Settings"; import TestConstants from "../testConstants"; import {ByteVector} from "../../src/byteVector"; -import {HeaderFlags} from "../../src/id3v2/headerFlags"; +import {Id3v2TagHeaderFlags} from "../../src/id3v2/id3v2TagHeader"; // Setup chai Chai.use(ChaiAsPromised); const assert = Chai.assert; -const getTestFooter = (majorVersion: number, minorVersion: number, flags: HeaderFlags): Footer => { +const getTestFooter = (majorVersion: number, minorVersion: number, flags: Id3v2TagHeaderFlags): Id3v2TagFooter => { const data = ByteVector.concatenate( - Footer.fileIdentifier, + Id3v2TagFooter.fileIdentifier, majorVersion, minorVersion, flags, TestConstants.syncedUintBytes ); - return new Footer(data); + return new Id3v2TagFooter(data); }; @suite(timeout(3000), slow(1000)) @@ -28,8 +28,8 @@ class Id3v2_TagFooter_ConstructorTests { @test public falsyData() { // Act/Assert - assert.throws(() => { const _ = new Footer(null); }); - assert.throws(() => { const _ = new Footer(undefined); }); + assert.throws(() => { const _ = new Id3v2TagFooter(null); }); + assert.throws(() => { const _ = new Id3v2TagFooter(undefined); }); } @test @@ -38,7 +38,7 @@ class Id3v2_TagFooter_ConstructorTests { const data = ByteVector.fromSize(1); // Act/Assert - assert.throws(() => { const _ = new Footer(data); }); + assert.throws(() => { const _ = new Id3v2TagFooter(data); }); } @test @@ -47,26 +47,26 @@ class Id3v2_TagFooter_ConstructorTests { const data = ByteVector.fromSize(10); // Act/Assert - assert.throws(() => { const _ = new Footer(data); }); + assert.throws(() => { const _ = new Id3v2TagFooter(data); }); } @test public invalidFlags_version4() { // Arrange const data = ByteVector.concatenate( - Footer.fileIdentifier, + Id3v2TagFooter.fileIdentifier, 0x04, 0x00, 0x07 ); // Act/Assert - assert.throws(() => { const _ = new Footer(data); }); + assert.throws(() => { const _ = new Id3v2TagFooter(data); }); } @test public invalidTagSizeBytes() { // Arrange const testData = ByteVector.concatenate( - Footer.fileIdentifier, + Id3v2TagFooter.fileIdentifier, 0x04, 0x00, 0x00 ); const testData1 = ByteVector.concatenate(testData, 0x80, 0x00, 0x00, 0x00); @@ -75,10 +75,10 @@ class Id3v2_TagFooter_ConstructorTests { const testData4 = ByteVector.concatenate(testData, 0x00, 0x00, 0x00, 0x80); // Act/Assert - assert.throws(() => { const _ = new Footer(testData1); }); - assert.throws(() => { const _ = new Footer(testData2); }); - assert.throws(() => { const _ = new Footer(testData3); }); - assert.throws(() => { const _ = new Footer(testData4); }); + assert.throws(() => { const _ = new Id3v2TagFooter(testData1); }); + assert.throws(() => { const _ = new Id3v2TagFooter(testData2); }); + assert.throws(() => { const _ = new Id3v2TagFooter(testData3); }); + assert.throws(() => { const _ = new Id3v2TagFooter(testData4); }); } @test @@ -88,7 +88,7 @@ class Id3v2_TagFooter_ConstructorTests { const minorVersion = 0x00; const flags = 0xE0; const testData = ByteVector.concatenate( - Footer.fileIdentifier, + Id3v2TagFooter.fileIdentifier, majorVersion, minorVersion, flags, @@ -96,7 +96,7 @@ class Id3v2_TagFooter_ConstructorTests { ); // Act - const output = new Footer(testData); + const output = new Id3v2TagFooter(testData); // Assert assert.equal(output.flags, flags); @@ -111,43 +111,43 @@ class Id3v2_TagFooter_PropertyTests { @test public getCompleteTagSize() { // Arrange - const footer = getTestFooter(4, 0, HeaderFlags.None); + const footer = getTestFooter(4, 0, Id3v2TagHeaderFlags.None); // Act const output = footer.completeTagSize; // Assert - assert.equal(output, footer.tagSize + Id3v2TagSettings.headerSize + Id3v2TagSettings.footerSize); + assert.equal(output, footer.tagSize + Id3v2Settings.headerSize + Id3v2Settings.footerSize); } @test public setFlags_validFlags() { // Arrange - const footer = getTestFooter(4, 0, HeaderFlags.None); + const footer = getTestFooter(4, 0, Id3v2TagHeaderFlags.None); // Act - footer.flags = HeaderFlags.FooterPresent; + footer.flags = Id3v2TagHeaderFlags.FooterPresent; // Assert - assert.equal(footer.flags, HeaderFlags.FooterPresent); + assert.equal(footer.flags, Id3v2TagHeaderFlags.FooterPresent); } @test public getMajorVersion_zero() { // Arrange - const footer = getTestFooter(0, 0, HeaderFlags.None); + const footer = getTestFooter(0, 0, Id3v2TagHeaderFlags.None); // Act const output = footer.majorVersion; // Assert - assert.equal(output, Id3v2TagSettings.defaultVersion); + assert.equal(output, Id3v2Settings.defaultVersion); } @test public getMajorVersion_nonZero() { // Arrange - const footer = getTestFooter(4, 0, HeaderFlags.None); + const footer = getTestFooter(4, 0, Id3v2TagHeaderFlags.None); // Act const output = footer.majorVersion; @@ -159,7 +159,7 @@ class Id3v2_TagFooter_PropertyTests { @test public setMajorVersion_invalidValues() { // Arrange - const footer = getTestFooter(4, 0, HeaderFlags.None); + const footer = getTestFooter(4, 0, Id3v2TagHeaderFlags.None); // Act/Assert assert.throws(() => { footer.majorVersion = -1; }); @@ -172,7 +172,7 @@ class Id3v2_TagFooter_PropertyTests { @test public setRevisionNumber_invalidValue() { // Arrange - const footer = getTestFooter(4, 0, HeaderFlags.None); + const footer = getTestFooter(4, 0, Id3v2TagHeaderFlags.None); // Act/Assert assert.throws(() => { footer.revisionNumber = -1; }); @@ -183,7 +183,7 @@ class Id3v2_TagFooter_PropertyTests { @test public setRevisionNumber_validValue() { // Arrange - const footer = getTestFooter(4, 0, HeaderFlags.None); + const footer = getTestFooter(4, 0, Id3v2TagHeaderFlags.None); // Act footer.revisionNumber = 2; @@ -195,7 +195,7 @@ class Id3v2_TagFooter_PropertyTests { @test public setTagSize_invalidValues() { // Arrange - const footer = getTestFooter(4, 0, HeaderFlags.None); + const footer = getTestFooter(4, 0, Id3v2TagHeaderFlags.None); // Act/Assert assert.throws(() => { footer.tagSize = -1; }); @@ -206,7 +206,7 @@ class Id3v2_TagFooter_PropertyTests { @test public publicsetTagSize_validValue() { // Arrange - const footer = getTestFooter(4, 0, HeaderFlags.None); + const footer = getTestFooter(4, 0, Id3v2TagHeaderFlags.None); // Act footer.tagSize = 0x1234; @@ -225,13 +225,13 @@ class Id3v2_TagFooter_RenderTests { const minorVersion = 0x00; const flags = 0xE0; const testData = ByteVector.concatenate( - Footer.fileIdentifier, + Id3v2TagFooter.fileIdentifier, majorVersion, minorVersion, flags, TestConstants.syncedUintBytes ); - const footer = new Footer(testData); + const footer = new Id3v2TagFooter(testData); // Act const output = footer.render(); diff --git a/test/id3v2/tagHeaderTests.ts b/test/id3v2/tagHeaderTests.ts index b21e1b9c..1f81d517 100644 --- a/test/id3v2/tagHeaderTests.ts +++ b/test/id3v2/tagHeaderTests.ts @@ -2,25 +2,24 @@ import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; -import Header from "../../src/id3v2/header"; -import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; +import Id3v2Settings from "../../src/id3v2/id3v2Settings"; import TestConstants from "../testConstants"; import {ByteVector} from "../../src/byteVector"; -import {HeaderFlags} from "../../src/id3v2/headerFlags"; +import {Id3v2TagHeader, Id3v2TagHeaderFlags} from "../../src/id3v2/id3v2TagHeader"; // Setup chai Chai.use(ChaiAsPromised); const assert = Chai.assert; -const getTestHeader = (majorVersion: number, minorVersion: number, flags: HeaderFlags): Header => { +const getTestHeader = (majorVersion: number, minorVersion: number, flags: Id3v2TagHeaderFlags): Id3v2TagHeader => { const data = ByteVector.concatenate( - Header.fileIdentifier, + Id3v2TagHeader.fileIdentifier, majorVersion, minorVersion, flags, 0x10, 0x10, 0x10, 0x10 ); - return new Header(data); + return new Id3v2TagHeader(data); }; @suite(timeout(3000), slow(1000)) @@ -28,8 +27,8 @@ class Id3v2_TagHeader_ConstructorTests { @test public falsyData() { // Act/Assert - assert.throws(() => { const _ = new Header(null); }); - assert.throws(() => { const _ = new Header(undefined); }); + assert.throws(() => { const _ = new Id3v2TagHeader(null); }); + assert.throws(() => { const _ = new Id3v2TagHeader(undefined); }); } @test @@ -39,63 +38,63 @@ class Id3v2_TagHeader_ConstructorTests { const data1 = ByteVector.fromSize(1); // Act/Assert - assert.throws(() => { const _ = new Header(data0); }); - assert.throws(() => { const _ = new Header(data1); }); + assert.throws(() => { const _ = new Id3v2TagHeader(data0); }); + assert.throws(() => { const _ = new Id3v2TagHeader(data1); }); } @test public invalidStartOfData() { // Act/Assert - assert.throws(() => { const _ = new Header(TestConstants.testByteVector); }); + assert.throws(() => { const _ = new Id3v2TagHeader(TestConstants.testByteVector); }); } @test public invalidFlagsForVersion2() { // Arrange const testData = ByteVector.concatenate( - Header.fileIdentifier, + Id3v2TagHeader.fileIdentifier, 0x02, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00 ); // Act/Assert - assert.throws(() => { const _ = new Header(testData); }); + assert.throws(() => { const _ = new Id3v2TagHeader(testData); }); } @test public invalidFlagsForVersion3() { // Arrange const testData = ByteVector.concatenate( - Header.fileIdentifier, + Id3v2TagHeader.fileIdentifier, 0x03, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00 ); // Act/Assert - assert.throws(() => { const _ = new Header(testData); }); + assert.throws(() => { const _ = new Id3v2TagHeader(testData); }); } @test public invalidFlagsForVersion4() { // Arrange const testData = ByteVector.concatenate( - Header.fileIdentifier, + Id3v2TagHeader.fileIdentifier, 0x04, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00 ); // Act/Assert - assert.throws(() => { const _ = new Header(testData); }); + assert.throws(() => { const _ = new Id3v2TagHeader(testData); }); } @test public invalidTagSizeBytes() { // Arrange const testData = ByteVector.concatenate( - Header.fileIdentifier, + Id3v2TagHeader.fileIdentifier, 0x04, 0x00, 0x00 ); @@ -105,10 +104,10 @@ class Id3v2_TagHeader_ConstructorTests { const testData4 = ByteVector.concatenate(testData, 0x00, 0x00, 0x00, 0x80); // Act/Assert - assert.throws(() => {const _ = new Header(testData1); }); - assert.throws(() => {const _ = new Header(testData2); }); - assert.throws(() => {const _ = new Header(testData3); }); - assert.throws(() => {const _ = new Header(testData4); }); + assert.throws(() => {const _ = new Id3v2TagHeader(testData1); }); + assert.throws(() => {const _ = new Id3v2TagHeader(testData2); }); + assert.throws(() => {const _ = new Id3v2TagHeader(testData3); }); + assert.throws(() => {const _ = new Id3v2TagHeader(testData4); }); } @test @@ -118,7 +117,7 @@ class Id3v2_TagHeader_ConstructorTests { const minorVersion = 0x00; const flags = 0xE0; const testData = ByteVector.concatenate( - Header.fileIdentifier, + Id3v2TagHeader.fileIdentifier, majorVersion, minorVersion, flags, @@ -126,7 +125,7 @@ class Id3v2_TagHeader_ConstructorTests { ); // Act - const output = new Header(testData); + const output = new Id3v2TagHeader(testData); // Assert assert.equal(output.flags, flags); @@ -141,7 +140,7 @@ class Id3v2_TagHeader_PropertyTests { @test public getFileIdentifier() { // Act - const output = Header.fileIdentifier; + const output = Id3v2TagHeader.fileIdentifier; // Assert assert.ok(output); @@ -150,85 +149,85 @@ class Id3v2_TagHeader_PropertyTests { @test public getCompleteTagSize_withFooter() { // Arrange - const header = getTestHeader(4, 0, HeaderFlags.FooterPresent); + const header = getTestHeader(4, 0, Id3v2TagHeaderFlags.FooterPresent); // Act const totalSize = header.completeTagSize; // Assert - assert.equal(totalSize, header.tagSize + Id3v2TagSettings.headerSize + Id3v2TagSettings.footerSize); + assert.equal(totalSize, header.tagSize + Id3v2Settings.headerSize + Id3v2Settings.footerSize); } @test public getCompleteTagSize_noFooter() { // Arrange - const header = getTestHeader(4, 0, HeaderFlags.None); + const header = getTestHeader(4, 0, Id3v2TagHeaderFlags.None); // Act const totalSize = header.completeTagSize; // Assert - assert.equal(totalSize, header.tagSize + Id3v2TagSettings.headerSize); + assert.equal(totalSize, header.tagSize + Id3v2Settings.headerSize); } @test public setFlags_version2InvalidFlags() { // Arrange - const header = getTestHeader(2, 0, HeaderFlags.None); + const header = getTestHeader(2, 0, Id3v2TagHeaderFlags.None); // Act - const set1 = () => { header.flags = HeaderFlags.ExtendedHeader; }; - const set2 = () => { header.flags = HeaderFlags.ExperimentalIndicator; }; - const set3 = () => { header.flags = HeaderFlags.FooterPresent; }; + const set1 = () => { header.flags = Id3v2TagHeaderFlags.ExtendedHeader; }; + const set2 = () => { header.flags = Id3v2TagHeaderFlags.ExperimentalIndicator; }; + const set3 = () => { header.flags = Id3v2TagHeaderFlags.FooterPresent; }; // Assert assert.throws(set1); assert.throws(set2); assert.throws(set3); - assert.equal(header.flags, HeaderFlags.None); + assert.equal(header.flags, Id3v2TagHeaderFlags.None); } @test public setFlags_version3InvalidFlags() { // Arrange - const header = getTestHeader(3, 0, HeaderFlags.None); + const header = getTestHeader(3, 0, Id3v2TagHeaderFlags.None); // Act - const set = () => { header.flags = HeaderFlags.FooterPresent; }; + const set = () => { header.flags = Id3v2TagHeaderFlags.FooterPresent; }; // Assert assert.throws(set); - assert.equal(header.flags, HeaderFlags.None); + assert.equal(header.flags, Id3v2TagHeaderFlags.None); } @test public setFlags_validFlags() { // Arrange - const header = getTestHeader(4, 0, HeaderFlags.None); + const header = getTestHeader(4, 0, Id3v2TagHeaderFlags.None); // Act - header.flags = HeaderFlags.FooterPresent; + header.flags = Id3v2TagHeaderFlags.FooterPresent; // Assert - assert.equal(header.flags, HeaderFlags.FooterPresent); + assert.equal(header.flags, Id3v2TagHeaderFlags.FooterPresent); } @test public getMajorVersion_zero() { // Arrange - const header = getTestHeader(0, 0, HeaderFlags.None); + const header = getTestHeader(0, 0, Id3v2TagHeaderFlags.None); // Act const output = header.majorVersion; // Assert - assert.equal(output, Id3v2TagSettings.defaultVersion); + assert.equal(output, Id3v2Settings.defaultVersion); } @test public getMajorVersion_nonZero() { // Arrange - const header = getTestHeader(4, 0, HeaderFlags.None); + const header = getTestHeader(4, 0, Id3v2TagHeaderFlags.None); // Act const output = header.majorVersion; @@ -240,7 +239,7 @@ class Id3v2_TagHeader_PropertyTests { @test public setMajorVersion_invalidValues() { // Arrange - const header = getTestHeader(4, 0, HeaderFlags.None); + const header = getTestHeader(4, 0, Id3v2TagHeaderFlags.None); // Act/Assert assert.throws(() => { header.majorVersion = -1; }); @@ -253,7 +252,9 @@ class Id3v2_TagHeader_PropertyTests { @test public setMajorVersion_2_unsets3And4Flags() { // Arrange - const flags = HeaderFlags.ExtendedHeader | HeaderFlags.ExperimentalIndicator | HeaderFlags.FooterPresent; + const flags = Id3v2TagHeaderFlags.ExtendedHeader | + Id3v2TagHeaderFlags.ExperimentalIndicator | + Id3v2TagHeaderFlags.FooterPresent; const header = getTestHeader(4, 0, flags); // Act @@ -267,7 +268,7 @@ class Id3v2_TagHeader_PropertyTests { @test public setMajorVersion3_unsets4Flags() { // Arrange - const flags = HeaderFlags.FooterPresent; + const flags = Id3v2TagHeaderFlags.FooterPresent; const header = getTestHeader(4, 0, flags); // Act @@ -281,7 +282,7 @@ class Id3v2_TagHeader_PropertyTests { @test public setRevisionNumber_invalidValue() { // Arrange - const header = getTestHeader(4, 0, HeaderFlags.None); + const header = getTestHeader(4, 0, Id3v2TagHeaderFlags.None); // Act/Assert assert.throws(() => { header.revisionNumber = -1; }); @@ -292,7 +293,7 @@ class Id3v2_TagHeader_PropertyTests { @test public setRevisionNumber_validValue() { // Arrange - const header = getTestHeader(4, 0, HeaderFlags.None); + const header = getTestHeader(4, 0, Id3v2TagHeaderFlags.None); // Act header.revisionNumber = 2; @@ -304,7 +305,7 @@ class Id3v2_TagHeader_PropertyTests { @test public setTagSize_invalidValues() { // Arrange - const header = getTestHeader(4, 0, HeaderFlags.None); + const header = getTestHeader(4, 0, Id3v2TagHeaderFlags.None); // Act/Assert assert.throws(() => { header.tagSize = -1; }); @@ -315,7 +316,7 @@ class Id3v2_TagHeader_PropertyTests { @test public setTagSize_validValue() { // Arrange - const header = getTestHeader(4, 0, HeaderFlags.None); + const header = getTestHeader(4, 0, Id3v2TagHeaderFlags.None); // Act header.tagSize = 0x1234; @@ -334,13 +335,13 @@ class Id3v2_TagHeader_RenderTests { const minorVersion = 0x00; const flags = 0xE0; const testData = ByteVector.concatenate( - Header.fileIdentifier, + Id3v2TagHeader.fileIdentifier, majorVersion, minorVersion, flags, 0x10, 0x10, 0x10, 0x10 ); - const header = new Header(testData); + const header = new Id3v2TagHeader(testData); // Act const output = header.render(); diff --git a/test/id3v2/termsOfUseFrameTests.ts b/test/id3v2/termsOfUseFrameTests.ts index e0405228..b0a8ecb0 100644 --- a/test/id3v2/termsOfUseFrameTests.ts +++ b/test/id3v2/termsOfUseFrameTests.ts @@ -4,7 +4,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; +import Id3v2Settings from "../../src/id3v2/id3v2Settings"; import TermsOfUseFrame from "../../src/id3v2/frames/termsOfUseFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; @@ -31,7 +31,7 @@ class Id3v2_TermsOfUseFrame_ConstructorTests extends FrameConstructorTests { const output = TermsOfUseFrame.fromFields("fux"); // Assert - this.assertFrame(output, "fux", "", Id3v2TagSettings.defaultEncoding); + this.assertFrame(output, "fux", "", Id3v2Settings.defaultEncoding); } @test diff --git a/test/id3v2/textInformationFrameTests.ts b/test/id3v2/textInformationFrameTests.ts index 14b51fbe..e65e8144 100644 --- a/test/id3v2/textInformationFrameTests.ts +++ b/test/id3v2/textInformationFrameTests.ts @@ -4,7 +4,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import FramePropertyTests from "./framePropertyTests"; -import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; +import Id3v2Settings from "../../src/id3v2/id3v2Settings"; import {TextInformationFrame} from "../../src/id3v2/frames/textInformationFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; @@ -52,7 +52,7 @@ class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests assert.isOk(frame.text); assert.isArray(frame.text); assert.isEmpty(frame.text); - assert.strictEqual(frame.textEncoding, Id3v2TagSettings.defaultEncoding); + assert.strictEqual(frame.textEncoding, Id3v2Settings.defaultEncoding); } @test @@ -353,7 +353,7 @@ class Id3v2_TextInformationFrame_MethodTests { header.frameSize = 8; const data = ByteVector.concatenate( header.render(4), - Id3v2TagSettings.defaultEncoding, + Id3v2Settings.defaultEncoding, ByteVector.fromString("fux", StringType.Latin1), ByteVector.getTextDelimiter(StringType.Latin1), ByteVector.fromString("bux", StringType.Latin1), @@ -375,7 +375,7 @@ class Id3v2_TextInformationFrame_MethodTests { header.frameSize = 8; const data = ByteVector.concatenate( header.render(4), - Id3v2TagSettings.defaultEncoding, + Id3v2Settings.defaultEncoding, ByteVector.fromString("fux", StringType.Latin1), ByteVector.getTextDelimiter(StringType.Latin1), ByteVector.fromString("bux", StringType.Latin1), diff --git a/test/id3v2/userTextInformationFrameTests.ts b/test/id3v2/userTextInformationFrameTests.ts index 26486b96..e979f152 100644 --- a/test/id3v2/userTextInformationFrameTests.ts +++ b/test/id3v2/userTextInformationFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import Id3v2TagSettings from "../../src/id3v2/id3v2TagSettings"; +import Id3v2Settings from "../../src/id3v2/id3v2Settings"; import {UserTextInformationFrame} from "../../src/id3v2/frames/textInformationFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; @@ -44,7 +44,7 @@ class Id3v2_UserInformationFrame_ConstructorTests extends FrameConstructorTests const frame = UserTextInformationFrame.fromDescription("foo"); // Assert - this.assertFrame(frame, "foo", [], Id3v2TagSettings.defaultEncoding); + this.assertFrame(frame, "foo", [], Id3v2Settings.defaultEncoding); } @test From 970f371e7d23a70009837bc9e9624e68c535369f Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Mon, 24 Feb 2020 20:50:13 -0500 Subject: [PATCH 50/71] Some more tests for tag --- src/errors.ts | 8 ++ src/id3v2/id3v2Tag.ts | 60 ++++----------- src/id3v2/syncData.ts | 2 +- test/id3v2/id3v2TagTests.ts | 143 ++++++++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 45 deletions(-) diff --git a/src/errors.ts b/src/errors.ts index cbf15e6a..a3b3834b 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -4,6 +4,10 @@ export class CorruptFileError extends Error { public constructor(msg?: string) { super(msg); } + + public static errorIs(e: Error): boolean { + return e.hasOwnProperty("isCorruptFileError"); + } } export class NotImplementedError extends Error { @@ -12,4 +16,8 @@ export class NotImplementedError extends Error { public constructor(message?: string) { super(`Not implemented${message ? `: ${message}` : ""}`); } + + public static errorIs(e: Error): boolean { + return e.hasOwnProperty("isNotImplementedError"); + } } diff --git a/src/id3v2/id3v2Tag.ts b/src/id3v2/id3v2Tag.ts index 8c825914..f9d3008e 100644 --- a/src/id3v2/id3v2Tag.ts +++ b/src/id3v2/id3v2Tag.ts @@ -10,6 +10,7 @@ import SyncData from "./syncData"; import UniqueFileIdentifierFrame from "./frames/uniqueFileIdentifierFrame"; import UnsynchronizedLyricsFrame from "./frames/unsynchronizedLyricsFrame"; import {ByteVector, StringType} from "../byteVector"; +import {CorruptFileError} from "../errors"; import {File, FileAccessMode, ReadStyle} from "../file"; import {Frame, FrameClassType} from "./frames/frame"; import {FrameIdentifier, FrameIdentifiers} from "./frameIdentifiers"; @@ -20,7 +21,6 @@ import {Tag, TagTypes} from "../tag"; import {TextInformationFrame, UserTextInformationFrame} from "./frames/textInformationFrame"; import {UrlLinkFrame} from "./frames/urlLinkFrame"; import {Guards} from "../utils"; -import {CorruptFileError} from "../errors"; export default class Id3v2Tag extends Tag { private static _language: string = undefined; // @TODO: Use the os-locale module to supply a @@ -55,7 +55,6 @@ export default class Id3v2Tag extends Tag { throw new Error("Argument out of range: position must be within size of the file"); } - file.mode = FileAccessMode.Read; const tag = new Id3v2Tag(); tag.read(file, position, style); return tag; @@ -73,19 +72,19 @@ export default class Id3v2Tag extends Tag { throw new CorruptFileError("Provided data does not contain enough bytes for an ID3v2 tag header"); } - const header = new Id3v2TagHeader(data); + const tag = new Id3v2Tag(); + tag._header = new Id3v2TagHeader(data); // If the tag size is 0, then this is an invalid tag. Tags must contain at least one frame - const tag = new Id3v2Tag(); - if (header.tagSize === 0) { + if (tag._header.tagSize === 0) { return tag; } - if (data.length - Id3v2Settings.headerSize < header.tagSize) { + if (data.length - Id3v2Settings.headerSize < tag._header.tagSize) { throw new CorruptFileError("Provided data does not enough tag data"); } - tag.parse(data.mid(Id3v2Settings.headerSize, header.tagSize), undefined, 0, ReadStyle.None); + tag.parse(data.mid(Id3v2Settings.headerSize, tag._header.tagSize), undefined, 0, ReadStyle.None); return tag; } @@ -970,7 +969,7 @@ export default class Id3v2Tag extends Tag { // Avoid loading all the ID3 tag if PictureLazy is enabled and size is significant enough // (ID3v2.4 and later only) - if (data && ( + if (file && ( fullTagUnsync || this._header.tagSize < 1024 || (style & ReadStyle.PictureLazy) !== 0 || @@ -985,8 +984,7 @@ export default class Id3v2Tag extends Tag { } let frameDataPosition = data ? 0 : position; - let frameDataEndPosition = (data ? data.length : this._header.tagSize) - + frameDataPosition - Id3v2FrameHeader.getSize(this._header.majorVersion); + let frameDataEndPosition = (data ? data.length : this._header.tagSize) + frameDataPosition; // Check for the extended header if ((this._header.flags & Id3v2TagHeaderFlags.ExtendedHeader) !== 0) { @@ -1016,20 +1014,24 @@ export default class Id3v2Tag extends Tag { this._header.majorVersion, fullTagUnsync ); + + // If the frame factory returned undefined, that means we've hit the end of frames + if (!frameRead) { + break; + } + + // We found a frame, deconstruct the read result frame = frameRead.frame; frameDataPosition = frameRead.offset; } catch (e) { if (!e.hasOwnProperty("isNotImplementedError") && !e.hasOwnProperty("isCorruptFileError")) { throw e; } else { + // @TODO: In this case, we're actually entering an infinite loop. Add a catch for exceptions in the frame factory continue; } } - if (!frame) { - break; - } - // Only add frames that contain data if (frame.size === 0) { continue; @@ -1086,9 +1088,6 @@ export default class Id3v2Tag extends Tag { } protected read(file: File, position: number, style: ReadStyle): void { - Guards.truthy(file, "file"); - Guards.uint(position, "position"); - file.mode = FileAccessMode.Read; if (position > file.length - Id3v2Settings.headerSize) { @@ -1154,33 +1153,6 @@ export default class Id3v2Tag extends Tag { return result || undefined; } - private makeFirstOfType(frame: Frame): void { - const type = frame.frameId; - - let swapping: Frame; - for (let i = 0; i < this._frameList.length; i++) { - if (!swapping) { - if (this._frameList[i].frameId === type) { - swapping = frame; - } else { - continue; - } - } - - const tmp = this._frameList[i]; - this._frameList[i] = swapping; - swapping = tmp; - - if (swapping === frame) { - return; - } - } - - if (swapping) { - this._frameList.push(swapping); - } - } - private setUfidText(owner: string, text: string): void { // Get the UFID frame, create if necessary const frames = this.getFramesByClassType(FrameClassType.UniqueFileIdentifierFrame); diff --git a/src/id3v2/syncData.ts b/src/id3v2/syncData.ts index c08d1869..e17dd095 100644 --- a/src/id3v2/syncData.ts +++ b/src/id3v2/syncData.ts @@ -33,7 +33,7 @@ export default { * @param data Object to resynchronize */ resyncByteVector: (data: ByteVector): void => { - Guards.notNullOrUndefined(data, "data"); + Guards.truthy(data, "data"); let i = 0; let j = 0; diff --git a/test/id3v2/id3v2TagTests.ts b/test/id3v2/id3v2TagTests.ts index e69de29b..9d00879b 100644 --- a/test/id3v2/id3v2TagTests.ts +++ b/test/id3v2/id3v2TagTests.ts @@ -0,0 +1,143 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import * as TypeMoq from "typemoq"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import Id3v2Tag from "../../src/id3v2/id3v2Tag"; +import id3v2Tag from "../../src/id3v2/id3v2Tag"; +import SyncData from "../../src/id3v2/syncData"; +import {ByteVector, StringType} from "../../src/byteVector"; +import {File, ReadStyle} from "../../src/file"; +import {Id3v2TagHeaderFlags} from "../../src/id3v2/id3v2TagHeader"; +import PlayCountFrame from "../../src/id3v2/frames/playCountFrame"; +import UniqueFileIdentifierFrame from "../../src/id3v2/frames/uniqueFileIdentifierFrame"; +import UnknownFrame from "../../src/id3v2/frames/unknownFrame"; +import {FrameClassType} from "../../src/id3v2/frames/frame"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; + +// Setup Chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +function getTestTagHeader(version: number, flags: Id3v2TagHeaderFlags, tagSize: number): ByteVector { + return ByteVector.concatenate( + ByteVector.fromString("ID3", StringType.Latin1), + version, 0x00, + flags, + SyncData.fromUint(tagSize) + ); +} + +@suite(slow(1000), timeout(3000)) +class Id3v2_Tag_ConstructorTests { + @test + public fromData_falsyData() { + // Act / Assert + assert.throws(() => { Id3v2Tag.fromData(undefined); }); + assert.throws(() => { Id3v2Tag.fromData(null); }); + } + + @test + public fromData_dataAbsolutelyTooShort() { + // Arrange + const data = ByteVector.fromSize(5); + + // Act / Assert + assert.throws(() => { Id3v2Tag.fromData(data); }); + } + + @test + public fromData_emptyTag() { + // Arrange + const data = getTestTagHeader(4, Id3v2TagHeaderFlags.None, 0); + + // Act + const output = Id3v2Tag.fromData(data); + + // Assert + assert.isOk(output); + } + + @test + public fromData_dataTooShortForTagLength() { + // Arrange + const data = ByteVector.concatenate( + getTestTagHeader(4, Id3v2TagHeaderFlags.None, 5), + 0x00, 0x00 + ); + + // Act / Assert + assert.throws(() => { Id3v2Tag.fromData(data); }); + } + + @test + public fromData_v4Tag() { + // Arrange + const frame1 = PlayCountFrame.fromEmpty().render(4); + const frame2 = UniqueFileIdentifierFrame.fromData("foo", ByteVector.fromString("bar")).render(4); + const emptyFrame = UnknownFrame.fromData(FrameIdentifiers.RVRB, ByteVector.empty()).render(4); + const data = ByteVector.concatenate( + getTestTagHeader(4, Id3v2TagHeaderFlags.None, frame1.length + frame2.length + emptyFrame.length + 5), + frame1, + frame2, + emptyFrame, + 0x00, 0x00, 0x00, 0x00, 0x00 // Padding at end of tag + ); + + // Act + const tag = id3v2Tag.fromData(data); + + // Assert + assert.isOk(tag); + + let frame1Found = false; + let frame2Found = false; + let emptyFrameFound = false; + for (const f of tag.frames) { + switch (f.frameClassType) { + case FrameClassType.PlayCountFrame: + assert.isFalse(frame1Found); + frame1Found = true; + break; + case FrameClassType.UniqueFileIdentifierFrame: + assert.isFalse(frame2Found); + frame2Found = true; + break; + case FrameClassType.UnknownFrame: + emptyFrameFound = true; + break; + default: + assert.fail(f.frameClassType, undefined, "Unexpected frame found"); + } + } + assert.isTrue(frame1Found); + assert.isTrue(frame2Found); + assert.isFalse(emptyFrameFound); + } + + // @test + // public fromData_fullyUnsynchronized() { + // // Arrange + // const data = ByteVector.concatenate( + // getTestTagHeader(3, Id3v2TagHeaderFlags.Unsynchronication, ), + // + // ) + // } + + @test + public fromFile_falsyFile() { + // Act / Assert + assert.throws(() => { Id3v2Tag.fromFile(undefined, 0, ReadStyle.None); }); + assert.throws(() => { Id3v2Tag.fromFile(null, 0, ReadStyle.None); }); + } + + @test + public fromFile_positionTooFar() { + // Arrange + const mockFile = TypeMoq.Mock.ofType(); + mockFile.setup((f: File) => f.length).returns(() => 14); + + // Act / Assert + assert.throws(() => { Id3v2Tag.fromFile(mockFile.object, 5, ReadStyle.None); }); + } +} \ No newline at end of file From 4489a35cd9e72494ab99176372298a9f87e72b7d Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Fri, 28 Feb 2020 22:28:09 -0500 Subject: [PATCH 51/71] More tests for Id3v2 tag parsing --- src/id3v2/frameIdentifiers.ts | 4 +- src/id3v2/frames/frameFactory.ts | 184 ++++++++++++++++--------------- src/id3v2/id3v2ExtendedHeader.ts | 2 + src/id3v2/id3v2Tag.ts | 160 +++++++++++---------------- test/id3v2/id3v2TagTests.ts | 31 ++++-- 5 files changed, 191 insertions(+), 190 deletions(-) diff --git a/src/id3v2/frameIdentifiers.ts b/src/id3v2/frameIdentifiers.ts index 210789bc..6a52abc8 100644 --- a/src/id3v2/frameIdentifiers.ts +++ b/src/id3v2/frameIdentifiers.ts @@ -1,5 +1,6 @@ import {ByteVector, StringType} from "../byteVector"; import {Guards} from "../utils"; +import {Frame} from "./frames/frame"; /** * @summary Represents the identifier of a frame, depending on the version this may be 3 or 4 @@ -99,7 +100,7 @@ const uniqueFrameIdentifiers: {[key: string]: FrameIdentifier} = { TDEN: new FrameIdentifier("TDEN", undefined, undefined), // Encoding time TDLY: new FrameIdentifier("TDLY", "TDLY", "TDY"), // Playlist delay TDOR: new FrameIdentifier("TDOR", "TORY", "TOR"), // Original release time - TDRC: new FrameIdentifier("TDRC", "TYER", "TYE"), // Recording time (v2.4/v2.3) / Year (v2.2) + TDRC: new FrameIdentifier("TDRC", undefined, undefined), // Recording time TDRL: new FrameIdentifier("TDRL", undefined, undefined), // Release time TDTG: new FrameIdentifier("TDTG", undefined, undefined), // Tagging time TENC: new FrameIdentifier("TENC", "TENC", "TEN"), // Encoded by @@ -140,6 +141,7 @@ const uniqueFrameIdentifiers: {[key: string]: FrameIdentifier} = { TSSE: new FrameIdentifier("TSSE", "TSSE", "TSS"), // Software/hardware and setting used for encoding TSST: new FrameIdentifier("TSST", undefined, undefined), // Set subtitle TXXX: new FrameIdentifier("TXXX", "TXXX", "TXX"), // User defined text information frame + TYER: new FrameIdentifier(undefined, "TYER", "TYE"), // Year UFID: new FrameIdentifier("UFID", "UFID", "UFI"), // Unique file identifer USER: new FrameIdentifier("USER", "USER", undefined), // Terms of use USLT: new FrameIdentifier("USLT", "USLT", "ULT"), // Unsynchronised lyric/text transcription diff --git a/src/id3v2/frames/frameFactory.ts b/src/id3v2/frames/frameFactory.ts index 249db91a..bc577df3 100644 --- a/src/id3v2/frames/frameFactory.ts +++ b/src/id3v2/frames/frameFactory.ts @@ -10,7 +10,7 @@ import UniqueFileIdentifierFrame from "./uniqueFileIdentifierFrame"; import UnknownFrame from "./unknownFrame"; import UnsynchronizedLyricsFrame from "./unsynchronizedLyricsFrame"; import {ByteVector} from "../../byteVector"; -import {NotImplementedError} from "../../errors"; +import {CorruptFileError, NotImplementedError} from "../../errors"; import {EventTimeCodeFrame} from "./eventTimeCodeFrame"; import {File} from "../../file"; import {Frame} from "./frame"; @@ -118,96 +118,108 @@ export default { throw new NotImplementedError("Encryption is not supported"); } - // Try to find a custom creator - for (const creator of customFrameCreators) { - // @TODO: If we're reading from a file, data will only ever contain the header - const frame = creator(data, position, header, version); - if (frame) { - return { - frame: frame, - offset: offset - }; + try { + // Try to find a custom creator + for (const creator of customFrameCreators) { + // @TODO: If we're reading from a file, data will only ever contain the header + const frame = creator(data, position, header, version); + if (frame) { + return { + frame: frame, + offset: offset + }; + } } - } - // This is where things get necessarily nasty. Here we determine which frame subclass (or - // if none is found, simply a frame) based on the frame ID. Since there are a lot of - // possibilities, that means a lot of if statements. - - // Lazy object loading handling - if (file) { - // Attached picture (frames 4.14) - // General encapsulated object (frames 4.15) - if (header.frameId === FrameIdentifiers.APIC || header.frameId === FrameIdentifiers.GEOB) { - const picture = PictureLazy.fromFile(file.fileAbstraction, filePosition, offset - filePosition); - return { - frame: AttachmentFrame.fromPicture(picture), - offset: offset - }; + // This is where things get necessarily nasty. Here we determine which frame subclass (or + // if none is found, simply a frame) based on the frame ID. Since there are a lot of + // possibilities, that means a lot of if statements. + + // Lazy object loading handling + if (file) { + // Attached picture (frames 4.14) + // General encapsulated object (frames 4.15) + if (header.frameId === FrameIdentifiers.APIC || header.frameId === FrameIdentifiers.GEOB) { + const picture = PictureLazy.fromFile(file.fileAbstraction, filePosition, offset - filePosition); + return { + frame: AttachmentFrame.fromPicture(picture), + offset: offset + }; + } + + // Read remaining part of the frame for the non lazy Frame + file.seek(filePosition); + data.addByteVector(file.readBlock(offset - filePosition)); } - // Read remaining part of the frame for the non lazy Frame - file.seek(filePosition); - data.addByteVector(file.readBlock(offset - filePosition)); - } + let func; + if (header.frameId === FrameIdentifiers.TXXX) { + // User text identification frame + func = UserTextInformationFrame.fromOffsetRawData; + } else if (header.frameId.isTextFrame) { + // Text identifiacation frame (frames 4.2) Starts with T + func = TextInformationFrame.fromOffsetRawData; + } else if (header.frameId === FrameIdentifiers.UFID) { + // Unique file identifier (frames 4.1) + func = UniqueFileIdentifierFrame.fromOffsetRawData; + } else if (header.frameId === FrameIdentifiers.MCDI) { + // Music CD identifier (frames 4.5) + func = MusicCdIdentifierFrame.fromOffsetRawData; + } else if (header.frameId === FrameIdentifiers.USLT) { + // Unsynchronized lyrics (frames 4.8) + func = UnsynchronizedLyricsFrame.fromOffsetRawData; + } else if (header.frameId === FrameIdentifiers.SYLT) { + // Synchronized lyrics (frames 4.8) + func = SynchronizedLyricsFrame.fromOffsetRawData; + } else if (header.frameId === FrameIdentifiers.COMM) { + // Comments (frames 4.10) + func = CommentsFrame.fromOffsetRawData; + } else if (header.frameId === FrameIdentifiers.RVA2) { + // Relative volume adjustment (frames 4.11) + func = RelativeVolumeFrame.fromOffsetRawData; + } else if (header.frameId === FrameIdentifiers.APIC || header.frameId === FrameIdentifiers.GEOB) { + // Attached picture (frames 4.14) + func = AttachmentFrame.fromOffsetRawData; + } else if (header.frameId === FrameIdentifiers.PCNT) { + // Play count (frames 4.16) + func = PlayCountFrame.fromOffsetRawData; + } else if (header.frameId === FrameIdentifiers.POPM) { + // Popularimeter (frames 4.17) + func = PopularimeterFrame.fromOffsetRawData; + } else if (header.frameId === FrameIdentifiers.USER) { + // Terms of Use (frames 4.22) + func = TermsOfUseFrame.fromOffsetRawData; + } else if (header.frameId === FrameIdentifiers.PRIV) { + // Private (frames 4.27) + func = PrivateFrame.fromOffsetRawData; + } else if (header.frameId === FrameIdentifiers.WXXX) { + // User URL link + func = UserUrlLinkFrame.fromOffsetRawData; + } else if (header.frameId.isUrlFrame) { + // URL link (frame 4.3.1) starts with 'W' + func = UrlLinkFrame.fromOffsetRawData; + } else if (header.frameId === FrameIdentifiers.ETCO) { + // Event timing codes (frames 4.6) + func = EventTimeCodeFrame.fromOffsetRawData; + } else { + // Return unknown + func = UnknownFrame.fromOffsetRawData; + } - let func; - if (header.frameId === FrameIdentifiers.TXXX) { - // User text identification frame - func = UserTextInformationFrame.fromOffsetRawData; - } else if (header.frameId.isTextFrame) { - // Text identifiacation frame (frames 4.2) Starts with T - func = TextInformationFrame.fromOffsetRawData; - } else if (header.frameId === FrameIdentifiers.UFID) { - // Unique file identifier (frames 4.1) - func = UniqueFileIdentifierFrame.fromOffsetRawData; - } else if (header.frameId === FrameIdentifiers.MCDI) { - // Music CD identifier (frames 4.5) - func = MusicCdIdentifierFrame.fromOffsetRawData; - } else if (header.frameId === FrameIdentifiers.USLT) { - // Unsynchronized lyrics (frames 4.8) - func = UnsynchronizedLyricsFrame.fromOffsetRawData; - } else if (header.frameId === FrameIdentifiers.SYLT) { - // Synchronized lyrics (frames 4.8) - func = SynchronizedLyricsFrame.fromOffsetRawData; - } else if (header.frameId === FrameIdentifiers.COMM) { - // Comments (frames 4.10) - func = CommentsFrame.fromOffsetRawData; - } else if (header.frameId === FrameIdentifiers.RVA2) { - // Relative volume adjustment (frames 4.11) - func = RelativeVolumeFrame.fromOffsetRawData; - } else if (header.frameId === FrameIdentifiers.APIC || header.frameId === FrameIdentifiers.GEOB) { - // Attached picture (frames 4.14) - func = AttachmentFrame.fromOffsetRawData; - } else if (header.frameId === FrameIdentifiers.PCNT) { - // Play count (frames 4.16) - func = PlayCountFrame.fromOffsetRawData; - } else if (header.frameId === FrameIdentifiers.POPM) { - // Popularimeter (frames 4.17) - func = PopularimeterFrame.fromOffsetRawData; - } else if (header.frameId === FrameIdentifiers.USER) { - // Terms of Use (frames 4.22) - func = TermsOfUseFrame.fromOffsetRawData; - } else if (header.frameId === FrameIdentifiers.PRIV) { - // Private (frames 4.27) - func = PrivateFrame.fromOffsetRawData; - } else if (header.frameId === FrameIdentifiers.WXXX) { - // User URL link - func = UserUrlLinkFrame.fromOffsetRawData; - } else if (header.frameId.isUrlFrame) { - // URL link (frame 4.3.1) starts with 'W' - func = UrlLinkFrame.fromOffsetRawData; - } else if (header.frameId === FrameIdentifiers.ETCO) { - // Event timing codes (frames 4.6) - func = EventTimeCodeFrame.fromOffsetRawData; - } else { - // Return unknown - func = UnknownFrame.fromOffsetRawData; - } + return { + frame: func(data, position, header, version), + offset: offset + }; + } catch (e) { + if (CorruptFileError.errorIs(e) || NotImplementedError.errorIs(e)) { + throw e; + } - return { - frame: func(data, position, header, version), - offset: offset - }; + // Other exceptions will just mean we ignore the frame + return { + frame: undefined, + offset: offset + }; + } } }; diff --git a/src/id3v2/id3v2ExtendedHeader.ts b/src/id3v2/id3v2ExtendedHeader.ts index 4f3ec908..ed2753aa 100644 --- a/src/id3v2/id3v2ExtendedHeader.ts +++ b/src/id3v2/id3v2ExtendedHeader.ts @@ -36,5 +36,7 @@ export default class Id3v2ExtendedHeader { protected parse(data: ByteVector, version: number): void { this._size = (version === 3 ? 4 : 0) + SyncData.toUint(data.mid(0, 4)); + + // TODO: Are we going to actually support any of the flags? } } diff --git a/src/id3v2/id3v2Tag.ts b/src/id3v2/id3v2Tag.ts index f9d3008e..d48ccde2 100644 --- a/src/id3v2/id3v2Tag.ts +++ b/src/id3v2/id3v2Tag.ts @@ -14,7 +14,7 @@ import {CorruptFileError} from "../errors"; import {File, FileAccessMode, ReadStyle} from "../file"; import {Frame, FrameClassType} from "./frames/frame"; import {FrameIdentifier, FrameIdentifiers} from "./frameIdentifiers"; -import {Id3v2FrameFlags, Id3v2FrameHeader} from "./frames/frameHeader"; +import {Id3v2FrameFlags} from "./frames/frameHeader"; import {Id3v2TagHeader, Id3v2TagHeaderFlags} from "./id3v2TagHeader"; import {IPicture} from "../picture"; import {Tag, TagTypes} from "../tag"; @@ -339,29 +339,66 @@ export default class Id3v2Tag extends Tag { this.setTextFrame(FrameIdentifiers.TCON, ...value); } - /** @inheritDoc via TDRC frame */ + /** + * @inheritDoc + * If a TDRC frame exists, the year will be read from that. If a TDRC frame doesn't exist and a + * TYER or TYE frame exists, the year will be read from that. Failing both cases, 0 will be + * returned. + */ get year(): number { - const text = this.getTextAsString(FrameIdentifiers.TDRC); - if (!text || text.length < 4) { return 0; } + // @TODO: get it from TDRC if it exists, get it from TYER failing that + // Case 1: We have a TDRC frame (v2.4), preferentially use that + const tdrcText = this.getTextAsString(FrameIdentifiers.TDRC); + if (tdrcText && tdrcText.length > 4) { + // @TODO: Check places where we use this pattern... .parseInt doesn't parse the whole string if it started + // with good data + return Number.parseInt(tdrcText.substr(0, 4), 10); + } - const year = Number.parseInt(text.substr(0, 4), 10); - // @TODO: Check places where we use this pattern... .parseInt doesn't parse the whole string if it started with - // good data - if (Number.isNaN(year) || year < 0) { - return 0; + // Case 2: We have a TYER frame (v2.3/v2.2) + const tyerText = this.getTextAsString(FrameIdentifiers.TYER); + if (tyerText && tyerText.length > 4) { + // @TODO: Check places where we use this pattern... .parseInt doesn't parse the whole string if it started + // with good data + return Number.parseInt(tdrcText.substr(0, 4), 10); } - return year; + // Case 3: Neither, return 0 + return 0; } /** - * @inheritDoc via TDRC frame + * @inheritDoc * NOTE: values >9999will remove the frame */ set year(value: number) { Guards.uint(value, "value"); + + // Case 0: Frame should be deleted if (value > 9999) { value = 0; } + + // Case 1: We have a TDRC frame (v2.4), preferentially replace contents with year + const tdrcFrames = this.getFramesByIdentifier( + FrameClassType.TextInformationFrame, + FrameIdentifiers.TDRC + ); + if (tdrcFrames.length > 0) { + this.setNumberFrame(FrameIdentifiers.TDRC, value, 0); + return; + } + + // Case 2: We have a TYER/TYE frame (v2.3/v2.2) + const tyerFrames = this.getFramesByIdentifier( + FrameClassType.TextInformationFrame, + FrameIdentifiers.TYER + ); + if (tyerFrames.length > 0) { + this.setNumberFrame(FrameIdentifiers.TYER, value, 0); + return; + } + + // Case 3: We have neither type of frame, create a TDRC frame this.setNumberFrame(FrameIdentifiers.TDRC, value, 0); } @@ -623,9 +660,6 @@ export default class Id3v2Tag extends Tag { /** @inheritDoc */ public get isEmpty(): boolean { return this._frameList.length === 0; } - - // #endregion - // #endregion // #region Public Methods @@ -996,94 +1030,30 @@ export default class Id3v2Tag extends Tag { } } - // Parse the frames. TDRC, TDAT, and TIME will be needed for post-processing, so check for - // for them as they are loaded - let tdrc: TextInformationFrame; - let tyer: TextInformationFrame; - let tdat: TextInformationFrame; - let time: TextInformationFrame; - + // Parse the frames while (frameDataPosition < frameDataEndPosition) { - let frame: Frame; - - try { - const frameRead = FrameFactory.createFrame( - data, - file, - frameDataPosition, - this._header.majorVersion, - fullTagUnsync - ); - - // If the frame factory returned undefined, that means we've hit the end of frames - if (!frameRead) { - break; - } - - // We found a frame, deconstruct the read result - frame = frameRead.frame; - frameDataPosition = frameRead.offset; - } catch (e) { - if (!e.hasOwnProperty("isNotImplementedError") && !e.hasOwnProperty("isCorruptFileError")) { - throw e; - } else { - // @TODO: In this case, we're actually entering an infinite loop. Add a catch for exceptions in the frame factory - continue; - } + const frameRead = FrameFactory.createFrame( + data, + file, + frameDataPosition, + this._header.majorVersion, + fullTagUnsync + ); + + // If the frame factory returned undefined, that means we've hit the end of frames + if (!frameRead) { + break; } + // We found a frame, deconstruct the read result + const frame = frameRead.frame; + frameDataPosition = frameRead.offset; + // Only add frames that contain data - if (frame.size === 0) { + if (!frame || frame.size === 0) { continue; } - this.addFrame(frame); - - // If the tag is version 4, no post-processing needed - if (this._header.majorVersion === 4) { - continue; - } - - // Load up the first instance of each for post-processing - if (!tdrc && frame.frameId === FrameIdentifiers.TDRC) { - tdrc = frame; - } else if (!tyer && frame.frameId === FrameIdentifiers.TYER) { - tyer = frame; - } else if (!tdat && frame.frameId === FrameIdentifiers.TDAT) { - tdat = frame; - } else if (!time && frame.frameId === FrameIdentifiers.TIME) { - time = frame; - } - } - - // Try to fill out the data/time of the TDRC frame. Can't do that if no TDRC frame exists, - // or if there is no TDAT frame, or if TDRC already has the date. - if (!tdrc || !tdat || tdrc.toString().length !== 4) { - return; - } - - // Start with the year already in TDRC, then add the TDAT and TIME if available - let tdrcText = tdrc.toString(); - - // Add the data - if (tdat) { - const tdatText = tdat.toString(); - if (tdatText.length === 4) { - tdrcText += `-${tdatText.substr(0, 2)}-${tdatText.substr(2, 2)}`; - - // Add the time - if (time) { - const timeText = time.toString(); - - if (timeText.length === 4) { - tdrcText += `T${timeText.substr(0, 2)}:${timeText.substr(2, 2)}`; - } - - this.removeFrames(FrameIdentifiers.TDAT); - } - } - - tdrc.text = [tdrcText.toString()]; } } diff --git a/test/id3v2/id3v2TagTests.ts b/test/id3v2/id3v2TagTests.ts index 9d00879b..b4f44964 100644 --- a/test/id3v2/id3v2TagTests.ts +++ b/test/id3v2/id3v2TagTests.ts @@ -115,14 +115,29 @@ class Id3v2_Tag_ConstructorTests { assert.isFalse(emptyFrameFound); } - // @test - // public fromData_fullyUnsynchronized() { - // // Arrange - // const data = ByteVector.concatenate( - // getTestTagHeader(3, Id3v2TagHeaderFlags.Unsynchronication, ), - // - // ) - // } + @test + public fromData_extendedHeader() { + // Arrange + const frame1 = PlayCountFrame.fromEmpty().render(4); + const data = ByteVector.concatenate( + getTestTagHeader(4, Id3v2TagHeaderFlags.ExtendedHeader, frame1.length + 10), + SyncData.fromUint(10), + 0x01, + 0x00, + 0x00, 0x00, 0x00, 0x00, + frame1 + ); + + // Act + const tag = id3v2Tag.fromData(data); + + // Assert + assert.isOk(tag); + + // - Right frames + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.PlayCountFrame); + } @test public fromFile_falsyFile() { From 431dd79ae2a2b57b1da3a6ea63967d05cce07a51 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 29 Feb 2020 14:43:39 -0500 Subject: [PATCH 52/71] Finishing up the Id3v2 tag constructor tests --- src/id3v2/id3v2Tag.ts | 5 -- src/id3v2/syncData.ts | 4 +- test/id3v2/id3v2TagTests.ts | 92 +++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/id3v2/id3v2Tag.ts b/src/id3v2/id3v2Tag.ts index d48ccde2..084b570b 100644 --- a/src/id3v2/id3v2Tag.ts +++ b/src/id3v2/id3v2Tag.ts @@ -1059,11 +1059,6 @@ export default class Id3v2Tag extends Tag { protected read(file: File, position: number, style: ReadStyle): void { file.mode = FileAccessMode.Read; - - if (position > file.length - Id3v2Settings.headerSize) { - throw new Error("Argument out of range: position must be less than the length of the file"); - } - file.seek(position); this._header = new Id3v2TagHeader(file.readBlock(Id3v2Settings.headerSize)); diff --git a/src/id3v2/syncData.ts b/src/id3v2/syncData.ts index e17dd095..47650b20 100644 --- a/src/id3v2/syncData.ts +++ b/src/id3v2/syncData.ts @@ -42,12 +42,12 @@ export default { data.set(j, data.get(i)); } - i += (data.get(i) === 0xFF && data.get(i + 1) === 0) ? 2 : 1; + i += (data.get(i) === 0xFF && data.get(i + 1) === 0x00) ? 2 : 1; j ++; } if (i < data.length) { - data.set(data.get(j++), data.get(i + 1)); + data.set(j++, data.get(i++)); } data.resize(j); diff --git a/test/id3v2/id3v2TagTests.ts b/test/id3v2/id3v2TagTests.ts index b4f44964..7f2dce89 100644 --- a/test/id3v2/id3v2TagTests.ts +++ b/test/id3v2/id3v2TagTests.ts @@ -6,6 +6,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import Id3v2Tag from "../../src/id3v2/id3v2Tag"; import id3v2Tag from "../../src/id3v2/id3v2Tag"; import SyncData from "../../src/id3v2/syncData"; +import TestFile from "../utilities/testFile"; import {ByteVector, StringType} from "../../src/byteVector"; import {File, ReadStyle} from "../../src/file"; import {Id3v2TagHeaderFlags} from "../../src/id3v2/id3v2TagHeader"; @@ -139,6 +140,27 @@ class Id3v2_Tag_ConstructorTests { assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.PlayCountFrame); } + @test + public fromData_needsResync() { + // Arrange + const frame1 = PlayCountFrame.fromEmpty().render(4); + const data = ByteVector.concatenate( + getTestTagHeader(3, Id3v2TagHeaderFlags.Unsynchronication, frame1.length), + frame1 + ); + SyncData.unsyncByteVector(data); + + // Act + const tag = id3v2Tag.fromData(data); + + // Assert + assert.isOk(tag); + + // - Right frames + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.PlayCountFrame); + } + @test public fromFile_falsyFile() { // Act / Assert @@ -146,6 +168,17 @@ class Id3v2_Tag_ConstructorTests { assert.throws(() => { Id3v2Tag.fromFile(null, 0, ReadStyle.None); }); } + @test + public fromFile_invalidPosition() { + // Arrange + const file = TestFile.getFile(ByteVector.empty()); + + // Act / Assert + assert.throws(() => { Id3v2Tag.fromFile(file, -1, ReadStyle.None); }); + assert.throws(() => { Id3v2Tag.fromFile(file, 1.23, ReadStyle.None); }); + assert.throws(() => { Id3v2Tag.fromFile(file, Number.MAX_SAFE_INTEGER + 1, ReadStyle.None); }); + } + @test public fromFile_positionTooFar() { // Arrange @@ -155,4 +188,63 @@ class Id3v2_Tag_ConstructorTests { // Act / Assert assert.throws(() => { Id3v2Tag.fromFile(mockFile.object, 5, ReadStyle.None); }); } + + @test + public fromFile_emptyTag() { + // Arrange + const data = getTestTagHeader(4, Id3v2TagHeaderFlags.None, 0); + const file = TestFile.getFile(data); + + // Act + const output = Id3v2Tag.fromFile(file, 0, ReadStyle.None); + + // Assert + assert.isOk(output); + } + + @test + public fromFile_v4Tag() { + const frame1 = PlayCountFrame.fromEmpty().render(4); + const frame2 = UniqueFileIdentifierFrame.fromData("foo", ByteVector.fromString("bar")).render(4); + const emptyFrame = UnknownFrame.fromData(FrameIdentifiers.RVRB, ByteVector.empty()).render(4); + const data = ByteVector.concatenate( + 0x00, 0x00, + getTestTagHeader(4, Id3v2TagHeaderFlags.None, frame1.length + frame2.length + emptyFrame.length + 5), + frame1, + frame2, + emptyFrame, + 0x00, 0x00, 0x00, 0x00, 0x00 // Padding at end of tag + ); + const file = TestFile.getFile(data); + + // Act + const tag = Id3v2Tag.fromFile(file, 2, ReadStyle.None); + + // Assert + assert.isOk(tag); + + let frame1Found = false; + let frame2Found = false; + let emptyFrameFound = false; + for (const f of tag.frames) { + switch (f.frameClassType) { + case FrameClassType.PlayCountFrame: + assert.isFalse(frame1Found); + frame1Found = true; + break; + case FrameClassType.UniqueFileIdentifierFrame: + assert.isFalse(frame2Found); + frame2Found = true; + break; + case FrameClassType.UnknownFrame: + emptyFrameFound = true; + break; + default: + assert.fail(f.frameClassType, undefined, "Unexpected frame found"); + } + } + assert.isTrue(frame1Found); + assert.isTrue(frame2Found); + assert.isFalse(emptyFrameFound); + } } \ No newline at end of file From 5764470483bc5ffd5450cadfb15d4bbeea8c3016 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 29 Feb 2020 16:57:22 -0500 Subject: [PATCH 53/71] Moving a couple files around, adding first round of property tests for id3v2Tag --- src/id3v2/frameIdentifiers.ts | 4 + src/id3v2/id3v2Settings.ts | 2 + src/id3v2/id3v2Tag.ts | 18 +- test/id3v2/attachmentsFrameTests.ts | 36 +-- test/id3v2/commentsFrameTests.ts | 26 +- test/id3v2/eventTimeCodeFrameTests.ts | 16 +- test/id3v2/id3v2TagTests.ts | 226 +++++++++++++++++- test/id3v2/musicCdIdentifierFrameTests.ts | 4 +- test/id3v2/playCountFrameTests.ts | 10 +- test/id3v2/popularimeterFrameTests.ts | 24 +- test/id3v2/privateFrameTests.ts | 4 +- test/id3v2/relativeVolumeFrameTests.ts | 8 +- test/id3v2/synchronizedLyricsFrameTests.ts | 24 +- test/id3v2/termsOfUseFrameTests.ts | 20 +- test/id3v2/textInformationFrameTests.ts | 8 +- test/id3v2/uniqueFileIdentifierFrameTests.ts | 6 +- test/id3v2/unsynchronizedLyricsFrameTests.ts | 26 +- test/id3v2/urlLinkFrameTests.ts | 6 +- .../propertyTests.ts} | 2 +- 19 files changed, 348 insertions(+), 122 deletions(-) rename test/{id3v2/framePropertyTests.ts => utilities/propertyTests.ts} (94%) diff --git a/src/id3v2/frameIdentifiers.ts b/src/id3v2/frameIdentifiers.ts index 6a52abc8..138f2ee9 100644 --- a/src/id3v2/frameIdentifiers.ts +++ b/src/id3v2/frameIdentifiers.ts @@ -134,7 +134,9 @@ const uniqueFrameIdentifiers: {[key: string]: FrameIdentifier} = { TRSN: new FrameIdentifier("TRSN", "TRSN", undefined), // Internet radio station name TRSO: new FrameIdentifier("TRSO", "TRSO", undefined), // Internet radio station owner TSIZ: new FrameIdentifier(undefined, "TSIZ", "TSI"), // Size + TSO2: new FrameIdentifier("TSO2", "TSO2", undefined), // iTunes only "Album Artist Sort" TSOA: new FrameIdentifier("TSOA", undefined, undefined), // Album sort order + TSOC: new FrameIdentifier("TSOC", "TSOC", undefined), // iTunes only "Composer Sort" TSOP: new FrameIdentifier("TSOP", undefined, undefined), // Performer sort order TSOT: new FrameIdentifier("TSOT", undefined, undefined), // Title sort order TSRC: new FrameIdentifier("TSRC", "TSRC", "TRC"), // ISRC (International standard recording code) @@ -276,7 +278,9 @@ export const FrameIdentifiers: {[key: string]: FrameIdentifier} = { TRSO: uniqueFrameIdentifiers.TRSO, TSI: uniqueFrameIdentifiers.TSIZ, TSIZ: uniqueFrameIdentifiers.TSIZ, + TSO2: uniqueFrameIdentifiers.TSO2, TSOA: uniqueFrameIdentifiers.TSOA, + TSOC: uniqueFrameIdentifiers.TSOC, TSOP: uniqueFrameIdentifiers.TSOP, TSOT: uniqueFrameIdentifiers.TSOT, TSRC: uniqueFrameIdentifiers.TSRC, diff --git a/src/id3v2/id3v2Settings.ts b/src/id3v2/id3v2Settings.ts index 0ceec5ce..25baa394 100644 --- a/src/id3v2/id3v2Settings.ts +++ b/src/id3v2/id3v2Settings.ts @@ -91,4 +91,6 @@ export default class Id3v2Settings { * @param value Whether or not to use genres with numeric values when values when possible */ public static set useNumericGenres(value: boolean) { this._useNumericGenres = value; } + + // @TODO: Add flag for disabling iTunes-only frames } diff --git a/src/id3v2/id3v2Tag.ts b/src/id3v2/id3v2Tag.ts index 084b570b..0b476ece 100644 --- a/src/id3v2/id3v2Tag.ts +++ b/src/id3v2/id3v2Tag.ts @@ -115,7 +115,7 @@ export default class Id3v2Tag extends Tag { public get flags(): Id3v2TagHeaderFlags { return this._header.flags; } /** * Sets the header flags applied to the current instance - * @param value Bitwise combined {@see Id3v2TagHeaderFlags} value contiaining flags applied to the + * @param value Bitwise combined {@see Id3v2TagHeaderFlags} value containing flags applied to the * current instance. */ public set flags(value: Id3v2TagHeaderFlags) { this._header.flags = value; } @@ -132,7 +132,7 @@ export default class Id3v2Tag extends Tag { */ public get isCompilation(): boolean { const val = this.getTextAsString(FrameIdentifiers.TCMP); - return val && val !== "0"; + return !!val && val !== "0"; } /** * Gets whether or not the album described by the current instance is a compilation. @@ -168,13 +168,11 @@ export default class Id3v2Tag extends Tag { public get tagTypes(): TagTypes { return TagTypes.Id3v2; } /** - * @inheritDoc - * From TIT2 frame + * @inheritDoc via TIT2 frame */ public get title(): string { return this.getTextAsString(FrameIdentifiers.TIT2); } /** - * @inheritDoc - * Stored in TIT2 frame + * @inheritDoc via TIT2 frame */ public set title(value: string) { this.setTextFrame(FrameIdentifiers.TIT2, value); } @@ -241,14 +239,14 @@ export default class Id3v2Tag extends Tag { set performersRole(value: string[]) { this._performersRole = value || []; } /** @inheritDoc via TSO2 frame */ - get albumArtists(): string[] { return this.getTextAsArray(FrameIdentifiers.TSO2); } + get albumArtists(): string[] { return this.getTextAsArray(FrameIdentifiers.TPE2); } /** @inheritDoc via TSO2 frame */ - set albumArtists(value: string[]) { this.setTextFrame(FrameIdentifiers.TSO2, ...value); } + set albumArtists(value: string[]) { this.setTextFrame(FrameIdentifiers.TPE2, ...value); } /** @inheritDoc via TPE2 frame */ - get albumArtistsSort(): string[] { return this.getTextAsArray(FrameIdentifiers.TPE2); } + get albumArtistsSort(): string[] { return this.getTextAsArray(FrameIdentifiers.TSO2); } /** @inheritDoc via TPE2 frame */ - set albumArtistsSort(value: string[]) { this.setTextFrame(FrameIdentifiers.TPE2, ...value); } + set albumArtistsSort(value: string[]) { this.setTextFrame(FrameIdentifiers.TSO2, ...value); } /** @inheritDoc via TCOM frame */ get composers(): string[] { return this.getTextAsArray(FrameIdentifiers.TCOM); } diff --git a/test/id3v2/attachmentsFrameTests.ts b/test/id3v2/attachmentsFrameTests.ts index ad43c0a5..d4132760 100644 --- a/test/id3v2/attachmentsFrameTests.ts +++ b/test/id3v2/attachmentsFrameTests.ts @@ -11,7 +11,7 @@ import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; import {FrameIdentifier, FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; import {IPicture, PictureType} from "../../src/picture"; import Id3v2Settings from "../../src/id3v2/id3v2Settings"; -import FramePropertyTests from "./framePropertyTests"; +import PropertyTests from "../utilities/propertyTests"; // Setup chai Chai.use(ChaiAsPromised); @@ -282,9 +282,9 @@ class Id3v2_AttachmentFrame_PropertyTests { const set = (v: ByteVector) => { frame.data = v; }; // Act / Assert - FramePropertyTests.propertyRoundTrip(set, get, ByteVector.fromString("xyz", StringType.Latin1)); - FramePropertyTests.propertyNormalized(set, get, undefined, ByteVector.empty()); - FramePropertyTests.propertyNormalized(set, get, null, ByteVector.empty()); + PropertyTests.propertyRoundTrip(set, get, ByteVector.fromString("xyz", StringType.Latin1)); + PropertyTests.propertyNormalized(set, get, undefined, ByteVector.empty()); + PropertyTests.propertyNormalized(set, get, null, ByteVector.empty()); } @test @@ -295,9 +295,9 @@ class Id3v2_AttachmentFrame_PropertyTests { const set = (v: string) => { frame.description = v; }; // Act / Assert - FramePropertyTests.propertyRoundTrip(set, get, "its funky enough"); - FramePropertyTests.propertyNormalized(set, get, undefined, ""); - FramePropertyTests.propertyNormalized(set, get, null, ""); + PropertyTests.propertyRoundTrip(set, get, "its funky enough"); + PropertyTests.propertyNormalized(set, get, undefined, ""); + PropertyTests.propertyNormalized(set, get, null, ""); } @test @@ -308,9 +308,9 @@ class Id3v2_AttachmentFrame_PropertyTests { const set = (v: string) => { frame.filename = v; }; // Act / Assert - FramePropertyTests.propertyRoundTrip(set, get, "the choice is yours (revisited)"); - FramePropertyTests.propertyRoundTrip(set, get, undefined); - FramePropertyTests.propertyRoundTrip(set, get, null); + PropertyTests.propertyRoundTrip(set, get, "the choice is yours (revisited)"); + PropertyTests.propertyRoundTrip(set, get, undefined); + PropertyTests.propertyRoundTrip(set, get, null); } @test @@ -321,9 +321,9 @@ class Id3v2_AttachmentFrame_PropertyTests { const set = (v: string) => { frame.mimeType = v; }; // Act / Assert - FramePropertyTests.propertyRoundTrip(set, get, "chief rocka"); - FramePropertyTests.propertyNormalized(set, get, undefined, ""); - FramePropertyTests.propertyNormalized(set, get, null, ""); + PropertyTests.propertyRoundTrip(set, get, "chief rocka"); + PropertyTests.propertyNormalized(set, get, undefined, ""); + PropertyTests.propertyNormalized(set, get, null, ""); } @test @@ -332,7 +332,7 @@ class Id3v2_AttachmentFrame_PropertyTests { const frame = getTestFrame(); // Act / Assert - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF8 @@ -345,7 +345,7 @@ class Id3v2_AttachmentFrame_PropertyTests { const frame = getTestFrame(); // Act / Assert - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.type = v; }, () => frame.type, PictureType.BackCover @@ -359,7 +359,7 @@ class Id3v2_AttachmentFrame_PropertyTests { const frame = getTestFrame(); // Act / Assert - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.type = v; }, () => frame.type, PictureType.NotAPicture @@ -375,9 +375,9 @@ class Id3v2_AttachmentFrame_PropertyTests { const get = () => frame.type; // Act - FramePropertyTests.propertyRoundTrip(set, get, PictureType.NotAPicture); + PropertyTests.propertyRoundTrip(set, get, PictureType.NotAPicture); assert.strictEqual(frame.frameId, FrameIdentifiers.GEOB); - FramePropertyTests.propertyRoundTrip(set, get, PictureType.BackCover); + PropertyTests.propertyRoundTrip(set, get, PictureType.BackCover); assert.strictEqual(frame.frameId, FrameIdentifiers.APIC); } } diff --git a/test/id3v2/commentsFrameTests.ts b/test/id3v2/commentsFrameTests.ts index 8252782e..f7f62522 100644 --- a/test/id3v2/commentsFrameTests.ts +++ b/test/id3v2/commentsFrameTests.ts @@ -4,7 +4,7 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import CommentsFrame from "../../src/id3v2/frames/commentsFrame"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertyTests from "./framePropertyTests"; +import PropertyTests from "../utilities/propertyTests"; import Id3v2Settings from "../../src/id3v2/id3v2Settings"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; @@ -302,9 +302,9 @@ class Id3v2_CommentsFrame_PropertyTests { const set = (v: string) => { frame.description = v; }; const get = () => frame.description; - FramePropertyTests.propertyRoundTrip(set, get, "fux"); - FramePropertyTests.propertyNormalized(set, get, undefined, ""); - FramePropertyTests.propertyNormalized(set, get, null, ""); + PropertyTests.propertyRoundTrip(set, get, "fux"); + PropertyTests.propertyNormalized(set, get, undefined, ""); + PropertyTests.propertyNormalized(set, get, null, ""); } @test @@ -313,11 +313,11 @@ class Id3v2_CommentsFrame_PropertyTests { const set = (v: string) => { frame.language = v; }; const get = () => frame.language; - FramePropertyTests.propertyRoundTrip(set, get, "jpn"); - FramePropertyTests.propertyNormalized(set, get, undefined, "XXX"); - FramePropertyTests.propertyNormalized(set, get, null, "XXX"); - FramePropertyTests.propertyNormalized(set, get, "ab", "XXX"); - FramePropertyTests.propertyNormalized(set, get, "abcd", "abc"); + PropertyTests.propertyRoundTrip(set, get, "jpn"); + PropertyTests.propertyNormalized(set, get, undefined, "XXX"); + PropertyTests.propertyNormalized(set, get, null, "XXX"); + PropertyTests.propertyNormalized(set, get, "ab", "XXX"); + PropertyTests.propertyNormalized(set, get, "abcd", "abc"); } @test @@ -326,16 +326,16 @@ class Id3v2_CommentsFrame_PropertyTests { const set = (v: string) => { frame.text = v; }; const get = () => frame.text; - FramePropertyTests.propertyRoundTrip(set, get, "fux"); - FramePropertyTests.propertyNormalized(set, get, undefined, ""); - FramePropertyTests.propertyNormalized(set, get, null, ""); + PropertyTests.propertyRoundTrip(set, get, "fux"); + PropertyTests.propertyNormalized(set, get, undefined, ""); + PropertyTests.propertyNormalized(set, get, null, ""); } @test public textEncoding() { const frame = getTestFrame(); - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16 diff --git a/test/id3v2/eventTimeCodeFrameTests.ts b/test/id3v2/eventTimeCodeFrameTests.ts index a622f91d..69e3287f 100644 --- a/test/id3v2/eventTimeCodeFrameTests.ts +++ b/test/id3v2/eventTimeCodeFrameTests.ts @@ -3,14 +3,14 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertyTests from "./framePropertyTests"; +import PropertyTests from "../utilities/propertyTests"; import {ByteVector} from "../../src/byteVector"; import {EventTimeCode, EventTimeCodeFrame} from "../../src/id3v2/frames/eventTimeCodeFrame"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameFlags, Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; import {EventType, TimestampFormat} from "../../src/id3v2/utilTypes"; -import framePropertyTests from "./framePropertyTests"; +import framePropertyTests from "../utilities/propertyTests"; // Setup Chai Chai.use(ChaiAsPromised); @@ -61,7 +61,7 @@ class Id3v2_EventTimeCodeTests { const output = EventTimeCode.fromEmpty(); // Act/Assert - FramePropertyTests.propertyRoundTrip((v) => { output.time = v; }, () => output.time, 123); + PropertyTests.propertyRoundTrip((v) => { output.time = v; }, () => output.time, 123); } @test @@ -70,7 +70,7 @@ class Id3v2_EventTimeCodeTests { const output = EventTimeCode.fromEmpty(); // Act/Assert - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { output.eventType = v; }, () => output.eventType, EventType.IntroEnd @@ -221,10 +221,10 @@ class Id3v2_EventTimeCodeFrame_PropertyTests { const get = () => frame.events; // Act / Assert - FramePropertyTests.propertyRoundTrip(set, get, [new EventTimeCode(EventType.Profanity, 123)]); - FramePropertyTests.propertyRoundTrip(set, get, []); + PropertyTests.propertyRoundTrip(set, get, [new EventTimeCode(EventType.Profanity, 123)]); + PropertyTests.propertyRoundTrip(set, get, []); framePropertyTests.propertyNormalized(set, get, undefined, []); - FramePropertyTests.propertyNormalized(set, get, null, []); + PropertyTests.propertyNormalized(set, get, null, []); } @test @@ -233,7 +233,7 @@ class Id3v2_EventTimeCodeFrame_PropertyTests { const frame = EventTimeCodeFrame.fromTimestampFormat(TimestampFormat.AbsoluteMilliseconds); // Act / Assert - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.timestampFormat = v; }, () => frame.timestampFormat, TimestampFormat.AbsoluteMpegFrames diff --git a/test/id3v2/id3v2TagTests.ts b/test/id3v2/id3v2TagTests.ts index 7f2dce89..9fe76d7c 100644 --- a/test/id3v2/id3v2TagTests.ts +++ b/test/id3v2/id3v2TagTests.ts @@ -14,7 +14,10 @@ import PlayCountFrame from "../../src/id3v2/frames/playCountFrame"; import UniqueFileIdentifierFrame from "../../src/id3v2/frames/uniqueFileIdentifierFrame"; import UnknownFrame from "../../src/id3v2/frames/unknownFrame"; import {FrameClassType} from "../../src/id3v2/frames/frame"; -import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; +import {FrameIdentifier, FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; +import PropertyTests from "../utilities/propertyTests"; +import {TextInformationFrame, UserTextInformationFrame} from "../../src/id3v2/frames/textInformationFrame"; +import {TagTypes} from "../../src/tag"; // Setup Chai Chai.use(ChaiAsPromised); @@ -90,6 +93,7 @@ class Id3v2_Tag_ConstructorTests { // Assert assert.isOk(tag); + assert.strictEqual(tag.tagTypes, TagTypes.Id3v2); let frame1Found = false; let frame2Found = false; @@ -134,6 +138,7 @@ class Id3v2_Tag_ConstructorTests { // Assert assert.isOk(tag); + assert.strictEqual(tag.tagTypes, TagTypes.Id3v2); // - Right frames assert.strictEqual(tag.frames.length, 1); @@ -155,6 +160,7 @@ class Id3v2_Tag_ConstructorTests { // Assert assert.isOk(tag); + assert.strictEqual(tag.tagTypes, TagTypes.Id3v2); // - Right frames assert.strictEqual(tag.frames.length, 1); @@ -200,6 +206,7 @@ class Id3v2_Tag_ConstructorTests { // Assert assert.isOk(output); + assert.strictEqual(output.tagTypes, TagTypes.Id3v2); } @test @@ -222,6 +229,7 @@ class Id3v2_Tag_ConstructorTests { // Assert assert.isOk(tag); + assert.strictEqual(tag.tagTypes, TagTypes.Id3v2); let frame1Found = false; let frame2Found = false; @@ -247,4 +255,218 @@ class Id3v2_Tag_ConstructorTests { assert.isTrue(frame2Found); assert.isFalse(emptyFrameFound); } -} \ No newline at end of file +} + +@suite(slow(1000), timeout(3000)) +class Id3v2_Tag_PropertyTests { + @test + public flags() { + // Arrange + const tag = new Id3v2Tag(); + + // Act / Assert + PropertyTests.propertyRoundTrip( + (v) => { tag.flags = v; }, + () => tag.flags, + Id3v2TagHeaderFlags.ExperimentalIndicator + ); + } + + @test + public isCompilation() { + // Arrange + const tag = new Id3v2Tag(); + const get = () => tag.isCompilation; + const set = (v: boolean) => { tag.isCompilation = v; }; + + // Act / Assert + assert.isFalse(tag.isCompilation); + + PropertyTests.propertyRoundTrip(set, get, true); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TCMP); + assert.deepStrictEqual(( tag.frames[0]).text, ["1"]); + + PropertyTests.propertyRoundTrip(set, get, false); + assert.strictEqual(tag.frames.length, 0); + + tag.setTextFrame(FrameIdentifiers.TCMP, "0"); + assert.strictEqual(tag.isCompilation, false); + } + + @test + public version() { + // TODO: Need to figure out what to do if header doesn't exist + } + + @test + public title() { + this.testTextFrameProperty( + (t, v) => { t.title = v; }, + (t) => t.title, + FrameIdentifiers.TIT2 + ); + } + + @test + public titleSort() { + this.testTextFrameProperty( + (t, v) => { t.titleSort = v; }, + (t) => t.titleSort, + FrameIdentifiers.TSOT + ); + } + + @test + public subtitle() { + this.testTextFrameProperty( + (t, v) => { t.subtitle = v; }, + (t) => t.subtitle, + FrameIdentifiers.TIT3 + ); + } + + @test + public description() { + // Arrange + const tag = new Id3v2Tag(); + const set = (v: string) => { tag.description = v; }; + const get = () => tag.description; + + // Act / Assert + assert.isUndefined(tag.description); + + PropertyTests.propertyRoundTrip(set, get, "foo"); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.UserTextInformationFrame); + assert.strictEqual(( tag.frames[0]).description, "Description"); + assert.deepStrictEqual(( tag.frames[0]).text, ["foo"]); + + PropertyTests.propertyRoundTrip(set, get, undefined); + assert.strictEqual(tag.frames.length, 0); + } + + @test + public performers() { + this.testArrayTextFrameProperty( + (t, v) => { t.performers = v; }, + (t) => t.performers, + FrameIdentifiers.TPE1 + ); + } + + @test + public performersSort() { + this.testArrayTextFrameProperty( + (t, v) => { t.performersSort = v; }, + (t) => t.performersSort, + FrameIdentifiers.TSOP + ); + } + + @test + public performersRole() { + // TODO: Write test + } + + @test + public albumArtists() { + this.testArrayTextFrameProperty( + (t, v) => { t.albumArtists = v; }, + (t) => t.albumArtists, + FrameIdentifiers.TPE2 + ); + } + + @test + public albumArtistsSort() { + this.testArrayTextFrameProperty( + (t, v) => { t.albumArtistsSort = v; }, + (t) => t.albumArtistsSort, + FrameIdentifiers.TSO2 + ); + } + + @test + public composers() { + this.testArrayTextFrameProperty( + (t, v) => { t.composers = v; }, + (t) => t.composers, + FrameIdentifiers.TCOM + ); + } + + @test + public composersSort() { + this.testArrayTextFrameProperty( + (t, v) => { t.composersSort = v; }, + (t) => t.composersSort, + FrameIdentifiers.TSOC + ); + } + + @test + public album() { + this.testTextFrameProperty( + (t, v) => { t.album = v; }, + (t) => t.album, + FrameIdentifiers.TALB + ); + } + + @test + public albumSort() { + this.testTextFrameProperty( + (t, v) => { t.albumSort = v; }, + (t) => t.albumSort, + FrameIdentifiers.TSOA + ); + } + + private testTextFrameProperty( + set: (t: Id3v2Tag, v: string) => void, + get: (t: Id3v2Tag) => string, + fId: FrameIdentifier + ) { + // Arrange + const tag = new Id3v2Tag(); + const setProp = (v: string) => set(tag, v); + const getProp = () => get(tag); + + // Act / Assert + assert.isUndefined(getProp()); + + PropertyTests.propertyRoundTrip(setProp, getProp, "foo"); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, fId); + assert.deepStrictEqual(( tag.frames[0]).text, ["foo"]); + + PropertyTests.propertyRoundTrip(setProp, getProp, undefined); + assert.strictEqual(tag.frames.length, 0); + } + + private testArrayTextFrameProperty( + set: (t: Id3v2Tag, v: string[]) => void, + get: (t: Id3v2Tag) => string[], + fId: FrameIdentifier + ) { + // Arrange + const tag = new Id3v2Tag(); + const setProp = (v: string[]) => set(tag, v); + const getProp = () => get(tag); + + // Act / Assert + assert.deepStrictEqual(getProp(), []); + + PropertyTests.propertyRoundTrip(setProp, getProp, ["foo", "bar", "baz"]); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, fId); + assert.deepStrictEqual(( tag.frames[0]).text, ["foo", "bar", "baz"]); + + PropertyTests.propertyRoundTrip(setProp, getProp, []); + assert.strictEqual(tag.frames.length, 0); + } +} diff --git a/test/id3v2/musicCdIdentifierFrameTests.ts b/test/id3v2/musicCdIdentifierFrameTests.ts index 5652c956..a0483711 100644 --- a/test/id3v2/musicCdIdentifierFrameTests.ts +++ b/test/id3v2/musicCdIdentifierFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertyTests from "./framePropertyTests"; +import PropertyTests from "../utilities/propertyTests"; import MusicCdIdentifierFrame from "../../src/id3v2/frames/musicCdIdentifierFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; @@ -66,7 +66,7 @@ class Id3v2_MusicCdIdentifierFrameTests extends FrameConstructorTests { const data = ByteVector.fromString("fuxbuxqux", StringType.Latin1); // Act / Assert - FramePropertyTests.propertyRoundTrip((v) => { frame.data = v; }, () => frame.data, data); + PropertyTests.propertyRoundTrip((v) => { frame.data = v; }, () => frame.data, data); } @test diff --git a/test/id3v2/playCountFrameTests.ts b/test/id3v2/playCountFrameTests.ts index 8451b5ab..cd3b8531 100644 --- a/test/id3v2/playCountFrameTests.ts +++ b/test/id3v2/playCountFrameTests.ts @@ -4,7 +4,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertyTests from "./framePropertyTests"; +import PropertyTests from "../utilities/propertyTests"; import PlayCountFrame from "../../src/id3v2/frames/playCountFrame"; import {ByteVector} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; @@ -122,10 +122,10 @@ class Id3v2_PlayCountFrame_PropertyTests { const get = () => frame.playCount; // Act / Assert - FramePropertyTests.propertyThrows(set, BigInt(-1)); - FramePropertyTests.propertyThrows(set, BigInt("18446744073709551616", 10)); - FramePropertyTests.propertyRoundTrip(set, get, BigInt(100)); - FramePropertyTests.propertyRoundTrip(set, get, BigInt("68719476721", 10)); + PropertyTests.propertyThrows(set, BigInt(-1)); + PropertyTests.propertyThrows(set, BigInt("18446744073709551616", 10)); + PropertyTests.propertyRoundTrip(set, get, BigInt(100)); + PropertyTests.propertyRoundTrip(set, get, BigInt("68719476721", 10)); } } diff --git a/test/id3v2/popularimeterFrameTests.ts b/test/id3v2/popularimeterFrameTests.ts index a7cdf5bb..d97941fe 100644 --- a/test/id3v2/popularimeterFrameTests.ts +++ b/test/id3v2/popularimeterFrameTests.ts @@ -4,7 +4,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertyTests from "./framePropertyTests"; +import PropertyTests from "../utilities/propertyTests"; import PopularimeterFrame from "../../src/id3v2/frames/popularimeterFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; @@ -187,10 +187,10 @@ class Id3v2_PopularimeterFrame_PropertyTests { const get = () => frame.playCount; // Act - FramePropertyTests.propertyRoundTrip(set, get, BigInt(1234)); - FramePropertyTests.propertyRoundTrip(set, get, undefined); - FramePropertyTests.propertyThrows(set, null); - FramePropertyTests.propertyThrows(set, BigInt(-1)); + PropertyTests.propertyRoundTrip(set, get, BigInt(1234)); + PropertyTests.propertyRoundTrip(set, get, undefined); + PropertyTests.propertyThrows(set, null); + PropertyTests.propertyThrows(set, BigInt(-1)); } @test @@ -201,10 +201,10 @@ class Id3v2_PopularimeterFrame_PropertyTests { const get = () => frame.rating; // Act - FramePropertyTests.propertyRoundTrip(set, get, 5); - FramePropertyTests.propertyThrows(set, -1); - FramePropertyTests.propertyThrows(set, 1.23); - FramePropertyTests.propertyThrows(set, 0x100); + PropertyTests.propertyRoundTrip(set, get, 5); + PropertyTests.propertyThrows(set, -1); + PropertyTests.propertyThrows(set, 1.23); + PropertyTests.propertyThrows(set, 0x100); } @test @@ -215,9 +215,9 @@ class Id3v2_PopularimeterFrame_PropertyTests { const get = () => frame.user; // Act - FramePropertyTests.propertyRoundTrip(set, get, "bux"); - FramePropertyTests.propertyNormalized(set, get, undefined, ""); - FramePropertyTests.propertyNormalized(set, get, null, ""); + PropertyTests.propertyRoundTrip(set, get, "bux"); + PropertyTests.propertyNormalized(set, get, undefined, ""); + PropertyTests.propertyNormalized(set, get, null, ""); } } diff --git a/test/id3v2/privateFrameTests.ts b/test/id3v2/privateFrameTests.ts index d9809135..ce2b0496 100644 --- a/test/id3v2/privateFrameTests.ts +++ b/test/id3v2/privateFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertyTests from "./framePropertyTests"; +import PropertyTests from "../utilities/propertyTests"; import PrivateFrame from "../../src/id3v2/frames/privateFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; @@ -120,7 +120,7 @@ class Id3v2_PrivateFrame_PropertyTests { const frame = PrivateFrame.fromOwner("fux"); // Act / Assert - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.privateData = v; }, () => frame.privateData, ByteVector.fromString("bux") diff --git a/test/id3v2/relativeVolumeFrameTests.ts b/test/id3v2/relativeVolumeFrameTests.ts index b0ef43fc..e4ec15e0 100644 --- a/test/id3v2/relativeVolumeFrameTests.ts +++ b/test/id3v2/relativeVolumeFrameTests.ts @@ -4,7 +4,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import ConstructorTests from "./frameConstructorTests"; -import FramePropertyTests from "./framePropertyTests"; +import PropertyTests from "../utilities/propertyTests"; import {ChannelData, ChannelType, RelativeVolumeFrame} from "../../src/id3v2/frames/relativeVolumeFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; @@ -437,7 +437,7 @@ class Id3v2_RelativeVolumeFrameMethodTests { const frame = RelativeVolumeFrame.fromIdentification("foo"); // Act / Assert - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.setPeakBits(ChannelType.Subwoofer, v); }, () => frame.getPeakBits(ChannelType.Subwoofer), 8 @@ -451,7 +451,7 @@ class Id3v2_RelativeVolumeFrameMethodTests { frame.setPeakBits(ChannelType.Subwoofer, 16); // Act / Assert - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.setPeakVolume(ChannelType.Subwoofer, v); }, () => frame.getPeakVolume(ChannelType.Subwoofer), BigInt(123) @@ -464,7 +464,7 @@ class Id3v2_RelativeVolumeFrameMethodTests { const frame = RelativeVolumeFrame.fromIdentification("foo"); // Act / Assert - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.setVolumeAdjustment(ChannelType.Subwoofer, v); }, () => frame.getVolumeAdjustment(ChannelType.Subwoofer), 8 diff --git a/test/id3v2/synchronizedLyricsFrameTests.ts b/test/id3v2/synchronizedLyricsFrameTests.ts index c4603539..4b08eff8 100644 --- a/test/id3v2/synchronizedLyricsFrameTests.ts +++ b/test/id3v2/synchronizedLyricsFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertyTests from "./framePropertyTests"; +import PropertyTests from "../utilities/propertyTests"; import Id3v2Settings from "../../src/id3v2/id3v2Settings"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; @@ -348,8 +348,8 @@ class Id3v2_SynchronizedLyricsFrame_PropertyTests { const get = () => frame.description; // Act / Assert - FramePropertyTests.propertyRoundTrip(set, get, "fux" ); - FramePropertyTests.propertyRoundTrip(set, get, undefined); + PropertyTests.propertyRoundTrip(set, get, "fux" ); + PropertyTests.propertyRoundTrip(set, get, undefined); } @test @@ -358,7 +358,7 @@ class Id3v2_SynchronizedLyricsFrame_PropertyTests { const frame = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord); // Act / Assert - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.format = v; }, () => frame.format, TimestampFormat.AbsoluteMilliseconds @@ -373,9 +373,9 @@ class Id3v2_SynchronizedLyricsFrame_PropertyTests { const get = () => frame.language; // Act / Assert - FramePropertyTests.propertyRoundTrip(set, get, "fux"); - FramePropertyTests.propertyRoundTrip(set, get, "shoe"); - FramePropertyTests.propertyRoundTrip(set, get, "ab"); + PropertyTests.propertyRoundTrip(set, get, "fux"); + PropertyTests.propertyRoundTrip(set, get, "shoe"); + PropertyTests.propertyRoundTrip(set, get, "ab"); } @test @@ -387,9 +387,9 @@ class Id3v2_SynchronizedLyricsFrame_PropertyTests { const value = [new SynchronizedText(123, "foo")]; // Act / Assert - FramePropertyTests.propertyRoundTrip(set, get, value); - FramePropertyTests.propertyNormalized(set, get, undefined, []); - FramePropertyTests.propertyNormalized(set, get, null, []); + PropertyTests.propertyRoundTrip(set, get, value); + PropertyTests.propertyNormalized(set, get, undefined, []); + PropertyTests.propertyNormalized(set, get, null, []); } @test @@ -398,7 +398,7 @@ class Id3v2_SynchronizedLyricsFrame_PropertyTests { const frame = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord); // Act / Assert - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16BE @@ -411,7 +411,7 @@ class Id3v2_SynchronizedLyricsFrame_PropertyTests { const frame = SynchronizedLyricsFrame.fromInfo("foo", "bar", SynchronizedTextType.Chord); // Act / Assert - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.textType = v; }, () => frame.textType, SynchronizedTextType.Trivia diff --git a/test/id3v2/termsOfUseFrameTests.ts b/test/id3v2/termsOfUseFrameTests.ts index b0a8ecb0..28be9d00 100644 --- a/test/id3v2/termsOfUseFrameTests.ts +++ b/test/id3v2/termsOfUseFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertyTests from "./framePropertyTests"; +import PropertyTests from "../utilities/propertyTests"; import Id3v2Settings from "../../src/id3v2/id3v2Settings"; import TermsOfUseFrame from "../../src/id3v2/frames/termsOfUseFrame"; import {ByteVector, StringType} from "../../src/byteVector"; @@ -132,11 +132,11 @@ class Id3v2_TermsOfUseFrame_PropertyTests { // Act/Assert const frame = TermsOfUseFrame.fromFields("eng"); - FramePropertyTests.propertyNormalized(set, get, "fu", "XXX"); - FramePropertyTests.propertyNormalized(set, get, "fuxx", "fux"); - FramePropertyTests.propertyNormalized(set, get, undefined, "XXX"); - FramePropertyTests.propertyNormalized(set, get, null, "XXX"); - FramePropertyTests.propertyRoundTrip(set, get, "fux"); + PropertyTests.propertyNormalized(set, get, "fu", "XXX"); + PropertyTests.propertyNormalized(set, get, "fuxx", "fux"); + PropertyTests.propertyNormalized(set, get, undefined, "XXX"); + PropertyTests.propertyNormalized(set, get, null, "XXX"); + PropertyTests.propertyRoundTrip(set, get, "fux"); } @test @@ -146,15 +146,15 @@ class Id3v2_TermsOfUseFrame_PropertyTests { const get = () => frame.text; const frame = TermsOfUseFrame.fromFields("eng"); - FramePropertyTests.propertyRoundTrip(set, get, "fux"); - FramePropertyTests.propertyNormalized(set, get, undefined, ""); - FramePropertyTests.propertyNormalized(set, get, null, ""); + PropertyTests.propertyRoundTrip(set, get, "fux"); + PropertyTests.propertyNormalized(set, get, undefined, ""); + PropertyTests.propertyNormalized(set, get, null, ""); } @test public textEncoding() { const frame = TermsOfUseFrame.fromFields("eng", StringType.Latin1); - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16 diff --git a/test/id3v2/textInformationFrameTests.ts b/test/id3v2/textInformationFrameTests.ts index e65e8144..4dfefce0 100644 --- a/test/id3v2/textInformationFrameTests.ts +++ b/test/id3v2/textInformationFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertyTests from "./framePropertyTests"; +import PropertyTests from "../utilities/propertyTests"; import Id3v2Settings from "../../src/id3v2/id3v2Settings"; import {TextInformationFrame} from "../../src/id3v2/frames/textInformationFrame"; import {ByteVector, StringType} from "../../src/byteVector"; @@ -240,7 +240,7 @@ class Id3v2_TextInformationFrame_PropertyTests { const frame = getTestFrame(); // Act / Assert - FramePropertyTests.propertyRoundTrip((v) => { frame.text = v; }, () => frame.text, ["bux", "fux"]); + PropertyTests.propertyRoundTrip((v) => { frame.text = v; }, () => frame.text, ["bux", "fux"]); } @test @@ -249,7 +249,7 @@ class Id3v2_TextInformationFrame_PropertyTests { const frame = getTestFrame(); // Act / Assert - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16BE @@ -263,7 +263,7 @@ class Id3v2_TextInformationFrame_PropertyTests { const _ = frame.text; // Force a read // Act / Assert - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16BE diff --git a/test/id3v2/uniqueFileIdentifierFrameTests.ts b/test/id3v2/uniqueFileIdentifierFrameTests.ts index 8a288b13..8f64b619 100644 --- a/test/id3v2/uniqueFileIdentifierFrameTests.ts +++ b/test/id3v2/uniqueFileIdentifierFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertyTests from "./framePropertyTests"; +import PropertyTests from "../utilities/propertyTests"; import UniqueFileIdentifierFrame from "../../src/id3v2/frames/uniqueFileIdentifierFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; @@ -202,7 +202,7 @@ class Id3v2_UniqueFileIdentifierFrame_PropertyTests { const frame = UniqueFileIdentifierFrame.fromData("fuxqux", ByteVector.fromSize(1)); // Act/Assert - FramePropertyTests.propertyThrows((v) => { frame.identifier = v; }, ByteVector.fromSize(65)); + PropertyTests.propertyThrows((v) => { frame.identifier = v; }, ByteVector.fromSize(65)); } @test @@ -212,7 +212,7 @@ class Id3v2_UniqueFileIdentifierFrame_PropertyTests { const identifier = ByteVector.fromString("quxx"); // Act / Assert - FramePropertyTests.propertyRoundTrip((v) => { frame.identifier = v; }, () => frame.identifier, identifier); + PropertyTests.propertyRoundTrip((v) => { frame.identifier = v; }, () => frame.identifier, identifier); } } diff --git a/test/id3v2/unsynchronizedLyricsFrameTests.ts b/test/id3v2/unsynchronizedLyricsFrameTests.ts index 5fc02fa0..e173ed28 100644 --- a/test/id3v2/unsynchronizedLyricsFrameTests.ts +++ b/test/id3v2/unsynchronizedLyricsFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertyTests from "./framePropertyTests"; +import PropertyTests from "../utilities/propertyTests"; import UnsynchronizedLyricsFrame from "../../src/id3v2/frames/unsynchronizedLyricsFrame"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; @@ -161,9 +161,9 @@ class Id3v2_UnsynchronizedLyricsFrame_PropertyTests { const get = () => frame.description; // Act / Assert - FramePropertyTests.propertyRoundTrip(set, get, "fux"); - FramePropertyTests.propertyNormalized(set, get, undefined, ""); - FramePropertyTests.propertyNormalized(set, get, null, ""); + PropertyTests.propertyRoundTrip(set, get, "fux"); + PropertyTests.propertyNormalized(set, get, undefined, ""); + PropertyTests.propertyNormalized(set, get, null, ""); } @test @@ -174,11 +174,11 @@ class Id3v2_UnsynchronizedLyricsFrame_PropertyTests { const get = () => frame.language; // Act / assert - FramePropertyTests.propertyRoundTrip(set, get, "ABC"); - FramePropertyTests.propertyNormalized(set, get, undefined, "XXX"); - FramePropertyTests.propertyNormalized(set, get, null, "XXX"); - FramePropertyTests.propertyNormalized(set, get, "AB", "XXX"); - FramePropertyTests.propertyNormalized(set, get, "ABCD", "XXX"); + PropertyTests.propertyRoundTrip(set, get, "ABC"); + PropertyTests.propertyNormalized(set, get, undefined, "XXX"); + PropertyTests.propertyNormalized(set, get, null, "XXX"); + PropertyTests.propertyNormalized(set, get, "AB", "XXX"); + PropertyTests.propertyNormalized(set, get, "ABCD", "XXX"); } @test @@ -189,9 +189,9 @@ class Id3v2_UnsynchronizedLyricsFrame_PropertyTests { const get = () => frame.text; // Act / Assert - FramePropertyTests.propertyRoundTrip(set, get, "fux qux quxx"); - FramePropertyTests.propertyNormalized(set, get, undefined, ""); - FramePropertyTests.propertyNormalized(set, get, null, ""); + PropertyTests.propertyRoundTrip(set, get, "fux qux quxx"); + PropertyTests.propertyNormalized(set, get, undefined, ""); + PropertyTests.propertyNormalized(set, get, null, ""); } @test @@ -200,7 +200,7 @@ class Id3v2_UnsynchronizedLyricsFrame_PropertyTests { const frame = getTestUnsynchronizedLyricsFrame(); // Act / Assert - FramePropertyTests.propertyRoundTrip( + PropertyTests.propertyRoundTrip( (v) => { frame.textEncoding = v; }, () => frame.textEncoding, StringType.UTF16BE diff --git a/test/id3v2/urlLinkFrameTests.ts b/test/id3v2/urlLinkFrameTests.ts index 58d0097e..0fa75711 100644 --- a/test/id3v2/urlLinkFrameTests.ts +++ b/test/id3v2/urlLinkFrameTests.ts @@ -3,7 +3,7 @@ import * as ChaiAsPromised from "chai-as-promised"; import {slow, suite, test, timeout} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; -import FramePropertyTests from "./framePropertyTests"; +import PropertyTests from "../utilities/propertyTests"; import TestConstants from "../testConstants"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; @@ -171,8 +171,8 @@ class Id3v2_UrlLinkFrame_PropertyTests { const get = () => frame.text; // Act / Assert - FramePropertyTests.propertyNormalized(set, get, undefined, []); - FramePropertyTests.propertyNormalized(set, get, null, []); + PropertyTests.propertyNormalized(set, get, undefined, []); + PropertyTests.propertyNormalized(set, get, null, []); } @test diff --git a/test/id3v2/framePropertyTests.ts b/test/utilities/propertyTests.ts similarity index 94% rename from test/id3v2/framePropertyTests.ts rename to test/utilities/propertyTests.ts index e583162c..fefed4bb 100644 --- a/test/id3v2/framePropertyTests.ts +++ b/test/utilities/propertyTests.ts @@ -5,7 +5,7 @@ import * as ChaiAsPromised from "chai-as-promised"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -export default class FramePropertyTests { +export default class PropertyTests { public static propertyRoundTrip(set: (v: T) => void, get: () => T, val: T) { // Act set(val); From 8fd0ec6d0bd4e769195d4fe80005f684e03e6b8f Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 3 Mar 2020 18:52:11 -0500 Subject: [PATCH 54/71] MORE PROPERTY TESTS --- src/id3v2/id3v2Tag.ts | 13 +- test/id3v2/id3v2TagTests.ts | 548 ++++++++++++++++++++++++++++++++++++ 2 files changed, 551 insertions(+), 10 deletions(-) diff --git a/src/id3v2/id3v2Tag.ts b/src/id3v2/id3v2Tag.ts index 0b476ece..016bbf64 100644 --- a/src/id3v2/id3v2Tag.ts +++ b/src/id3v2/id3v2Tag.ts @@ -279,12 +279,8 @@ export default class Id3v2Tag extends Tag { const commentFrames = this.getFramesByClassType(FrameClassType.CommentsFrame); // Delete the "" comment frames that are in this language - // @TODO: Verify that only the frames in this language should be deleted if (!value) { - const matchingFrames = CommentsFrame.findAll(commentFrames, "", Id3v2Tag.language); - for (const f of matchingFrames) { - this.removeFrame(f); - } + this.removeFrames(FrameIdentifiers.COMM); return; } @@ -433,10 +429,7 @@ export default class Id3v2Tag extends Tag { // Delete all unsynchronized lyrics frames in this language // @TODO: Verify that deleting only this language is the correct behavior if (!value) { - const matchFrames = UnsynchronizedLyricsFrame.findAll(frames, "", Id3v2Tag.language); - for (const f of matchFrames) { - this.removeFrame(f); - } + this.removeFrames(FrameIdentifiers.USLT); return; } @@ -937,7 +930,7 @@ export default class Id3v2Tag extends Tag { const formattedNumerator = numerator.toString().padStart(minPlaces, "0"); this.setTextFrame(ident, `${formattedNumerator}/${denominator}`); } else { - this.setTextFrame(ident, numerator.toString()); + this.setTextFrame(ident, numerator.toString().padStart(minPlaces, "0")); } } diff --git a/test/id3v2/id3v2TagTests.ts b/test/id3v2/id3v2TagTests.ts index 9fe76d7c..fdf2b4c3 100644 --- a/test/id3v2/id3v2TagTests.ts +++ b/test/id3v2/id3v2TagTests.ts @@ -18,6 +18,9 @@ import {FrameIdentifier, FrameIdentifiers} from "../../src/id3v2/frameIdentifier import PropertyTests from "../utilities/propertyTests"; import {TextInformationFrame, UserTextInformationFrame} from "../../src/id3v2/frames/textInformationFrame"; import {TagTypes} from "../../src/tag"; +import CommentsFrame from "../../src/id3v2/frames/commentsFrame"; +import Id3v2Settings from "../../src/id3v2/id3v2Settings"; +import UnsynchronizedLyricsFrame from "../../src/id3v2/frames/unsynchronizedLyricsFrame"; // Setup Chai Chai.use(ChaiAsPromised); @@ -424,6 +427,551 @@ class Id3v2_Tag_PropertyTests { ); } + @test + public comment_noCommentFrames() { + // Arrange + const tag = new Id3v2Tag(); + + // Act + const output = tag.comment; + + // Assert + assert.isUndefined(output); + } + + @test + public comment_multipleFrames_picksBestLanguage() { + // Arrange + const tag = new Id3v2Tag(); + const frame1 = CommentsFrame.fromDescription("", "jpn"); + frame1.text = "foo"; + tag.addFrame(frame1); + const frame2 = CommentsFrame.fromDescription("", "eng"); + frame2.text = "bar"; + tag.addFrame(frame2); + + const initialLanguage = Id3v2Tag.language; + try { + // Act + Id3v2Tag.language = "eng"; + const output = tag.comment; + + // Assert + assert.strictEqual(output, "bar"); + } finally { + Id3v2Tag.language = initialLanguage; + } + } + + @test + public comment_setToFalsy_removesFrames() { + // Arrange + const tag = new Id3v2Tag(); + const frame1 = CommentsFrame.fromDescription("", "jpn"); + frame1.text = "foo"; + tag.addFrame(frame1); + const frame2 = CommentsFrame.fromDescription("", "eng"); + frame2.text = "bar"; + tag.addFrame(frame2); + + const initialLanguage = Id3v2Tag.language; + try { + // Act + Id3v2Tag.language = "eng"; + tag.comment = undefined; + + // Assert + assert.strictEqual(tag.comment, undefined); + assert.strictEqual(tag.frames.length, 0); + } finally { + Id3v2Tag.language = initialLanguage; + } + } + + @test + public comment_setToTruthy_setsLanguageFrame() { + // Arrange + const tag = new Id3v2Tag(); + const frame1 = CommentsFrame.fromDescription("", "jpn"); + frame1.text = "foo"; + tag.addFrame(frame1); + const frame2 = CommentsFrame.fromDescription("", "eng"); + frame2.text = "bar"; + tag.addFrame(frame2); + + const initialLanguage = Id3v2Tag.language; + try { + // Act + Id3v2Tag.language = "eng"; + tag.comment = "qux"; + + // Assert + assert.strictEqual(tag.comment, "qux"); + assert.strictEqual(tag.frames.length, 2); + assert.strictEqual(frame2.text, "qux"); + assert.strictEqual(frame1.text, "foo"); + } finally { + Id3v2Tag.language = initialLanguage; + } + } + + @test + public comment_setToTruthy_addsLanguageFrame() { + // Arrange + const tag = new Id3v2Tag(); + + const initialLanguage = Id3v2Tag.language; + try { + // Act + Id3v2Tag.language = "eng"; + tag.comment = "foo"; + + // Assert + assert.strictEqual(tag.comment, "foo"); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.CommentsFrame); + assert.strictEqual(( tag.frames[0]).text, "foo"); + assert.strictEqual(( tag.frames[0]).language, "eng"); + assert.strictEqual(( tag.frames[0]).description, ""); + } finally { + Id3v2Tag.language = initialLanguage; + } + } + + @test + public genres_noFrame() { + // Arrange + const tag = new Id3v2Tag(); + + // Act + const output = tag.genres; + + // Assert + assert.deepStrictEqual(output, []); + } + + @test + public genres_withFrame() { + // Arrange + const tag = new Id3v2Tag(); + const frame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCON); + frame.text = ["32", "foo"]; + tag.addFrame(frame); + + // Act + const output = tag.genres; + + // Assert + assert.deepStrictEqual(output, ["Classical", "foo"]); + } + + @test + public genres_setValueWithNoFrame() { + // Arrange + const tag = new Id3v2Tag(); + + // Act + tag.genres = ["Classical", "foo"]; + + // Assert + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TCON); + assert.deepStrictEqual(( tag.frames[0]).text, ["32", "foo"]); + } + + @test + public genres_setValueWithFrame() { + // Arrange + const tag = new Id3v2Tag(); + const frame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCON); + frame.text = ["qux"]; + tag.addFrame(frame); + + // Act + tag.genres = ["Classical", "foo"]; + + // Assert + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TCON); + assert.deepStrictEqual(( tag.frames[0]).text, ["32", "foo"]); + } + + @test + public genres_setValueWithNumericGenresTurnedOff() { + // Arrange + const tag = new Id3v2Tag(); + + const initialSetting = Id3v2Settings.useNumericGenres; + try { + // Act + Id3v2Settings.useNumericGenres = false; + tag.genres = ["Classical", "foo"]; + + // Assert + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TCON); + assert.deepStrictEqual(( tag.frames[0]).text, ["Classical", "foo"]); + } finally { + Id3v2Settings.useNumericGenres = initialSetting; + } + } + + public year() { + // TODO: do it. + } + + @test + public track_invalidValue() { + // Arrange + const tag = new Id3v2Tag(); + const set = (v: number) => { tag.track = v; }; + + // Act / Assert + PropertyTests.propertyThrows(set, undefined); + PropertyTests.propertyThrows(set, null); + PropertyTests.propertyThrows(set, 1.23); + PropertyTests.propertyThrows(set, -1); + PropertyTests.propertyThrows(set, Number.MAX_SAFE_INTEGER + 1); + } + + @test + public track_noTrackCount() { + // Arrange + const tag = new Id3v2Tag(); + const set = (v: number) => { tag.track = v; }; + const get = () => tag.track; + + // Act / Assert + assert.strictEqual(tag.track, 0); + + PropertyTests.propertyRoundTrip(set, get, 2); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TRCK); + assert.deepStrictEqual(( tag.frames[0]).text, ["02"]); + + PropertyTests.propertyRoundTrip(set, get, 123); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TRCK); + assert.deepStrictEqual(( tag.frames[0]).text, ["123"]); + + PropertyTests.propertyRoundTrip(set, get, 0); + assert.strictEqual(tag.frames.length, 0); + } + + @test + public track_withTrackCount() { + // Arrange + const tag = new Id3v2Tag(); + tag.trackCount = 123; + const set = (v: number) => { tag.track = v; }; + const get = () => tag.track; + + // Act / Assert + assert.strictEqual(tag.track, 0); + + PropertyTests.propertyRoundTrip(set, get, 2); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TRCK); + assert.deepStrictEqual(( tag.frames[0]).text, ["02/123"]); + + PropertyTests.propertyRoundTrip(set, get, 123); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TRCK); + assert.deepStrictEqual(( tag.frames[0]).text, ["123/123"]); + + PropertyTests.propertyRoundTrip(set, get, 0); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TRCK); + assert.deepStrictEqual(( tag.frames[0]).text, ["00/123"]); + } + + @test + public trackCount_invalidValue() { + // Arrange + const tag = new Id3v2Tag(); + const set = (v: number) => { tag.trackCount = v; }; + + // Act / Assert + PropertyTests.propertyThrows(set, undefined); + PropertyTests.propertyThrows(set, null); + PropertyTests.propertyThrows(set, 1.23); + PropertyTests.propertyThrows(set, -1); + PropertyTests.propertyThrows(set, Number.MAX_SAFE_INTEGER + 1); + } + + @test + public trackCount_noTrack() { + // Arrange + const tag = new Id3v2Tag(); + const set = (v: number) => { tag.trackCount = v; }; + const get = () => tag.trackCount; + + // Act / Assert + assert.strictEqual(tag.trackCount, 0); + + PropertyTests.propertyRoundTrip(set, get, 2); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TRCK); + assert.deepStrictEqual(( tag.frames[0]).text, ["0/2"]); + + PropertyTests.propertyRoundTrip(set, get, 123); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TRCK); + assert.deepStrictEqual(( tag.frames[0]).text, ["0/123"]); + + PropertyTests.propertyRoundTrip(set, get, 0); + assert.strictEqual(tag.frames.length, 0); + } + + @test + public trackCount_withTrack() { + // Arrange + const tag = new Id3v2Tag(); + tag.track = 12; + const set = (v: number) => { tag.trackCount = v; }; + const get = () => tag.trackCount; + + // Act / Assert + assert.strictEqual(tag.trackCount, 0); + + PropertyTests.propertyRoundTrip(set, get, 2); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TRCK); + assert.deepStrictEqual(( tag.frames[0]).text, ["12/2"]); + + PropertyTests.propertyRoundTrip(set, get, 123); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TRCK); + assert.deepStrictEqual(( tag.frames[0]).text, ["12/123"]); + + PropertyTests.propertyRoundTrip(set, get, 0); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TRCK); + assert.deepStrictEqual(( tag.frames[0]).text, ["12"]); + } + + @test + public disc_invalidValue() { + // Arrange + const tag = new Id3v2Tag(); + const set = (v: number) => { tag.disc = v; }; + + // Act / Assert + PropertyTests.propertyThrows(set, undefined); + PropertyTests.propertyThrows(set, null); + PropertyTests.propertyThrows(set, 1.23); + PropertyTests.propertyThrows(set, -1); + PropertyTests.propertyThrows(set, Number.MAX_SAFE_INTEGER + 1); + } + + @test + public disc_noDiscCount() { + // Arrange + const tag = new Id3v2Tag(); + const set = (v: number) => { tag.disc = v; }; + const get = () => tag.disc; + + // Act / Assert + assert.strictEqual(tag.disc, 0); + + PropertyTests.propertyRoundTrip(set, get, 2); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TPOS); + assert.deepStrictEqual(( tag.frames[0]).text, ["2"]); + + PropertyTests.propertyRoundTrip(set, get, 123); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TPOS); + assert.deepStrictEqual(( tag.frames[0]).text, ["123"]); + + PropertyTests.propertyRoundTrip(set, get, 0); + assert.strictEqual(tag.frames.length, 0); + } + + @test + public disc_withDiscCount() { + // Arrange + const tag = new Id3v2Tag(); + tag.discCount = 123; + const set = (v: number) => { tag.disc = v; }; + const get = () => tag.disc; + + // Act / Assert + assert.strictEqual(tag.disc, 0); + + PropertyTests.propertyRoundTrip(set, get, 2); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TPOS); + assert.deepStrictEqual(( tag.frames[0]).text, ["2/123"]); + + PropertyTests.propertyRoundTrip(set, get, 123); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TPOS); + assert.deepStrictEqual(( tag.frames[0]).text, ["123/123"]); + + PropertyTests.propertyRoundTrip(set, get, 0); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TPOS); + assert.deepStrictEqual(( tag.frames[0]).text, ["0/123"]); + } + + @test + public discCount_invalidValue() { + // Arrange + const tag = new Id3v2Tag(); + const set = (v: number) => { tag.discCount = v; }; + + // Act / Assert + PropertyTests.propertyThrows(set, undefined); + PropertyTests.propertyThrows(set, null); + PropertyTests.propertyThrows(set, 1.23); + PropertyTests.propertyThrows(set, -1); + PropertyTests.propertyThrows(set, Number.MAX_SAFE_INTEGER + 1); + } + + @test + public discCount_noDisc() { + // Arrange + const tag = new Id3v2Tag(); + const set = (v: number) => { tag.discCount = v; }; + const get = () => tag.discCount; + + // Act / Assert + assert.strictEqual(tag.discCount, 0); + + PropertyTests.propertyRoundTrip(set, get, 2); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TPOS); + assert.deepStrictEqual(( tag.frames[0]).text, ["0/2"]); + + PropertyTests.propertyRoundTrip(set, get, 0); + assert.strictEqual(tag.frames.length, 0); + } + + @test + public discCount_withDisc() { + // Arrange + const tag = new Id3v2Tag(); + tag.disc = 12; + const set = (v: number) => { tag.discCount = v; }; + const get = () => tag.discCount; + + // Act / Assert + assert.strictEqual(tag.discCount, 0); + + PropertyTests.propertyRoundTrip(set, get, 2); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TPOS); + assert.deepStrictEqual(( tag.frames[0]).text, ["12/2"]); + + PropertyTests.propertyRoundTrip(set, get, 0); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TPOS); + assert.deepStrictEqual(( tag.frames[0]).text, ["12"]); + } + + @test + public lyrics() { + // Arrange + const tag = new Id3v2Tag(); + const set = (v: string) => { tag.lyrics = v; }; + const get = () => tag.lyrics; + + const initialLanguage = Id3v2Tag.language; + try { + Id3v2Tag.language = "eng"; + + // Act / Assert + assert.strictEqual(tag.lyrics, undefined); + + PropertyTests.propertyRoundTrip(set, get, "lyrics"); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.UnsynchronizedLyricsFrame); + assert.strictEqual(( tag.frames[0]).description, ""); + assert.deepStrictEqual(( tag.frames[0]).text, "lyrics"); + assert.strictEqual(( tag.frames[0]).textEncoding, Id3v2Settings.defaultEncoding); + + PropertyTests.propertyRoundTrip(set, get, undefined); + assert.strictEqual(tag.frames.length, 0); + } finally { + Id3v2Tag.language = initialLanguage; + } + } + + @test + public grouping() { + this.testTextFrameProperty( + (t, v) => { t.grouping = v; }, + (t) => t.grouping, + FrameIdentifiers.TIT1 + ); + } + + @test + public bpm() { + // Arrange + const tag = new Id3v2Tag(); + const set = (v: number) => { tag.beatsPerMinute = v; }; + const get = () => tag.beatsPerMinute; + + // Act / Assert + assert.strictEqual(tag.beatsPerMinute, 0); + + PropertyTests.propertyRoundTrip(set, get, 128); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TBPM); + assert.deepStrictEqual(( tag.frames[0]).text, ["123"]); + + ( tag.frames[0]).text = ["123.4"]; + assert.strictEqual(tag.beatsPerMinute, 123); + + PropertyTests.propertyRoundTrip(set, get, 0); + assert.strictEqual(tag.frames.length, 1); + } + + @test + public conductor() { + this.testTextFrameProperty( + (t, v) => { t.conductor = v; }, + (t) => t.conductor, + FrameIdentifiers.TPE3 + ); + } + + @test + public copyright() { + this.testTextFrameProperty( + (t, v) => { t.copyright = v; }, + (t) => t.copyright, + FrameIdentifiers.TCOP + ); + } + + // @test + // public + private testTextFrameProperty( set: (t: Id3v2Tag, v: string) => void, get: (t: Id3v2Tag) => string, From 5183871509d730b9b5c940cced5eee96b8c9dcbf Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 7 Mar 2020 23:44:28 -0500 Subject: [PATCH 55/71] Pretty much all the property from frame tests are done --- src/id3v2/frameIdentifiers.ts | 3 +- src/id3v2/frames/attachmentFrame.ts | 5 +- src/id3v2/id3v2Tag.ts | 104 +++--- test/id3v2/id3v2TagTests.ts | 534 +++++++++++++++++++++++++--- test/utilities/testFile.ts | 1 - 5 files changed, 552 insertions(+), 95 deletions(-) diff --git a/src/id3v2/frameIdentifiers.ts b/src/id3v2/frameIdentifiers.ts index 138f2ee9..7803645d 100644 --- a/src/id3v2/frameIdentifiers.ts +++ b/src/id3v2/frameIdentifiers.ts @@ -1,6 +1,5 @@ import {ByteVector, StringType} from "../byteVector"; import {Guards} from "../utils"; -import {Frame} from "./frames/frame"; /** * @summary Represents the identifier of a frame, depending on the version this may be 3 or 4 @@ -236,7 +235,7 @@ export const FrameIdentifiers: {[key: string]: FrameIdentifier} = { TIT2: uniqueFrameIdentifiers.TIT2, TIT3: uniqueFrameIdentifiers.TIT3, TKE: uniqueFrameIdentifiers.TKEY, - TKYE: uniqueFrameIdentifiers.TKEY, + TKEY: uniqueFrameIdentifiers.TKEY, TLA: uniqueFrameIdentifiers.TLAN, TLAN: uniqueFrameIdentifiers.TLAN, TLE: uniqueFrameIdentifiers.TLEN, diff --git a/src/id3v2/frames/attachmentFrame.ts b/src/id3v2/frames/attachmentFrame.ts index 6534e8ad..7164a8a8 100644 --- a/src/id3v2/frames/attachmentFrame.ts +++ b/src/id3v2/frames/attachmentFrame.ts @@ -7,7 +7,10 @@ import {FrameIdentifiers} from "../frameIdentifiers"; import {IPicture, Picture, PictureType} from "../../picture"; import {Guards} from "../../utils"; -export default class AttachmentFrame extends Frame { +export default class AttachmentFrame extends Frame implements IPicture { + // NOTE: It probably doesn't look necessary to implement IPicture, but it makes converting a + // frame to a picture so much easier, which we need in the Id3v2Tag class. + private _data: ByteVector; private _description: string; private _encoding: StringType = Id3v2Settings.defaultEncoding; diff --git a/src/id3v2/id3v2Tag.ts b/src/id3v2/id3v2Tag.ts index 016bbf64..6d741738 100644 --- a/src/id3v2/id3v2Tag.ts +++ b/src/id3v2/id3v2Tag.ts @@ -36,10 +36,16 @@ export default class Id3v2Tag extends Tag { /** * Constructs an empty ID3v2 tag */ - public constructor() { + private constructor() { super(); } + public static fromEmpty(): Id3v2Tag { + const tag = new Id3v2Tag(); + + return tag; + } + /** * Constructs and initializes a new Tag by reading the contents from a specified position in * the provided file. @@ -194,7 +200,10 @@ export default class Id3v2Tag extends Tag { /** @inheritDoc via TPE1 frame */ get performers(): string[] { return this.getTextAsArray(FrameIdentifiers.TPE1); } /** @inheritDoc via TPE1 frame */ - set performers(value: string[]) { this.setTextFrame(FrameIdentifiers.TPE1, ...value); } + set performers(value: string[]) { + this.setTextFrame(FrameIdentifiers.TPE1, ...value); + this._performersRole = []; + } /** @inheritDoc via TSOP frame */ get performersSort(): string[] { return this.getTextAsArray(FrameIdentifiers.TSOP); } @@ -203,40 +212,54 @@ export default class Id3v2Tag extends Tag { /** @inheritDoc via TMCL frame */ get performersRole(): string[] { - if (this._performersRole) { return this._performersRole; } + // Use the temporary storage if it exists + if (this._performersRole !== undefined && this._performersRole.length > 0) { + return this._performersRole; + } - const perfRef = this.performers; - if (!perfRef) { return []; } + // If there aren't any performers, just return a blank list + if (this.performers === undefined || this.performers.length === 0) { + return []; + } - // Map the instruments to the performers - const map = this.getTextAsArray(FrameIdentifiers.TMCL); - this._performersRole = []; - for (let i = 0; i + 1 < map.length; i += 2) { - const inst = map[i]; - const perfs = map[i + 1]; - if (!inst || !perfs) { continue; } - - const perfList = perfs.split(","); - for (const iperf of perfList) { - if (!iperf) { continue; } - - const perf = iperf.trim(); - if (!perf) { continue; } - - for (let j = 0; j < perfRef.length; j++) { - if (perfRef[j] === perf) { - this._performersRole[j] = this._performersRole[j] - ? this._performersRole[j] + "; " + inst - : inst; - } + // We're going to basically flip the format of the TMCL frame. + // TMCL frames look like: + // [ "instrument", "artist1,artist2", ... ] + // We want: + // { "artist1": ["instrument", ...], "artist2": ["instrument", ...], ...} + const map = this.performers.reduce((o: any, v: string) => { + o[v] = []; + return o; + }, {}); + + const frameData = this.getTextAsArray(FrameIdentifiers.TMCL); + for (let i = 0; i + 1 < frameData.length; i += 2) { + const instrument = frameData[i]; + const performers = frameData[i + 1]; + if (!instrument || !performers) { + continue; + } + + const performersList = performers.split(","); + for (const performer of performersList) { + if (!map[performer]) { + continue; } + + map[performer].push(instrument); } } + // Collapse the instrument lists and return that + this._performersRole = Object.values(map).map((e: string[]) => e.length > 0 ? e.join(";") : undefined); return this._performersRole; } /** @inheritDoc via TMCL frame */ - set performersRole(value: string[]) { this._performersRole = value || []; } + set performersRole(value: string[]) { + // TODO: We shoud really just write this out to the frame instead of this temporary storage + this.removeFrames(FrameIdentifiers.TMCL); + this._performersRole = value ? value.slice(0) : []; + } /** @inheritDoc via TSO2 frame */ get albumArtists(): string[] { return this.getTextAsArray(FrameIdentifiers.TPE2); } @@ -340,10 +363,9 @@ export default class Id3v2Tag extends Tag { * returned. */ get year(): number { - // @TODO: get it from TDRC if it exists, get it from TYER failing that // Case 1: We have a TDRC frame (v2.4), preferentially use that const tdrcText = this.getTextAsString(FrameIdentifiers.TDRC); - if (tdrcText && tdrcText.length > 4) { + if (tdrcText && tdrcText.length >= 4) { // @TODO: Check places where we use this pattern... .parseInt doesn't parse the whole string if it started // with good data return Number.parseInt(tdrcText.substr(0, 4), 10); @@ -351,7 +373,7 @@ export default class Id3v2Tag extends Tag { // Case 2: We have a TYER frame (v2.3/v2.2) const tyerText = this.getTextAsString(FrameIdentifiers.TYER); - if (tyerText && tyerText.length > 4) { + if (tyerText && tyerText.length >= 4) { // @TODO: Check places where we use this pattern... .parseInt doesn't parse the whole string if it started // with good data return Number.parseInt(tdrcText.substr(0, 4), 10); @@ -362,14 +384,16 @@ export default class Id3v2Tag extends Tag { } /** * @inheritDoc - * NOTE: values >9999will remove the frame + * NOTE: values >9999 will remove the frame */ set year(value: number) { Guards.uint(value, "value"); // Case 0: Frame should be deleted if (value > 9999) { - value = 0; + this.removeFrames(FrameIdentifiers.TDRC); + this.removeFrames(FrameIdentifiers.TYER); + return; } // Case 1: We have a TDRC frame (v2.4), preferentially replace contents with year @@ -508,7 +532,7 @@ export default class Id3v2Tag extends Tag { /** @inheritDoc via UFID:http://musicbrainz.org frame */ get musicBrainzTrackId(): string { return this.getUfidText("http://musicbrainz.org"); } /** @inheritDoc via UFID:http://musicbrainz.org frame */ - set musicBrainzTrackId(value: string) { this.setUfidText("http://musicBrainz.org", value); } + set musicBrainzTrackId(value: string) { this.setUfidText("http://musicbrainz.org", value); } /** @inheritDoc via TXXX:MusicBrainz Disc Id frame */ get musicBrainzDiscId(): string { return this.getUserTextAsString("MusicBrainz Disc Id"); } @@ -533,7 +557,7 @@ export default class Id3v2Tag extends Tag { /** @inheritDoc via TXXX:MusicBrainz Album Type frame */ get musicBrainzReleaseType(): string { return this.getUserTextAsString("MusicBrainz Album Type"); } /** @inheritDoc via TXXX:MusicBrainz Album Type frame */ - set musicBrainzReleaseType(value: string) { this.setUserTextAsString("MusicBrainz Album Album Type", value); } + set musicBrainzReleaseType(value: string) { this.setUserTextAsString("MusicBrainz Album Type", value); } /** @inheritDoc via TXXX:MusicBrainz Album Release Country frame */ get musicBrainzReleaseCountry(): string { return this.getUserTextAsString("MusicBrainz Album Release Country"); } @@ -554,7 +578,7 @@ export default class Id3v2Tag extends Tag { } /** @inheritDoc via TXXX:REPLAY_GAIN_TRACK_GAIN frame */ set replayGainTrackGain(value: number) { - if (Number.isNaN(value)) { + if (value === undefined || value === null || Number.isNaN(value)) { this.setUserTextAsString("REPLAYGAIN_TRACK_GAIN", undefined, false); } else { const text = `${value.toFixed(2).toString()} dB`; @@ -569,7 +593,7 @@ export default class Id3v2Tag extends Tag { } /** @inheritDoc via TXXX:REPLAYGAIN_TRACK_PEAK frame */ set replayGainTrackPeak(value: number) { - if (Number.isNaN(value)) { + if (value === undefined || value === null || Number.isNaN(value)) { this.setUserTextAsString("REPLAYGAIN_TRACK_PEAK", undefined, false); } else { const text = value.toFixed(6).toString(); @@ -589,7 +613,7 @@ export default class Id3v2Tag extends Tag { } /** @inheritDoc via TXXX:REPLAYGAIN_ALBUM_GAIN frame */ set replayGainAlbumGain(value: number) { - if (Number.isNaN(value)) { + if (value === undefined || value === null || Number.isNaN(value)) { this.setUserTextAsString("REPLAYGAIN_ALBUM_GAIN", undefined, false); } else { const text = `${value.toFixed(2).toString()} dB`; @@ -604,11 +628,11 @@ export default class Id3v2Tag extends Tag { } /** @inheritDoc via TXXX:REPLAYGAIN_ALBUM_PEAK frame */ set replayGainAlbumPeak(value: number) { - if (Number.isNaN(value)) { - this.setUserTextAsString("REPLAYGAIN_TRACK_PEAK", undefined, false); + if (value === undefined || value === null || Number.isNaN(value)) { + this.setUserTextAsString("REPLAYGAIN_ALBUM_PEAK", undefined, false); } else { const text = value.toFixed(6).toString(); - this.setUserTextAsString("REPLAYGAIN_TRACK_PEAK", text, false); + this.setUserTextAsString("REPLAYGAIN_ALBUM_PEAK", text, false); } } diff --git a/test/id3v2/id3v2TagTests.ts b/test/id3v2/id3v2TagTests.ts index fdf2b4c3..b7150ea8 100644 --- a/test/id3v2/id3v2TagTests.ts +++ b/test/id3v2/id3v2TagTests.ts @@ -4,7 +4,6 @@ import * as TypeMoq from "typemoq"; import {slow, suite, test, timeout} from "mocha-typescript"; import Id3v2Tag from "../../src/id3v2/id3v2Tag"; -import id3v2Tag from "../../src/id3v2/id3v2Tag"; import SyncData from "../../src/id3v2/syncData"; import TestFile from "../utilities/testFile"; import {ByteVector, StringType} from "../../src/byteVector"; @@ -21,6 +20,7 @@ import {TagTypes} from "../../src/tag"; import CommentsFrame from "../../src/id3v2/frames/commentsFrame"; import Id3v2Settings from "../../src/id3v2/id3v2Settings"; import UnsynchronizedLyricsFrame from "../../src/id3v2/frames/unsynchronizedLyricsFrame"; +import {IPicture} from "../../src/picture"; // Setup Chai Chai.use(ChaiAsPromised); @@ -92,7 +92,7 @@ class Id3v2_Tag_ConstructorTests { ); // Act - const tag = id3v2Tag.fromData(data); + const tag = Id3v2Tag.fromData(data); // Assert assert.isOk(tag); @@ -137,7 +137,7 @@ class Id3v2_Tag_ConstructorTests { ); // Act - const tag = id3v2Tag.fromData(data); + const tag = Id3v2Tag.fromData(data); // Assert assert.isOk(tag); @@ -159,7 +159,7 @@ class Id3v2_Tag_ConstructorTests { SyncData.unsyncByteVector(data); // Act - const tag = id3v2Tag.fromData(data); + const tag = Id3v2Tag.fromData(data); // Assert assert.isOk(tag); @@ -265,7 +265,7 @@ class Id3v2_Tag_PropertyTests { @test public flags() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); // Act / Assert PropertyTests.propertyRoundTrip( @@ -278,7 +278,7 @@ class Id3v2_Tag_PropertyTests { @test public isCompilation() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const get = () => tag.isCompilation; const set = (v: boolean) => { tag.isCompilation = v; }; @@ -333,7 +333,7 @@ class Id3v2_Tag_PropertyTests { @test public description() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const set = (v: string) => { tag.description = v; }; const get = () => tag.description; @@ -370,7 +370,32 @@ class Id3v2_Tag_PropertyTests { @test public performersRole() { - // TODO: Write test + // Arrange + const tag = Id3v2Tag.fromEmpty(); + + // Act / Assert + assert.deepStrictEqual(tag.performersRole, []); + + const tmclFrame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TMCL); + const tmclText = ["saxophone", "alice,bob", "flugelhorn", "alice"]; + tmclFrame.text = tmclText; + tag.frames.push(tmclFrame); + tag.performers = ["alice", "bob", "malory"]; + const expected = ["saxophone;flugelhorn", "saxophone", undefined]; + assert.deepStrictEqual(tag.performersRole, expected); + + const newData = ["saxophone", "flugelhorn", undefined]; + tag.performersRole = newData; + assert.deepStrictEqual(tag.performersRole, newData); + assert.deepStrictEqual(tmclFrame.text, tmclText); + + tag.performersRole = undefined; + assert.deepStrictEqual(tag.performersRole, [undefined, undefined, undefined]); + assert.deepStrictEqual(tmclFrame.text, tmclText); + + tag.performers = []; + assert.deepStrictEqual(tag.performersRole, []); + assert.deepStrictEqual(tmclFrame.text, tmclText); } @test @@ -430,7 +455,7 @@ class Id3v2_Tag_PropertyTests { @test public comment_noCommentFrames() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); // Act const output = tag.comment; @@ -442,7 +467,7 @@ class Id3v2_Tag_PropertyTests { @test public comment_multipleFrames_picksBestLanguage() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const frame1 = CommentsFrame.fromDescription("", "jpn"); frame1.text = "foo"; tag.addFrame(frame1); @@ -466,7 +491,7 @@ class Id3v2_Tag_PropertyTests { @test public comment_setToFalsy_removesFrames() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const frame1 = CommentsFrame.fromDescription("", "jpn"); frame1.text = "foo"; tag.addFrame(frame1); @@ -491,7 +516,7 @@ class Id3v2_Tag_PropertyTests { @test public comment_setToTruthy_setsLanguageFrame() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const frame1 = CommentsFrame.fromDescription("", "jpn"); frame1.text = "foo"; tag.addFrame(frame1); @@ -518,7 +543,7 @@ class Id3v2_Tag_PropertyTests { @test public comment_setToTruthy_addsLanguageFrame() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const initialLanguage = Id3v2Tag.language; try { @@ -541,7 +566,7 @@ class Id3v2_Tag_PropertyTests { @test public genres_noFrame() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); // Act const output = tag.genres; @@ -553,7 +578,7 @@ class Id3v2_Tag_PropertyTests { @test public genres_withFrame() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const frame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCON); frame.text = ["32", "foo"]; tag.addFrame(frame); @@ -568,7 +593,7 @@ class Id3v2_Tag_PropertyTests { @test public genres_setValueWithNoFrame() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); // Act tag.genres = ["Classical", "foo"]; @@ -583,7 +608,7 @@ class Id3v2_Tag_PropertyTests { @test public genres_setValueWithFrame() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const frame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCON); frame.text = ["qux"]; tag.addFrame(frame); @@ -601,7 +626,7 @@ class Id3v2_Tag_PropertyTests { @test public genres_setValueWithNumericGenresTurnedOff() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const initialSetting = Id3v2Settings.useNumericGenres; try { @@ -619,14 +644,95 @@ class Id3v2_Tag_PropertyTests { } } - public year() { - // TODO: do it. + @test + public year_invalidValues() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const set = (v: number) => { tag.year = v; }; + + // Act / Assert + PropertyTests.propertyThrows(set, undefined); + PropertyTests.propertyThrows(set, -123); + PropertyTests.propertyThrows(set, 1.23); + PropertyTests.propertyThrows(set, Number.MAX_SAFE_INTEGER + 1); + } + + @test + public year_fromTrdc() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + + // Act / Assert + assert.strictEqual(tag.year, 0); + + const tdrcFrame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TDRC); + tdrcFrame.text = ["1234-04-25"]; + tag.frames.push(tdrcFrame); + assert.strictEqual(tag.year, 1234); + + const tyerFrame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TYER); + tyerFrame.text = ["2345"]; + tag.frames.push(tyerFrame); + assert.strictEqual(tag.year, 1234); + + tag.year = 3456; + assert.strictEqual(tag.year, 3456); + assert.deepStrictEqual(tdrcFrame.text, ["3456"]); + assert.deepStrictEqual(tyerFrame.text, ["2345"]); + assert.strictEqual(tag.frames.length, 2); + + tag.year = 99999; + assert.strictEqual(tag.year, 0); + assert.strictEqual(tag.frames.length, 0); + } + + @test + public year_fromTyer() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + + // Act / Assert + assert.strictEqual(tag.year, 0); + + const tyerFrame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TYER); + tyerFrame.text = ["1234"]; + tag.frames.push(tyerFrame); + assert.strictEqual(tag.year, 1234); + + tag.year = 2345; + assert.strictEqual(tag.year, 2345); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0], tyerFrame); + assert.deepStrictEqual(tyerFrame.text, ["2345"]); + + tag.year = 99999; + assert.strictEqual(tag.year, 0); + assert.strictEqual(tag.frames.length, 0); + } + + @test + public year_noExistingFrame() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + + // Act / Assert + assert.strictEqual(tag.year, 0); + + tag.year = 1234; + assert.strictEqual(tag.year, 1234); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TDRC); + assert.deepStrictEqual(( tag.frames[0]).text, ["1234"]); + + tag.year = 99999; + assert.strictEqual(tag.year, 0); + assert.strictEqual(tag.frames.length, 0); } @test public track_invalidValue() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const set = (v: number) => { tag.track = v; }; // Act / Assert @@ -640,7 +746,7 @@ class Id3v2_Tag_PropertyTests { @test public track_noTrackCount() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const set = (v: number) => { tag.track = v; }; const get = () => tag.track; @@ -666,7 +772,7 @@ class Id3v2_Tag_PropertyTests { @test public track_withTrackCount() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); tag.trackCount = 123; const set = (v: number) => { tag.track = v; }; const get = () => tag.track; @@ -696,7 +802,7 @@ class Id3v2_Tag_PropertyTests { @test public trackCount_invalidValue() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const set = (v: number) => { tag.trackCount = v; }; // Act / Assert @@ -710,7 +816,7 @@ class Id3v2_Tag_PropertyTests { @test public trackCount_noTrack() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const set = (v: number) => { tag.trackCount = v; }; const get = () => tag.trackCount; @@ -736,7 +842,7 @@ class Id3v2_Tag_PropertyTests { @test public trackCount_withTrack() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); tag.track = 12; const set = (v: number) => { tag.trackCount = v; }; const get = () => tag.trackCount; @@ -766,7 +872,7 @@ class Id3v2_Tag_PropertyTests { @test public disc_invalidValue() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const set = (v: number) => { tag.disc = v; }; // Act / Assert @@ -780,7 +886,7 @@ class Id3v2_Tag_PropertyTests { @test public disc_noDiscCount() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const set = (v: number) => { tag.disc = v; }; const get = () => tag.disc; @@ -806,7 +912,7 @@ class Id3v2_Tag_PropertyTests { @test public disc_withDiscCount() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); tag.discCount = 123; const set = (v: number) => { tag.disc = v; }; const get = () => tag.disc; @@ -836,7 +942,7 @@ class Id3v2_Tag_PropertyTests { @test public discCount_invalidValue() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const set = (v: number) => { tag.discCount = v; }; // Act / Assert @@ -850,7 +956,7 @@ class Id3v2_Tag_PropertyTests { @test public discCount_noDisc() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const set = (v: number) => { tag.discCount = v; }; const get = () => tag.discCount; @@ -870,7 +976,7 @@ class Id3v2_Tag_PropertyTests { @test public discCount_withDisc() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); tag.disc = 12; const set = (v: number) => { tag.discCount = v; }; const get = () => tag.discCount; @@ -894,7 +1000,7 @@ class Id3v2_Tag_PropertyTests { @test public lyrics() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const set = (v: string) => { tag.lyrics = v; }; const get = () => tag.lyrics; @@ -931,7 +1037,7 @@ class Id3v2_Tag_PropertyTests { @test public bpm() { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const set = (v: number) => { tag.beatsPerMinute = v; }; const get = () => tag.beatsPerMinute; @@ -942,13 +1048,13 @@ class Id3v2_Tag_PropertyTests { assert.strictEqual(tag.frames.length, 1); assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TBPM); - assert.deepStrictEqual(( tag.frames[0]).text, ["123"]); + assert.deepStrictEqual(( tag.frames[0]).text, ["128"]); ( tag.frames[0]).text = ["123.4"]; assert.strictEqual(tag.beatsPerMinute, 123); PropertyTests.propertyRoundTrip(set, get, 0); - assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames.length, 0); } @test @@ -969,8 +1075,334 @@ class Id3v2_Tag_PropertyTests { ); } - // @test - // public + @test + public dateTagged() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const set = (v: Date) => { tag.dateTagged = v; }; + const get = () => tag.dateTagged; + + // Act / Assert + assert.isUndefined(tag.dateTagged); + + PropertyTests.propertyRoundTrip(set, get, new Date("2020-04-25 12:34:56")); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TDTG); + assert.deepStrictEqual(( tag.frames[0]).text, ["2020-04-25T12:34:56"]); + + ( tag.frames[0]).text = ["bunchagarbage"]; + assert.isUndefined(tag.dateTagged); + + PropertyTests.propertyRoundTrip(set, get, undefined); + assert.strictEqual(tag.frames.length, 0); + } + + @test + public musicBrainzArtistId() { + this.testUserTextFrameProperty( + (t, v) => { t.musicBrainzArtistId = v; }, + (t) => t.musicBrainzArtistId, + "MusicBrainz Artist Id" + ); + } + + @test + public musicBrainzReleaseGroupId() { + this.testUserTextFrameProperty( + (t, v) => { t.musicBrainzReleaseGroupId = v; }, + (t) => t.musicBrainzReleaseGroupId, + "MusicBrainz Release Group Id" + ); + } + + @test + public musicBrainzReleaseId() { + this.testUserTextFrameProperty( + (t, v) => { t.musicBrainzReleaseId = v; }, + (t) => t.musicBrainzReleaseId, + "MusicBrainz Album Id" + ); + } + + @test + public musicBrainzReleaseArtistId() { + this.testUserTextFrameProperty( + (t, v) => { t.musicBrainzReleaseArtistId = v; }, + (t) => t.musicBrainzReleaseArtistId, + "MusicBrainz Album Artist Id" + ); + } + + @test + public musicBrainzTrackId() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const set = (v: string) => { tag.musicBrainzTrackId = v; }; + const get = () => tag.musicBrainzTrackId; + + // Act / Assert + assert.isUndefined(tag.dateTagged); + + PropertyTests.propertyRoundTrip(set, get, "abcd-ef12-3456-7890"); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.UniqueFileIdentifierFrame); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.UFID); + assert.deepStrictEqual(( tag.frames[0]).owner, "http://musicbrainz.org"); + const expectedBytes = ByteVector.fromString("abcd-ef12-3456-7890"); + const actualBytes = ( tag.frames[0]).identifier; + assert.isTrue(ByteVector.equal(actualBytes, expectedBytes)); + + PropertyTests.propertyRoundTrip(set, get, undefined); + assert.strictEqual(tag.frames.length, 0); + } + + @test + public musicBrainsDiscId() { + this.testUserTextFrameProperty( + (t, v) => { t.musicBrainzDiscId = v; }, + (t) => t.musicBrainzDiscId, + "MusicBrainz Disc Id" + ); + } + + @test + public musicIpId() { + this.testUserTextFrameProperty( + (t, v) => { t.musicIpId = v; }, + (t) => t.musicIpId, + "MusicIP PUID" + ); + } + + @test + public amazonId() { + this.testUserTextFrameProperty( + (t, v) => { t.amazonId = v; }, + (t) => t.amazonId, + "ASIN" + ); + } + + @test + public musicBrainzReleaseStatus() { + this.testUserTextFrameProperty( + (t, v) => { t.musicBrainzReleaseStatus = v; }, + (t) => t.musicBrainzReleaseStatus, + "MusicBrainz Album Status" + ); + } + + @test + public musicBrainzReleaseType() { + this.testUserTextFrameProperty( + (t, v) => { t.musicBrainzReleaseType = v; }, + (t) => t.musicBrainzReleaseType, + "MusicBrainz Album Type" + ); + } + + @test + public musicBrainzReleaseCountry() { + this.testUserTextFrameProperty( + (t, v) => { t.musicBrainzReleaseCountry = v; }, + (t) => t.musicBrainzReleaseCountry, + "MusicBrainz Album Release Country" + ); + } + + @test + public replayGainTrackGain() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const setProp = (v: number) => { tag.replayGainTrackGain = v; }; + const getProp = () => tag.replayGainTrackGain; + + // Act / Assert + assert.isNaN(getProp()); + + PropertyTests.propertyNormalized(setProp, getProp, 1.23456, 1.23); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.UserTextInformationFrame); + assert.strictEqual(( tag.frames[0]).description, "REPLAYGAIN_TRACK_GAIN"); + assert.deepStrictEqual(( tag.frames[0]).text, ["1.23 dB"]); + + ( tag.frames[0]).text = ["1.23"]; + assert.strictEqual(tag.replayGainTrackGain, 1.23); + + ( tag.frames[0]).text = ["abcdef"]; + assert.isNaN(tag.replayGainTrackGain); + + PropertyTests.propertyNormalized(setProp, getProp, undefined, Number.NaN); + assert.strictEqual(tag.frames.length, 0); + } + + @test + public replayGainTrackPeak() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const setProp = (v: number) => { tag.replayGainTrackPeak = v; }; + const getProp = () => tag.replayGainTrackPeak; + + // Act / Assert + assert.isNaN(getProp()); + + PropertyTests.propertyNormalized(setProp, getProp, 1.23456789, 1.234568); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.UserTextInformationFrame); + assert.strictEqual(( tag.frames[0]).description, "REPLAYGAIN_TRACK_PEAK"); + assert.deepStrictEqual(( tag.frames[0]).text, ["1.234568"]); + + ( tag.frames[0]).text = ["abcdef"]; + assert.isNaN(tag.replayGainTrackPeak); + + PropertyTests.propertyNormalized(setProp, getProp, undefined, Number.NaN); + assert.strictEqual(tag.frames.length, 0); + } + + @test + public replayGainAlbumGain() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const setProp = (v: number) => { tag.replayGainAlbumGain = v; }; + const getProp = () => tag.replayGainAlbumGain; + + // Act / Assert + assert.isNaN(getProp()); + + PropertyTests.propertyNormalized(setProp, getProp, 1.23456, 1.23); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.UserTextInformationFrame); + assert.strictEqual(( tag.frames[0]).description, "REPLAYGAIN_ALBUM_GAIN"); + assert.deepStrictEqual(( tag.frames[0]).text, ["1.23 dB"]); + + ( tag.frames[0]).text = ["1.23"]; + assert.strictEqual(tag.replayGainAlbumGain, 1.23); + + ( tag.frames[0]).text = ["abcdef"]; + assert.isNaN(tag.replayGainAlbumGain); + + PropertyTests.propertyNormalized(setProp, getProp, undefined, Number.NaN); + assert.strictEqual(tag.frames.length, 0); + } + + @test + public replayGainAlbumPeak() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const setProp = (v: number) => { tag.replayGainAlbumPeak = v; }; + const getProp = () => tag.replayGainAlbumPeak; + + // Act / Assert + assert.isNaN(getProp()); + + PropertyTests.propertyNormalized(setProp, getProp, 1.23456789, 1.234568); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.UserTextInformationFrame); + assert.strictEqual(( tag.frames[0]).description, "REPLAYGAIN_ALBUM_PEAK"); + assert.deepStrictEqual(( tag.frames[0]).text, ["1.234568"]); + + ( tag.frames[0]).text = ["abcdef"]; + assert.isNaN(tag.replayGainAlbumPeak); + + PropertyTests.propertyNormalized(setProp, getProp, undefined, Number.NaN); + assert.strictEqual(tag.frames.length, 0); + } + + @test + public initialKey() { + this.testTextFrameProperty( + (t, v) => { t.initialKey = v; }, + (t) => t.initialKey, + FrameIdentifiers.TKEY + ); + } + + @test + public remixedBy() { + this.testTextFrameProperty( + (t, v) => { t.remixedBy = v; }, + (t) => t.remixedBy, + FrameIdentifiers.TPE4 + ); + } + + @test + public publisher() { + this.testTextFrameProperty( + (t, v) => { t.publisher = v; }, + (t) => t.publisher, + FrameIdentifiers.TPUB + ); + } + + @test + public isrc() { + this.testTextFrameProperty( + (t, v) => { t.isrc = v; }, + (t) => t.isrc, + FrameIdentifiers.TSRC + ); + } + + @test + public pictures() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const mockPicture1 = TypeMoq.Mock.ofType(); + const mockPicture2 = TypeMoq.Mock.ofType(); + const pictures = [ + mockPicture1.object, + mockPicture2.object + ]; + + // Act / Assert + assert.ok(tag.pictures); + assert.isEmpty(tag.pictures); + + tag.pictures = pictures; + assert.strictEqual(tag.frames.length, 2); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.AttachmentFrame); + assert.strictEqual(tag.frames[1].frameClassType, FrameClassType.AttachmentFrame); + + tag.pictures = undefined; + assert.deepStrictEqual(tag.pictures, []); + } + + @test + public isEmpty() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + + // Act / Assert + assert.isTrue(tag.isEmpty); + + tag.isrc = "1234"; + assert.isFalse(tag.isEmpty); + } + + private testArrayTextFrameProperty( + set: (t: Id3v2Tag, v: string[]) => void, + get: (t: Id3v2Tag) => string[], + fId: FrameIdentifier + ) { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const setProp = (v: string[]) => set(tag, v); + const getProp = () => get(tag); + + // Act / Assert + assert.deepStrictEqual(getProp(), []); + + PropertyTests.propertyRoundTrip(setProp, getProp, ["foo", "bar", "baz"]); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); + assert.strictEqual(tag.frames[0].frameId, fId); + assert.deepStrictEqual(( tag.frames[0]).text, ["foo", "bar", "baz"]); + + PropertyTests.propertyRoundTrip(setProp, getProp, []); + assert.strictEqual(tag.frames.length, 0); + } private testTextFrameProperty( set: (t: Id3v2Tag, v: string) => void, @@ -978,7 +1410,7 @@ class Id3v2_Tag_PropertyTests { fId: FrameIdentifier ) { // Arrange - const tag = new Id3v2Tag(); + const tag = Id3v2Tag.fromEmpty(); const setProp = (v: string) => set(tag, v); const getProp = () => get(tag); @@ -995,26 +1427,26 @@ class Id3v2_Tag_PropertyTests { assert.strictEqual(tag.frames.length, 0); } - private testArrayTextFrameProperty( - set: (t: Id3v2Tag, v: string[]) => void, - get: (t: Id3v2Tag) => string[], - fId: FrameIdentifier + private testUserTextFrameProperty( + set: (t: Id3v2Tag, v: string) => void, + get: (t: Id3v2Tag) => string, + desc: string ) { // Arrange - const tag = new Id3v2Tag(); - const setProp = (v: string[]) => set(tag, v); + const tag = Id3v2Tag.fromEmpty(); + const setProp = (v: string) => set(tag, v); const getProp = () => get(tag); // Act / Assert - assert.deepStrictEqual(getProp(), []); + assert.isUndefined(getProp()); - PropertyTests.propertyRoundTrip(setProp, getProp, ["foo", "bar", "baz"]); + PropertyTests.propertyRoundTrip(setProp, getProp, "foo"); assert.strictEqual(tag.frames.length, 1); - assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); - assert.strictEqual(tag.frames[0].frameId, fId); - assert.deepStrictEqual(( tag.frames[0]).text, ["foo", "bar", "baz"]); + assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.UserTextInformationFrame); + assert.strictEqual(( tag.frames[0]).description, desc); + assert.deepStrictEqual(( tag.frames[0]).text, ["foo"]); - PropertyTests.propertyRoundTrip(setProp, getProp, []); + PropertyTests.propertyRoundTrip(setProp, getProp, undefined); assert.strictEqual(tag.frames.length, 0); } } diff --git a/test/utilities/testFile.ts b/test/utilities/testFile.ts index be214915..9ec18ac2 100644 --- a/test/utilities/testFile.ts +++ b/test/utilities/testFile.ts @@ -1,6 +1,5 @@ import {ByteVector} from "../../src/byteVector"; import * as TypeMoq from "typemoq"; -import {SeekOrigin} from "../../src/stream"; import {File} from "../../src/file"; export default { From fccc12e69e8e02fddab21673c4a892fdd6e13ce0 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sun, 8 Mar 2020 23:33:27 -0400 Subject: [PATCH 56/71] Method tests for id3v2Tag --- src/id3v2/frameIdentifiers.ts | 4 +- src/id3v2/frames/frame.ts | 2 +- src/id3v2/id3v2Tag.ts | 89 +++++-- src/id3v2/id3v2TagFooter.ts | 46 +++- src/id3v2/id3v2TagHeader.ts | 29 ++- test/id3v2/id3v2TagTests.ts | 476 +++++++++++++++++++++++++++++++++- test/id3v2/tagFooterTests.ts | 36 +-- test/id3v2/tagHeaderTests.ts | 50 ++-- 8 files changed, 651 insertions(+), 81 deletions(-) diff --git a/src/id3v2/frameIdentifiers.ts b/src/id3v2/frameIdentifiers.ts index 7803645d..8b0423f6 100644 --- a/src/id3v2/frameIdentifiers.ts +++ b/src/id3v2/frameIdentifiers.ts @@ -292,8 +292,8 @@ export const FrameIdentifiers: {[key: string]: FrameIdentifier} = { TXT: uniqueFrameIdentifiers.TEXT, TXX: uniqueFrameIdentifiers.TXXX, TXXX: uniqueFrameIdentifiers.TXXX, - TYE: uniqueFrameIdentifiers.TDRC, - TYER: uniqueFrameIdentifiers.TDRC, + TYE: uniqueFrameIdentifiers.TYER, + TYER: uniqueFrameIdentifiers.TYER, UFI: uniqueFrameIdentifiers.UFID, UFID: uniqueFrameIdentifiers.UFID, ULT: uniqueFrameIdentifiers.USLT, diff --git a/src/id3v2/frames/frame.ts b/src/id3v2/frames/frame.ts index 04faea11..e2a0d269 100644 --- a/src/id3v2/frames/frame.ts +++ b/src/id3v2/frames/frame.ts @@ -179,7 +179,7 @@ export abstract class Frame { frontData.addByte(this._encryptionId); } // @FIXME: Implement compression - if ((this.flags & Id3v2FrameFlags.Desynchronized) !== 0) { + if ((this.flags & Id3v2FrameFlags.Compression) !== 0) { throw new NotImplementedError("Compression is not yet supported"); } // @FIXME: Implement encryption diff --git a/src/id3v2/id3v2Tag.ts b/src/id3v2/id3v2Tag.ts index 6d741738..8d2dad8e 100644 --- a/src/id3v2/id3v2Tag.ts +++ b/src/id3v2/id3v2Tag.ts @@ -21,6 +21,7 @@ import {Tag, TagTypes} from "../tag"; import {TextInformationFrame, UserTextInformationFrame} from "./frames/textInformationFrame"; import {UrlLinkFrame} from "./frames/urlLinkFrame"; import {Guards} from "../utils"; +import Id3v2TagFooter from "./id3v2TagFooter"; export default class Id3v2Tag extends Tag { private static _language: string = undefined; // @TODO: Use the os-locale module to supply a @@ -42,7 +43,7 @@ export default class Id3v2Tag extends Tag { public static fromEmpty(): Id3v2Tag { const tag = new Id3v2Tag(); - + tag._header = new Id3v2TagHeader(); return tag; } @@ -79,7 +80,7 @@ export default class Id3v2Tag extends Tag { } const tag = new Id3v2Tag(); - tag._header = new Id3v2TagHeader(data); + tag._header = Id3v2TagHeader.fromData(data); // If the tag size is 0, then this is an invalid tag. Tags must contain at least one frame if (tag._header.tagSize === 0) { @@ -154,18 +155,68 @@ export default class Id3v2Tag extends Tag { * Gets the ID3v2 version for the current instance. */ public get version(): number { - return Id3v2Settings.forceDefaultVersion - ? Id3v2Settings.defaultVersion - : this._header.majorVersion; + return this._header.majorVersion; } /** * Sets the ID3v2 version for the current instance. * @param value ID3v2 version for the current instance. Must be 2, 3, or 4. */ public set version(value: number) { - Guards.byte(value, "value"); - Guards.betweenInclusive(value, 2, 4, "value"); + const originalVersion = this._header.majorVersion; this._header.majorVersion = value; + + // Migrate any incompatible frames that have direct migrations + if (value === 4 && (originalVersion === 2 || originalVersion === 3)) { + // * TYER, etc -> TDRC + const tyerFrames = this.getFramesByIdentifier( + FrameClassType.TextInformationFrame, + FrameIdentifiers.TYER + ); + const tdatFrames = this.getFramesByIdentifier( + FrameClassType.TextInformationFrame, + FrameIdentifiers.TDAT + ); + const timeFrames = this.getFramesByIdentifier( + FrameClassType.TextInformationFrame, + FrameIdentifiers.TIME + ); + if (tyerFrames.length > 0) { + this.removeFrames(FrameIdentifiers.TYER); + this.removeFrames(FrameIdentifiers.TDAT); + this.removeFrames(FrameIdentifiers.TIME); + + let tdrcText = tyerFrames[0].text[0]; + if (tdatFrames.length > 0) { + const tdatText = tdatFrames[0].text[0]; + tdrcText += `-${tdatText.substr(0, 2)}-${tdatText.substr(2, 2)}`; + if (timeFrames.length > 0) { + const timeText = timeFrames[0].text[0]; + tdrcText += `T${timeText}`; + } + } + + this.setTextFrame(FrameIdentifiers.TDRC, tdrcText); + } + } else if (originalVersion === 4 && (value === 2 || value === 3)) { + // * TDRC -> TYER, etc + const tdrcFrames = this.getFramesByIdentifier( + FrameClassType.TextInformationFrame, + FrameIdentifiers.TDRC + ); + if (tdrcFrames.length > 0) { + const tdrcText = tdrcFrames[0].text[0]; + this.removeFrames(FrameIdentifiers.TDRC); + this.setTextFrame(FrameIdentifiers.TYER, tdrcText.substr(0, 4)); + + if (tdrcText.length >= 10) { + this.setTextFrame(FrameIdentifiers.TDAT, tdrcText.substr(6, 5).replace("-", "")); + } + + if (tdrcText.length === 19) { + this.setTextFrame(FrameIdentifiers.TIME, tdrcText.substr(11, 8)); + } + } + } } // #region Tag Implementations @@ -376,7 +427,7 @@ export default class Id3v2Tag extends Tag { if (tyerText && tyerText.length >= 4) { // @TODO: Check places where we use this pattern... .parseInt doesn't parse the whole string if it started // with good data - return Number.parseInt(tdrcText.substr(0, 4), 10); + return Number.parseInt(tyerText.substr(0, 4), 10); } // Case 3: Neither, return 0 @@ -740,10 +791,10 @@ export default class Id3v2Tag extends Tag { * @returns TFrame[] Array of frames with the specified class type */ public getFramesByClassType(type: FrameClassType): TFrame[] { + // TODO: Can we access static properties from TFrame? if so, can we use that to get the frame class type? Guards.notNullOrUndefined(type, "type"); - return this._frameList.filter((f) => f && f.frameClassType === type) - .map((f) => f); + return this._frameList.filter((f) => f && f.frameClassType === type); } /** @@ -760,8 +811,7 @@ export default class Id3v2Tag extends Tag { Guards.notNullOrUndefined(type, "type"); Guards.truthy(ident, "ident"); - return this._frameList.filter((f) => f && f.frameClassType === type && f.frameId === ident) - .map((f) => f); + return this._frameList.filter((f) => f && f.frameClassType === type && f.frameId === ident); } /** @@ -823,8 +873,8 @@ export default class Id3v2Tag extends Tag { */ public render(): ByteVector { // Convert the perfmers role to the TMCL frame - const ret: string[] = undefined; if (this._performersRole) { + const ret: string[] = undefined; const map: {[key: string]: string} = {}; for (let i = 0; i < this._performersRole.length; i++) { const insts = this._performersRole[i]; @@ -852,9 +902,9 @@ export default class Id3v2Tag extends Tag { ret.push(key); ret.push(map[key]); } - } - this.setTextFrame(FrameIdentifiers.TMCL, ...ret); + this.setTextFrame(FrameIdentifiers.TMCL, ...ret); + } // We need to render the "tag data" first so that we have to correct size to render in the // tag's header. The "tag data" (everything that is included in Header.tagSize) includes @@ -905,6 +955,13 @@ export default class Id3v2Tag extends Tag { tagData.addByteVector(ByteVector.fromSize(size)); } + // Set the tag size and add the header/footer + this._header.tagSize = tagData.length; + tagData.insertByteVector(0, this._header.render()); + if (hasFooter) { + tagData.addByteVector(Id3v2TagFooter.fromHeader(this._header).render()); + } + return tagData; } @@ -1076,7 +1133,7 @@ export default class Id3v2Tag extends Tag { file.mode = FileAccessMode.Read; file.seek(position); - this._header = new Id3v2TagHeader(file.readBlock(Id3v2Settings.headerSize)); + this._header = Id3v2TagHeader.fromData(file.readBlock(Id3v2Settings.headerSize)); // If the tag size is 0, then this is an invalid tag. Tags must contain at least one frame. if (this._header.tagSize === 0) { diff --git a/src/id3v2/id3v2TagFooter.ts b/src/id3v2/id3v2TagFooter.ts index 09a2ad59..e3cf7ceb 100644 --- a/src/id3v2/id3v2TagFooter.ts +++ b/src/id3v2/id3v2TagFooter.ts @@ -2,21 +2,21 @@ import Id3v2Settings from "./id3v2Settings"; import SyncData from "./syncData"; import {ByteVector} from "../byteVector"; import {CorruptFileError} from "../errors"; -import {Id3v2TagHeaderFlags} from "./id3v2TagHeader"; +import {Id3v2TagHeader, Id3v2TagHeaderFlags} from "./id3v2TagHeader"; import {Guards} from "../utils"; export default class Id3v2TagFooter { private static readonly _fileIdentifier: ByteVector = ByteVector.fromString("3DI", undefined, undefined, true); - private _flags: Id3v2TagHeaderFlags; - private _majorVersion: number; - private _revisionNumber: number; - private _tagSize: number; + private _flags: Id3v2TagHeaderFlags = Id3v2TagHeaderFlags.FooterPresent; + private _majorVersion: number = 0; + private _revisionNumber: number = 0; + private _tagSize: number = 0; /** * Constructs and initializes a new instance by reading it from raw footer data. * @param data Raw data to build the instance from */ - public constructor(data: ByteVector) { + public static fromData(data: ByteVector): Id3v2TagFooter { Guards.truthy(data, "data"); if (data.length < Id3v2Settings.footerSize) { throw new CorruptFileError("Provided data is smaller than object size."); @@ -25,18 +25,19 @@ export default class Id3v2TagFooter { throw new CorruptFileError("Provided data does not start with the file identifier"); } - this._majorVersion = data.get(3); - this._revisionNumber = data.get(4); - this._flags = data.get(5); + const footer = new Id3v2TagFooter(); + footer._majorVersion = data.get(3); + footer._revisionNumber = data.get(4); + footer._flags = data.get(5); // TODO: Is there any point to supporting footers on versions less than 4? - if (this._majorVersion === 2 && (this._flags & 127) !== 0) { + if (footer._majorVersion === 2 && (footer._flags & 127) !== 0) { throw new CorruptFileError("Invalid flags set on version 2 tag"); } - if (this._majorVersion === 3 && (this._flags & 15) !== 0) { + if (footer._majorVersion === 3 && (footer._flags & 15) !== 0) { throw new CorruptFileError("Invalid flags set on version 3 tag"); } - if (this._majorVersion === 4 && (this._flags & 7) !== 0) { + if (footer._majorVersion === 4 && (footer._flags & 7) !== 0) { throw new CorruptFileError("Invalid flags set on version 4 tag"); } @@ -46,7 +47,26 @@ export default class Id3v2TagFooter { } } - this.tagSize = SyncData.toUint(data.mid(6, 4)); + footer.tagSize = SyncData.toUint(data.mid(6, 4)); + + return footer; + } + + /** + * Constructs and initializes a new footer based on the contents of the header used for the + * same tag. + * @param header Header from which to base the new footer + */ + public static fromHeader(header: Id3v2TagHeader): Id3v2TagFooter { + Guards.truthy(header, "header"); + + const footer = new Id3v2TagFooter(); + footer._majorVersion = header.majorVersion; + footer._revisionNumber = header.revisionNumber; + footer._flags = header.flags | Id3v2TagHeaderFlags.FooterPresent; + footer._tagSize = header.tagSize; + + return footer; } // #region Properties diff --git a/src/id3v2/id3v2TagHeader.ts b/src/id3v2/id3v2TagHeader.ts index 41a7a3f5..1de6ce6b 100644 --- a/src/id3v2/id3v2TagHeader.ts +++ b/src/id3v2/id3v2TagHeader.ts @@ -38,16 +38,16 @@ export class Id3v2TagHeader { undefined, true ); - private _flags: Id3v2TagHeaderFlags; - private _majorVersion: number; - private _revisionNumber: number; - private _tagSize: number; + private _flags: Id3v2TagHeaderFlags = Id3v2TagHeaderFlags.None; + private _majorVersion: number = 0; + private _revisionNumber: number = 0; + private _tagSize: number = 0; /** * Constructs and initializes a new instance by reading it from the raw header data. * @param data Object containing the raw data to build the new instance from. */ - public constructor(data: ByteVector) { + public static fromData(data: ByteVector): Id3v2TagHeader { Guards.truthy(data, "data"); if (data.length < Id3v2Settings.headerSize) { throw new CorruptFileError("Provided data is smaller than object size"); @@ -56,18 +56,19 @@ export class Id3v2TagHeader { throw new CorruptFileError("Provided data does not start with the file identifier"); } - this._majorVersion = data.get(3); - this._revisionNumber = data.get(4); - this._flags = data.get(5); + const header = new Id3v2TagHeader(); + header._majorVersion = data.get(3); + header._revisionNumber = data.get(4); + header._flags = data.get(5); // Make sure flags provided are legal - if (this._majorVersion === 2 && (this._flags & 63) !== 0) { + if (header._majorVersion === 2 && (header._flags & 63) !== 0) { throw new CorruptFileError("Invalid flags set on version 2 tag"); } - if (this._majorVersion === 3 && (this._flags & 15) > 0) { + if (header._majorVersion === 3 && (header._flags & 15) > 0) { throw new CorruptFileError("Invalid flags set on version 3 tag"); } - if (this._majorVersion === 4 && (this._flags & 7) > 0) { + if (header._majorVersion === 4 && (header._flags & 7) > 0) { throw new CorruptFileError("Invalid flags set on version 4 tag"); } @@ -77,7 +78,9 @@ export class Id3v2TagHeader { throw new CorruptFileError("One of the bytes in the tag size was greater than the allowed 128"); } } - this.tagSize = SyncData.toUint(data.mid(6, 4)); + header.tagSize = SyncData.toUint(data.mid(6, 4)); + + return header; } // #region Properties @@ -124,7 +127,7 @@ export class Id3v2TagHeader { * Gets the major version of the tag described by the current instance. */ public get majorVersion(): number { - return this._majorVersion === 0 + return this._majorVersion === 0 || Id3v2Settings.forceDefaultVersion ? Id3v2Settings.defaultVersion : this._majorVersion; } diff --git a/test/id3v2/id3v2TagTests.ts b/test/id3v2/id3v2TagTests.ts index b7150ea8..fe0a3032 100644 --- a/test/id3v2/id3v2TagTests.ts +++ b/test/id3v2/id3v2TagTests.ts @@ -1,3 +1,4 @@ +import * as BigInt from "big-integer"; import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; import * as TypeMoq from "typemoq"; @@ -8,7 +9,7 @@ import SyncData from "../../src/id3v2/syncData"; import TestFile from "../utilities/testFile"; import {ByteVector, StringType} from "../../src/byteVector"; import {File, ReadStyle} from "../../src/file"; -import {Id3v2TagHeaderFlags} from "../../src/id3v2/id3v2TagHeader"; +import {Id3v2TagHeader, Id3v2TagHeaderFlags} from "../../src/id3v2/id3v2TagHeader"; import PlayCountFrame from "../../src/id3v2/frames/playCountFrame"; import UniqueFileIdentifierFrame from "../../src/id3v2/frames/uniqueFileIdentifierFrame"; import UnknownFrame from "../../src/id3v2/frames/unknownFrame"; @@ -21,6 +22,9 @@ import CommentsFrame from "../../src/id3v2/frames/commentsFrame"; import Id3v2Settings from "../../src/id3v2/id3v2Settings"; import UnsynchronizedLyricsFrame from "../../src/id3v2/frames/unsynchronizedLyricsFrame"; import {IPicture} from "../../src/picture"; +import {UrlLinkFrame} from "../../src/id3v2/frames/urlLinkFrame"; +import Id3v2TagFooter from "../../src/id3v2/id3v2TagFooter"; +import {Id3v2FrameFlags} from "../../src/id3v2/frames/frameHeader"; // Setup Chai Chai.use(ChaiAsPromised); @@ -299,8 +303,31 @@ class Id3v2_Tag_PropertyTests { } @test - public version() { - // TODO: Need to figure out what to do if header doesn't exist + public version_invalidValue() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const set = (v: number) => { tag.version = v; }; + + // Act / Assert + PropertyTests.propertyThrows(set, -1); + PropertyTests.propertyThrows(set, 1.23); + PropertyTests.propertyThrows(set, 0); + PropertyTests.propertyThrows(set, 5); + PropertyTests.propertyThrows(set, 1); + } + + @test + public version_validValue() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const set = (v: number) => { tag.version = v; }; + const get = () => tag.version; + + // Act / Assert + Id3v2Settings.forceDefaultVersion = false; + PropertyTests.propertyRoundTrip(set, get, 2); + PropertyTests.propertyRoundTrip(set, get, 3); + PropertyTests.propertyRoundTrip(set, get, 4); } @test @@ -1450,3 +1477,446 @@ class Id3v2_Tag_PropertyTests { assert.strictEqual(tag.frames.length, 0); } } + +@suite(slow(1000), timeout(3000)) +class Id3v2_Tag_MethodTests { + @test + public clear() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + tag.frames.push(PlayCountFrame.fromEmpty()); + tag.frames.push(PlayCountFrame.fromEmpty()); + + // Act + tag.clear(); + + // Assert + assert.isOk(tag.frames); + assert.isEmpty(tag.frames); + } + + @test + public copyTo_invalidDestination() { + // Arrange + const source = Id3v2Tag.fromEmpty(); + + // Act / Assert + assert.throws(() => { source.copyTo(undefined, true); }); + assert.throws(() => { source.copyTo(null, true); }); + // TODO: Add a test for wrong tag type when we have more tag types. + } + + @test + public copyTo_noOverwrite() { + // Arrange + const source = Id3v2Tag.fromEmpty(); + const sFrame1 = PlayCountFrame.fromEmpty(); + sFrame1.playCount = BigInt(123); + const sFrame2 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + sFrame2.text = ["foo", "bar"]; + source.frames.push(sFrame1, sFrame2); + + const dest = Id3v2Tag.fromEmpty(); + const dFrame1 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + dest.frames.push(dFrame1); + + // Act + source.copyTo(dest, false); + + // Assert + assert.strictEqual(source.frames.length, 2); + assert.sameMembers(source.frames, [sFrame1, sFrame2]); + + assert.strictEqual(dest.frames.length, 2); + assert.notOwnInclude(dest.frames, sFrame1); + assert.notOwnInclude(dest.frames, sFrame2); + assert.ownInclude(dest.frames, dFrame1); + + const dPcnt = dest.getFramesByIdentifier( + FrameClassType.PlayCountFrame, + FrameIdentifiers.PCNT + )[0]; + assert.isTrue(dPcnt.playCount.eq(sFrame1.playCount)); + } + + @test + public copyTo_overwrite() { + // Arrange + const source = Id3v2Tag.fromEmpty(); + const sFrame1 = PlayCountFrame.fromEmpty(); + sFrame1.playCount = BigInt(123); + const sFrame2 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + sFrame2.text = ["foo", "bar"]; + source.frames.push(sFrame1, sFrame2); + + const dest = Id3v2Tag.fromEmpty(); + const dFrame1 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + dest.frames.push(dFrame1); + + // Act + source.copyTo(dest, true); + + // Assert + assert.strictEqual(source.frames.length, 2); + assert.sameMembers(source.frames, [sFrame1, sFrame2]); + + assert.strictEqual(dest.frames.length, 2); + assert.notOwnInclude(dest.frames, sFrame1); + assert.notOwnInclude(dest.frames, sFrame2); + + const dTcom = dest.getFramesByIdentifier( + FrameClassType.TextInformationFrame, + FrameIdentifiers.TCOM + )[0]; + assert.deepStrictEqual(dTcom.text, ["foo", "bar"]); + + const dPcnt = dest.getFramesByIdentifier( + FrameClassType.PlayCountFrame, + FrameIdentifiers.PCNT + )[0]; + assert.isTrue(dPcnt.playCount.eq(sFrame1.playCount)); + } + + @test + public getFramesByClassType_invalidClassType() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + + // Act / Assert + assert.throws(() => { tag.getFramesByClassType(undefined); }); + assert.throws(() => { tag.getFramesByClassType(null); }); + } + + @test + public getFramesByClassType_hasMatches() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const frame1 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + const frame2 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCON); + const frame3 = PlayCountFrame.fromEmpty(); + tag.frames.push(frame1, frame2, frame3); + + // Act + const result = tag.getFramesByClassType(FrameClassType.TextInformationFrame); + + // Assert + assert.isArray(result); + assert.strictEqual(result.length, 2); + assert.sameMembers(result, [frame1, frame2]); + } + + @test + public getFramesByClassType_noMatches() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + tag.frames.push(PlayCountFrame.fromEmpty()); + + // Act + const result = tag.getFramesByClassType(FrameClassType.TextInformationFrame); + + // Assert + assert.isArray(result); + assert.isEmpty(result); + } + + @test + public getFramesByIdentifier_invalidClassType() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + + // Act / Assert + assert.throws(() => { tag.getFramesByIdentifier(undefined, FrameIdentifiers.TCOM); }); + assert.throws(() => { tag.getFramesByIdentifier(null, FrameIdentifiers.TCOM); }); + } + + @test + public getFramesByIdentifier_invalidIdentifier() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const classType = FrameClassType.TextInformationFrame; + + // Act / Assert + assert.throws(() => { tag.getFramesByIdentifier(classType, undefined); }); + assert.throws(() => { tag.getFramesByIdentifier(classType, null); }); + } + + @test + public getFramesByIdentifier_hasMatches() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const frame1 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + const frame2 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + const frame3 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCON); + tag.frames.push(frame1, frame2, frame3); + + // Act + const result = tag.getFramesByIdentifier( + FrameClassType.TextInformationFrame, + FrameIdentifiers.TCOM + ); + + // Assert + assert.isArray(result); + assert.strictEqual(result.length, 2); + assert.sameMembers(result, [frame1, frame2]); + } + + @test + public getFramesByIdentifier_noMatches() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + tag.frames.push(TextInformationFrame.fromIdentifier(FrameIdentifiers.TCON)); + + // Act + const result = tag.getFramesByIdentifier( + FrameClassType.TextInformationFrame, + FrameIdentifiers.TCOM + ); + + // Assert + assert.isArray(result); + assert.isEmpty(result); + } + + @test + public getTextAsString_invalidIdentity() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + + // Act / Assert + assert.throws(() => { tag.getTextAsString(undefined); }); + assert.throws(() => { tag.getTextAsString(null); }); + } + + @test + public getTextAsString_urlFrame() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const frame1 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + const frame2 = UrlLinkFrame.fromIdentity(FrameIdentifiers.WCOM); + frame2.text = ["foo"]; + const frame3 = UrlLinkFrame.fromIdentity(FrameIdentifiers.WCOM); + frame3.text = ["bar"]; + tag.frames.push(frame1, frame2, frame3); + + // Act + const result = tag.getTextAsString(FrameIdentifiers.WCOM); + + // Assert + assert.strictEqual(result, "foo"); + } + + @test + public getTextAsString_textFrame() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const frame1 = UrlLinkFrame.fromIdentity(FrameIdentifiers.WCOM); + const frame2 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + frame2.text = ["foo"]; + const frame3 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + frame3.text = ["bar"]; + tag.frames.push(frame1, frame2, frame3); + + // Act + const result = tag.getTextAsString(FrameIdentifiers.TCOM); + + // Assert + assert.strictEqual(result, "foo"); + } + + @test + public getTextAsString_noMatches() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const frame1 = UrlLinkFrame.fromIdentity(FrameIdentifiers.WCOM); + tag.frames.push(frame1); + + // Act + const result = tag.getTextAsString(FrameIdentifiers.TCOM); + + // Assert + assert.isUndefined(result); + } + + @test + public removeFrame_invalidFrame() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + + // Act / Assert + assert.throws(() => tag.removeFrame(undefined)); + assert.throws(() => tag.removeFrame(null)); + } + + @test + public removeFrame_frameExists() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const frame = PlayCountFrame.fromEmpty(); + tag.frames.push(frame); + + // Act + tag.removeFrame(frame); + + // Assert + assert.isEmpty(tag.frames); + } + + @test + public removeFrame_frameDoesNotExist() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const frame1 = PlayCountFrame.fromEmpty(); + const frame2 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + tag.frames.push(frame2); + + // Act + tag.removeFrame(frame1); + + // Assert + assert.strictEqual(tag.frames.length, 1); + assert.deepStrictEqual(tag.frames, [frame2]); + } + + @test + public render_v4_noFooter() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + tag.version = 4; + + const frame1 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + const frame1Data = frame1.render(4); + const frame2 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCON); + const frame2Data = frame2.render(4); + tag.frames.push(frame1, frame2); + + // Act + const output = tag.render(); + + // Assert + const header = new Id3v2TagHeader(); + header.tagSize = 1024 + frame1Data.length + frame2Data.length; + header.majorVersion = 4; + const expected = ByteVector.concatenate( + header.render(), + frame1Data, + frame2Data, + ByteVector.fromSize(1024, 0x00) + ); + + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_v4_hasFooter() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + tag.version = 4; + tag.flags = Id3v2TagHeaderFlags.FooterPresent; + + const frame1 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + const frame1Data = frame1.render(4); + const frame2 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCON); + const frame2Data = frame2.render(4); + tag.frames.push(frame1, frame2); + + // Act + const output = tag.render(); + + // Assert + const header = new Id3v2TagHeader(); + header.tagSize = frame1Data.length + frame2Data.length; + header.majorVersion = 4; + header.flags = Id3v2TagHeaderFlags.FooterPresent; + const expected = ByteVector.concatenate( + header.render(), + frame1Data, + frame2Data, + Id3v2TagFooter.fromHeader(header).render() + ); + + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_v4_unsyncAtFrameLevel() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + tag.version = 4; + tag.flags = Id3v2TagHeaderFlags.Unsynchronication; + + const frame1 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + const frame2 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCON); + tag.frames.push(frame1, frame2); + + // Act + const output = tag.render(); + + // Assert + frame1.flags |= Id3v2FrameFlags.Desynchronized; + frame2.flags |= Id3v2FrameFlags.Desynchronized; + const frame1Data = frame1.render(4); + const frame2Data = frame2.render(4); + + const header = new Id3v2TagHeader(); + header.tagSize = 1024 + frame1Data.length + frame2Data.length; + header.flags = Id3v2TagHeaderFlags.Unsynchronication; + header.majorVersion = 4; + + const expected = ByteVector.concatenate( + header.render(), + frame1Data, + frame2Data, + ByteVector.fromSize(1024, 0x00) + ); + + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_v3_unsyncAtTagLevel() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + tag.version = 3; + tag.flags = Id3v2TagHeaderFlags.Unsynchronication; + + const frame1 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + const frame1Data = frame1.render(3); + const frame2 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCON); + const frame2Data = frame2.render(3); + tag.frames.push(frame1, frame2); + + // Act + const output = tag.render(); + + // Assert + const frameData = ByteVector.concatenate(frame1Data, frame2Data); + SyncData.unsyncByteVector(frameData); + + const header = new Id3v2TagHeader(); + header.flags = Id3v2TagHeaderFlags.Unsynchronication; + header.tagSize = 1024 + frameData.length; + header.majorVersion = 3; + + const expected = ByteVector.concatenate( + header.render(), + frameData, + ByteVector.fromSize(1024, 0x00) + ); + + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_v4_unsupportedFrameForVersion() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + tag.version = 4; + + const frame1 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TYER); + tag.frames.push(frame1); + + // Act / Assert + assert.throws(() => { const _ = tag.render(); }); + } +} diff --git a/test/id3v2/tagFooterTests.ts b/test/id3v2/tagFooterTests.ts index f7365f6b..0a74b0ee 100644 --- a/test/id3v2/tagFooterTests.ts +++ b/test/id3v2/tagFooterTests.ts @@ -20,38 +20,38 @@ const getTestFooter = (majorVersion: number, minorVersion: number, flags: Id3v2T flags, TestConstants.syncedUintBytes ); - return new Id3v2TagFooter(data); + return Id3v2TagFooter.fromData(data); }; @suite(timeout(3000), slow(1000)) class Id3v2_TagFooter_ConstructorTests { @test - public falsyData() { + public fromData_falsyData() { // Act/Assert - assert.throws(() => { const _ = new Id3v2TagFooter(null); }); - assert.throws(() => { const _ = new Id3v2TagFooter(undefined); }); + assert.throws(() => { const _ = Id3v2TagFooter.fromData(null); }); + assert.throws(() => { const _ = Id3v2TagFooter.fromData(undefined); }); } @test - public invalidDataLength() { + public fromData_invalidDataLength() { // Arrange const data = ByteVector.fromSize(1); // Act/Assert - assert.throws(() => { const _ = new Id3v2TagFooter(data); }); + assert.throws(() => { const _ = Id3v2TagFooter.fromData(data); }); } @test - public missingIdentifier() { + public fromData_missingIdentifier() { // Arrange const data = ByteVector.fromSize(10); // Act/Assert - assert.throws(() => { const _ = new Id3v2TagFooter(data); }); + assert.throws(() => { const _ = Id3v2TagFooter.fromData(data); }); } @test - public invalidFlags_version4() { + public fromData_invalidFlags_version4() { // Arrange const data = ByteVector.concatenate( Id3v2TagFooter.fileIdentifier, @@ -59,11 +59,11 @@ class Id3v2_TagFooter_ConstructorTests { ); // Act/Assert - assert.throws(() => { const _ = new Id3v2TagFooter(data); }); + assert.throws(() => { const _ = Id3v2TagFooter.fromData(data); }); } @test - public invalidTagSizeBytes() { + public fromData_invalidTagSizeBytes() { // Arrange const testData = ByteVector.concatenate( Id3v2TagFooter.fileIdentifier, @@ -75,14 +75,14 @@ class Id3v2_TagFooter_ConstructorTests { const testData4 = ByteVector.concatenate(testData, 0x00, 0x00, 0x00, 0x80); // Act/Assert - assert.throws(() => { const _ = new Id3v2TagFooter(testData1); }); - assert.throws(() => { const _ = new Id3v2TagFooter(testData2); }); - assert.throws(() => { const _ = new Id3v2TagFooter(testData3); }); - assert.throws(() => { const _ = new Id3v2TagFooter(testData4); }); + assert.throws(() => { const _ = Id3v2TagFooter.fromData(testData1); }); + assert.throws(() => { const _ = Id3v2TagFooter.fromData(testData2); }); + assert.throws(() => { const _ = Id3v2TagFooter.fromData(testData3); }); + assert.throws(() => { const _ = Id3v2TagFooter.fromData(testData4); }); } @test - public validParams() { + public fromData_validParams() { // Arrange const majorVersion = 0x04; const minorVersion = 0x00; @@ -96,7 +96,7 @@ class Id3v2_TagFooter_ConstructorTests { ); // Act - const output = new Id3v2TagFooter(testData); + const output = Id3v2TagFooter.fromData(testData); // Assert assert.equal(output.flags, flags); @@ -231,7 +231,7 @@ class Id3v2_TagFooter_RenderTests { flags, TestConstants.syncedUintBytes ); - const footer = new Id3v2TagFooter(testData); + const footer = Id3v2TagFooter.fromData(testData); // Act const output = footer.render(); diff --git a/test/id3v2/tagHeaderTests.ts b/test/id3v2/tagHeaderTests.ts index 1f81d517..5558117f 100644 --- a/test/id3v2/tagHeaderTests.ts +++ b/test/id3v2/tagHeaderTests.ts @@ -19,7 +19,7 @@ const getTestHeader = (majorVersion: number, minorVersion: number, flags: Id3v2T flags, 0x10, 0x10, 0x10, 0x10 ); - return new Id3v2TagHeader(data); + return Id3v2TagHeader.fromData(data); }; @suite(timeout(3000), slow(1000)) @@ -27,8 +27,8 @@ class Id3v2_TagHeader_ConstructorTests { @test public falsyData() { // Act/Assert - assert.throws(() => { const _ = new Id3v2TagHeader(null); }); - assert.throws(() => { const _ = new Id3v2TagHeader(undefined); }); + assert.throws(() => { const _ = Id3v2TagHeader.fromData(null); }); + assert.throws(() => { const _ = Id3v2TagHeader.fromData(undefined); }); } @test @@ -38,14 +38,14 @@ class Id3v2_TagHeader_ConstructorTests { const data1 = ByteVector.fromSize(1); // Act/Assert - assert.throws(() => { const _ = new Id3v2TagHeader(data0); }); - assert.throws(() => { const _ = new Id3v2TagHeader(data1); }); + assert.throws(() => { const _ = Id3v2TagHeader.fromData(data0); }); + assert.throws(() => { const _ = Id3v2TagHeader.fromData(data1); }); } @test public invalidStartOfData() { // Act/Assert - assert.throws(() => { const _ = new Id3v2TagHeader(TestConstants.testByteVector); }); + assert.throws(() => { const _ = Id3v2TagHeader.fromData(TestConstants.testByteVector); }); } @test @@ -59,7 +59,7 @@ class Id3v2_TagHeader_ConstructorTests { ); // Act/Assert - assert.throws(() => { const _ = new Id3v2TagHeader(testData); }); + assert.throws(() => { const _ = Id3v2TagHeader.fromData(testData); }); } @test @@ -73,7 +73,7 @@ class Id3v2_TagHeader_ConstructorTests { ); // Act/Assert - assert.throws(() => { const _ = new Id3v2TagHeader(testData); }); + assert.throws(() => { const _ = Id3v2TagHeader.fromData(testData); }); } @test @@ -87,7 +87,7 @@ class Id3v2_TagHeader_ConstructorTests { ); // Act/Assert - assert.throws(() => { const _ = new Id3v2TagHeader(testData); }); + assert.throws(() => { const _ = Id3v2TagHeader.fromData(testData); }); } @test @@ -104,10 +104,10 @@ class Id3v2_TagHeader_ConstructorTests { const testData4 = ByteVector.concatenate(testData, 0x00, 0x00, 0x00, 0x80); // Act/Assert - assert.throws(() => {const _ = new Id3v2TagHeader(testData1); }); - assert.throws(() => {const _ = new Id3v2TagHeader(testData2); }); - assert.throws(() => {const _ = new Id3v2TagHeader(testData3); }); - assert.throws(() => {const _ = new Id3v2TagHeader(testData4); }); + assert.throws(() => {const _ = Id3v2TagHeader.fromData(testData1); }); + assert.throws(() => {const _ = Id3v2TagHeader.fromData(testData2); }); + assert.throws(() => {const _ = Id3v2TagHeader.fromData(testData3); }); + assert.throws(() => {const _ = Id3v2TagHeader.fromData(testData4); }); } @test @@ -125,7 +125,7 @@ class Id3v2_TagHeader_ConstructorTests { ); // Act - const output = new Id3v2TagHeader(testData); + const output = Id3v2TagHeader.fromData(testData); // Assert assert.equal(output.flags, flags); @@ -236,6 +236,26 @@ class Id3v2_TagHeader_PropertyTests { assert.equal(output, 4); } + @test + public getMajorVersion_forcedDefault() { + // Arrange + const header = getTestHeader(4, 0, Id3v2TagHeaderFlags.None); + + const initialForceValue = Id3v2Settings.forceDefaultVersion; + try { + Id3v2Settings.forceDefaultVersion = true; + + // Act + const output = header.majorVersion; + + // Assert + assert.strictEqual(output, Id3v2Settings.defaultVersion); + } finally { + // Cleanup + Id3v2Settings.forceDefaultVersion = initialForceValue; + } + } + @test public setMajorVersion_invalidValues() { // Arrange @@ -341,7 +361,7 @@ class Id3v2_TagHeader_RenderTests { flags, 0x10, 0x10, 0x10, 0x10 ); - const header = new Id3v2TagHeader(testData); + const header = Id3v2TagHeader.fromData(testData); // Act const output = header.render(); From 55cb66fab20958c58c8c3e6f97c5b05f795a581b Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 10 Mar 2020 20:49:41 -0400 Subject: [PATCH 57/71] Last round of tests for Id3v2Tag --- test/id3v2/id3v2TagTests.ts | 208 ++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) diff --git a/test/id3v2/id3v2TagTests.ts b/test/id3v2/id3v2TagTests.ts index fe0a3032..8b2a7627 100644 --- a/test/id3v2/id3v2TagTests.ts +++ b/test/id3v2/id3v2TagTests.ts @@ -1919,4 +1919,212 @@ class Id3v2_Tag_MethodTests { // Act / Assert assert.throws(() => { const _ = tag.render(); }); } + + @test + public replaceFrame_invalidFrames() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const frame = PlayCountFrame.fromEmpty(); + + // Act / Assert + assert.throws(() => { tag.replaceFrame(undefined, frame); }); + assert.throws(() => { tag.replaceFrame(null, frame); }); + assert.throws(() => { tag.replaceFrame(frame, undefined); }); + assert.throws(() => { tag.replaceFrame(frame, null); }); + } + + @test + public replaceFrame_sameFrame() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const frame = PlayCountFrame.fromEmpty(); + + // Act + tag.replaceFrame(frame, frame); + + // Assert + assert.strictEqual(tag.frames.length, 0); + } + + @test + public replaceFrame_frameExists() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const oldFrame = PlayCountFrame.fromEmpty(); + const newFrame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + + tag.frames.push(oldFrame); + + // Act + tag.replaceFrame(oldFrame, newFrame); + + // Assert + assert.sameMembers(tag.frames, [newFrame]); + } + + @test + public replaceFrame_frameDoesNotExist() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const oldFrame = PlayCountFrame.fromEmpty(); + const newFrame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + const otherFrame = PlayCountFrame.fromEmpty(); + + tag.frames.push(otherFrame); + + // Act + tag.replaceFrame(oldFrame, newFrame); + + // Assert + assert.sameMembers(tag.frames, [otherFrame, newFrame]); + } + + @test + public setNumberFrame_invalidArgs() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const identifier = FrameIdentifiers.TCOM; + + // Act / Assert + assert.throws(() => { tag.setNumberFrame(undefined, 1, 1, 1); }); + assert.throws(() => { tag.setNumberFrame(null, 1, 1, 1); }); + + assert.throws(() => { tag.setNumberFrame(identifier, -1, 1, 1); }); + assert.throws(() => { tag.setNumberFrame(identifier, 1.23, 1, 1); }); + assert.throws(() => { tag.setNumberFrame(identifier, Number.MAX_SAFE_INTEGER + 1, 1, 1); }); + + assert.throws(() => { tag.setNumberFrame(identifier, 1, -1, 1); }); + assert.throws(() => { tag.setNumberFrame(identifier, 1, 1.23, 1); }); + assert.throws(() => { tag.setNumberFrame(identifier, 1, Number.MAX_SAFE_INTEGER + 1, 1); }); + + assert.throws(() => { tag.setNumberFrame(identifier, 1, 1, -1); }); + assert.throws(() => { tag.setNumberFrame(identifier, 1, 1, 1.23); }); + assert.throws(() => { tag.setNumberFrame(identifier, 1, 1, 0x100); }); + } + + @test + public setNumberFrame_removesFrame() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const frame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TRCK); + tag.frames.push(frame); + + // Act + tag.setNumberFrame(FrameIdentifiers.TRCK, 0, 0, 1); + + // Assert + assert.isEmpty(tag.frames); + } + + @test + public setNumberFrame_noDenominator() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + + // Act + tag.setNumberFrame(FrameIdentifiers.TRCK, 123, 0, 4); + + // Assert + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TRCK); + assert.deepStrictEqual(( tag.frames[0]).text, ["0123"]); + } + + @test + public setNumberFrame_withDenominator() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + + // Act + tag.setNumberFrame(FrameIdentifiers.TRCK, 123, 234, 4); + + // Assert + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TRCK); + assert.deepStrictEqual(( tag.frames[0]).text, ["0123/234"]); + } + + @test + public setTextFrame_invalidArgs() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + + // Act / Assert + assert.throws(() => { tag.setTextFrame(undefined, "foo"); }); + assert.throws(() => { tag.setTextFrame(null, "foo"); }); + } + + @test + public setTextFrame_removesFrame() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const frame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + tag.frames.push(frame); + + // Act + tag.setTextFrame(FrameIdentifiers.TCOM, undefined, undefined, undefined); + + // Assert + assert.strictEqual(tag.frames.length, 0); + } + + @test + public setTextFrame_urlFrameNoMatch() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + + // Act + tag.setTextFrame(FrameIdentifiers.WCOM, "foo", "bar"); + + // Assert + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.WCOM); + assert.deepStrictEqual(( tag.frames[0]).text, ["foo", "bar"]); + } + + @test + public setTextFrame_urlFrameWithMatch() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const frame = UrlLinkFrame.fromIdentity(FrameIdentifiers.WCOM); + frame.text = ["fux", "qux"]; + + // Act + tag.setTextFrame(FrameIdentifiers.WCOM, "foo", "bar"); + + // Assert + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.WCOM); + assert.deepStrictEqual(( tag.frames[0]).text, ["foo", "bar"]); + } + + @test + public setTextFrame_textFrameNoMatch() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + + // Act + tag.setTextFrame(FrameIdentifiers.TCOM, "foo", "bar"); + + // Assert + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TCOM); + assert.deepStrictEqual(( tag.frames[0]).text, ["foo", "bar"]); + } + + @test + public setTextFrame_textFrameWithMatch() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + const frame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCOM); + frame.text = ["fux", "qux"]; + + // Act + tag.setTextFrame(FrameIdentifiers.TCOM, "foo", "bar"); + + // Assert + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TCOM); + assert.deepStrictEqual(( tag.frames[0]).text, ["foo", "bar"]); + } } From 9eb29bb3cc4185076e881c3b5c2f2959d3042430 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Thu, 12 Mar 2020 00:04:20 -0400 Subject: [PATCH 58/71] Last round of Id3v2 unit tests --- src/id3v2/frames/attachmentFrame.ts | 15 +- src/id3v2/frames/uniqueFileIdentifierFrame.ts | 2 +- test/id3v2/attachmentsFrameTests.ts | 208 +++++++++++++++++- 3 files changed, 207 insertions(+), 18 deletions(-) diff --git a/src/id3v2/frames/attachmentFrame.ts b/src/id3v2/frames/attachmentFrame.ts index 7164a8a8..6b68c43d 100644 --- a/src/id3v2/frames/attachmentFrame.ts +++ b/src/id3v2/frames/attachmentFrame.ts @@ -64,7 +64,7 @@ export default class AttachmentFrame extends Frame implements IPicture { Guards.truthy(picture, "picture"); // In this case we will assume the frame is an APIC until the picture is parsed - const frame = new AttachmentFrame(new Id3v2FrameHeader(FrameIdentifiers.APIC, 4)); + const frame = new AttachmentFrame(new Id3v2FrameHeader(FrameIdentifiers.APIC)); frame._rawPicture = picture; return frame; } @@ -206,7 +206,7 @@ export default class AttachmentFrame extends Frame implements IPicture { /** @inheritDoc */ public clone(): Frame { - const frame = new AttachmentFrame(new Id3v2FrameHeader(this.frameId, 4)); + const frame = new AttachmentFrame(new Id3v2FrameHeader(this.frameId)); if (this._rawPicture) { frame._rawPicture = this._rawPicture; } else if (this._rawData) { @@ -274,12 +274,6 @@ export default class AttachmentFrame extends Frame implements IPicture { protected renderFields(version: number) { this.parseFromRaw(); - // Bypass processing if we haven't changed the data. Raw data is cleared when we touch any - // fields inside this frame. - if (this._rawData && this._rawVersion === version) { - return this._rawData; - } - const encoding = AttachmentFrame.correctEncoding(this.textEncoding, version); const data = ByteVector.empty(); @@ -288,8 +282,9 @@ export default class AttachmentFrame extends Frame implements IPicture { data.addByte(encoding); if (version === 2) { - const ext = Picture.getExtensionFromMimeType(this.mimeType); - data.addByteVector(ByteVector.fromString(ext && ext.length === 3 ? ext.toUpperCase() : "XXX")); + let ext = Picture.getExtensionFromMimeType(this.mimeType); + ext = ext && ext.length >= 3 ? ext.substring(ext.length - 3).toUpperCase() : "XXX"; + data.addByteVector(ByteVector.fromString(ext)); } else { data.addByteVector(ByteVector.fromString(this.mimeType, StringType.Latin1)); data.addByteVector(ByteVector.getTextDelimiter(StringType.Latin1)); diff --git a/src/id3v2/frames/uniqueFileIdentifierFrame.ts b/src/id3v2/frames/uniqueFileIdentifierFrame.ts index 8b32dd37..ba325390 100644 --- a/src/id3v2/frames/uniqueFileIdentifierFrame.ts +++ b/src/id3v2/frames/uniqueFileIdentifierFrame.ts @@ -119,7 +119,7 @@ export default class UniqueFileIdentifierFrame extends Frame { /** @inheritDoc */ public clone(): Frame { - const frame = new UniqueFileIdentifierFrame(new Id3v2FrameHeader(FrameIdentifiers.UFID, 4)); + const frame = new UniqueFileIdentifierFrame(new Id3v2FrameHeader(FrameIdentifiers.UFID)); frame._owner = this._owner; if (this._identifier) { frame.identifier = ByteVector.fromByteVector(this.identifier); diff --git a/test/id3v2/attachmentsFrameTests.ts b/test/id3v2/attachmentsFrameTests.ts index d4132760..ef7b93e3 100644 --- a/test/id3v2/attachmentsFrameTests.ts +++ b/test/id3v2/attachmentsFrameTests.ts @@ -5,26 +5,35 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import AttachmentFrame from "../../src/id3v2/frames/attachmentFrame"; import FrameConstructorTests from "./frameConstructorTests"; +import Id3v2Settings from "../../src/id3v2/id3v2Settings"; +import PropertyTests from "../utilities/propertyTests"; import {ByteVector, StringType} from "../../src/byteVector"; import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; import {Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; import {FrameIdentifier, FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; import {IPicture, PictureType} from "../../src/picture"; -import Id3v2Settings from "../../src/id3v2/id3v2Settings"; -import PropertyTests from "../utilities/propertyTests"; // Setup chai Chai.use(ChaiAsPromised); const assert = Chai.assert; function getTestFrame() { - const data = ByteVector.fromString("foobarbaz"); + return getCustomTestFrame( + ByteVector.fromString("foobarbaz"), + "fux", + "bux", + "application/octet-stream", + PictureType.FrontCover + ); +} + +function getCustomTestFrame(data: ByteVector, desc: string, filename: string, mimeType: string, type: PictureType) { const mockPicture = TypeMoq.Mock.ofType(); mockPicture.setup((p) => p.data).returns(() => data); - mockPicture.setup((p) => p.description).returns(() => "fux"); - mockPicture.setup((p) => p.filename).returns(() => "bux"); - mockPicture.setup((p) => p.mimeType).returns(() => "application/octet-stream"); - mockPicture.setup((p) => p.type).returns(() => PictureType.FrontCover); + mockPicture.setup((p) => p.description).returns(() => desc); + mockPicture.setup((p) => p.filename).returns(() => filename); + mockPicture.setup((p) => p.mimeType).returns(() => mimeType); + mockPicture.setup((p) => p.type).returns(() => type); return AttachmentFrame.fromPicture(mockPicture.object); } @@ -115,6 +124,20 @@ class Id3v2_AttachmentFrame_ConstructorTests extends FrameConstructorTests { assert.throws(() => { const _ = frame.type; }); } + @test + public fromRawData_dataTooShort() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.APIC); + header.frameSize = 3; + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromSize(3, 0x00) + ); + + // Act / Assert + assert.throws(() => { const _ = AttachmentFrame.fromRawData(data, 4); }); + } + @test public fromRawData_apicV4() { // Arrange @@ -576,4 +599,175 @@ class Id3v2_AttachmentFrame_MethodTests { // Assert assert.strictEqual(output, frame1); } + + @test + public render_apicV2InvalidMimeType_correctsEncoding() { + // Arrange + const data = ByteVector.fromString("fuxbuxqux"); + const frame = getCustomTestFrame(data, "foo", "bar", "this/isnot/amimetype", PictureType.FrontCover); + + // Act + const output = frame.render(2); + + // Assert + const header = new Id3v2FrameHeader(FrameIdentifiers.APIC); + header.frameSize = 24; + const expected = ByteVector.concatenate( + header.render(2), + StringType.UTF16, + ByteVector.fromString("XXX"), + PictureType.FrontCover, + ByteVector.fromString("foo", StringType.UTF16), + ByteVector.getTextDelimiter(StringType.UTF16), + data + ); + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_apicV2ValidMimeType_correctsEncoding() { + // Arrange + const data = ByteVector.fromString("fuxbuxqux"); + const frame = getCustomTestFrame(data, "foo", "bar", "image/gif", PictureType.FrontCover); + + // Act + const output = frame.render(2); + + // Assert + const header = new Id3v2FrameHeader(FrameIdentifiers.APIC); + header.frameSize = 24; + const expected = ByteVector.concatenate( + header.render(2), + StringType.UTF16, + ByteVector.fromString("GIF"), + PictureType.FrontCover, + ByteVector.fromString("foo", StringType.UTF16), + ByteVector.getTextDelimiter(StringType.UTF16), + data + ); + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_apicV4() { + // Arrange + const data = ByteVector.fromString("fuxbuxqux"); + const frame = getCustomTestFrame(data, "foo", "bar", "image/gif", PictureType.FrontCover); + + // Act + const output = frame.render(4); + + // Assert + const header = new Id3v2FrameHeader(FrameIdentifiers.APIC); + header.frameSize = 25; + const expected = ByteVector.concatenate( + header.render(4), + Id3v2Settings.defaultEncoding, + ByteVector.fromString("image/gif"), + ByteVector.getTextDelimiter(StringType.Latin1), + PictureType.FrontCover, + ByteVector.fromString("foo", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + data + ); + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_geobNoMimeType() { + // Arrange + const data = ByteVector.fromString("fuxbuxqux"); + const frame = getCustomTestFrame(data, undefined, undefined, undefined, PictureType.NotAPicture); + + // Act + const output = frame.render(4); + + // Assert + const header = new Id3v2FrameHeader(FrameIdentifiers.GEOB); + header.frameSize = 13; + const expected = ByteVector.concatenate( + header.render(4), + Id3v2Settings.defaultEncoding, + ByteVector.getTextDelimiter(Id3v2Settings.defaultEncoding), + ByteVector.getTextDelimiter(Id3v2Settings.defaultEncoding), + ByteVector.getTextDelimiter(Id3v2Settings.defaultEncoding), + data + ); + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_geobWithMimeType() { + // Arrange + const data = ByteVector.fromString("fuxbuxqux"); + const frame = getCustomTestFrame(data, undefined, undefined, "image/gif", PictureType.NotAPicture); + + // Act + const output = frame.render(4); + + // Assert + const header = new Id3v2FrameHeader(FrameIdentifiers.GEOB); + header.frameSize = 22; + const expected = ByteVector.concatenate( + header.render(4), + Id3v2Settings.defaultEncoding, + ByteVector.fromString("image/gif", Id3v2Settings.defaultEncoding), + ByteVector.getTextDelimiter(Id3v2Settings.defaultEncoding), + ByteVector.getTextDelimiter(Id3v2Settings.defaultEncoding), + ByteVector.getTextDelimiter(Id3v2Settings.defaultEncoding), + data + ); + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_geobWithMimeTypeAndFileName() { + // Arrange + const data = ByteVector.fromString("fuxbuxqux"); + const frame = getCustomTestFrame(data, undefined, "file.gif", "image/gif", PictureType.NotAPicture); + + // Act + const output = frame.render(4); + + // Assert + const header = new Id3v2FrameHeader(FrameIdentifiers.GEOB); + header.frameSize = 30; + const expected = ByteVector.concatenate( + header.render(4), + Id3v2Settings.defaultEncoding, + ByteVector.fromString("image/gif", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + ByteVector.fromString("file.gif", Id3v2Settings.defaultEncoding), + ByteVector.getTextDelimiter(Id3v2Settings.defaultEncoding), + ByteVector.getTextDelimiter(Id3v2Settings.defaultEncoding), + data + ); + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_geobWithMimeTypeAndFileNameAndDescription() { + // Arrange + const data = ByteVector.fromString("fuxbuxqux"); + const frame = getCustomTestFrame(data, "foobarbaz", "file.gif", "image/gif", PictureType.NotAPicture); + + // Act + const output = frame.render(4); + + // Assert + const header = new Id3v2FrameHeader(FrameIdentifiers.GEOB); + header.frameSize = 39; + const expected = ByteVector.concatenate( + header.render(4), + Id3v2Settings.defaultEncoding, + ByteVector.fromString("image/gif", StringType.Latin1), + ByteVector.getTextDelimiter(StringType.Latin1), + ByteVector.fromString("file.gif", Id3v2Settings.defaultEncoding), + ByteVector.getTextDelimiter(Id3v2Settings.defaultEncoding), + ByteVector.fromString("foobarbaz", Id3v2Settings.defaultEncoding), + ByteVector.getTextDelimiter(Id3v2Settings.defaultEncoding), + data + ); + assert.isTrue(ByteVector.equal(output, expected)); + } } From 2c383b980e43e257e112d07fbfdacedb6fe46937 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 17 Mar 2020 21:27:11 -0400 Subject: [PATCH 59/71] Last round of unit tests for Id3v2 --- src/id3v2/frames/frame.ts | 56 ++++----- src/id3v2/frames/frameHeader.ts | 2 +- src/utils.ts | 7 ++ test/id3v2/frameTests.ts | 209 ++++++++++++++++++++++++++++++++ 4 files changed, 243 insertions(+), 31 deletions(-) create mode 100644 test/id3v2/frameTests.ts diff --git a/src/id3v2/frames/frame.ts b/src/id3v2/frames/frame.ts index e2a0d269..4672800b 100644 --- a/src/id3v2/frames/frame.ts +++ b/src/id3v2/frames/frame.ts @@ -40,7 +40,6 @@ export abstract class Frame { // #region Constructors protected constructor(header: Id3v2FrameHeader) { - Guards.truthy(header, "header"); this._header = header; } @@ -50,25 +49,23 @@ export abstract class Frame { /** * Gets the encryption ID applied to the current instance. - * @returns number Value containing the encryption identifer for the current instance or -1 if - * not set. + * @returns number Value containing the encryption identifer for the current instance or + * `undefined` if not set. */ - public get encryptionId(): number { + public get encryptionId(): number | undefined { return (this.flags & Id3v2FrameFlags.Encryption) !== 0 ? this._encryptionId - : -1; + : undefined; } /** * Sets the encryption ID applied to the current instance. - * @param value Value containing the encryption identifier for the current instance. Must be a - * safe 16-bit integer. - * Encryption values can be betweenInclusive 0 and 255. Setting any other value will unset the - * encryption ID and set the value to -1; + * @param value Value containing the encryption identifier for the current instance. Must be an + * 8-bit unsigned integer. Setting to `undefined` will remove the encryption header and ID */ - public set encryptionId(value: number) { - Guards.short(value, "value"); - if (value >= 0x00 && value <= 0xFF) { - this._encryptionId = value; + public set encryptionId(value: number | undefined) { + Guards.optionalByte(value, "value"); + this._encryptionId = value; + if (value !== undefined) { this.flags |= Id3v2FrameFlags.Encryption; } else { this.flags &= ~Id3v2FrameFlags.Encryption; @@ -96,24 +93,23 @@ export abstract class Frame { /** * Gets the grouping ID applied to the current instance. - * @returns number Value containing the grouping identifier for the current instance, of -1 if - * not set. + * @returns number Value containing the grouping identifier for the current instance, or + * `undefined` if not set. */ - public get groupId(): number { + public get groupId(): number | undefined { return (this.flags & Id3v2FrameFlags.GroupingIdentity) !== 0 ? this._groupId - : -1; + : undefined; } /** * Sets the grouping ID applied to the current instance. - * @param value Grouping identifier for the current instance. Must be a 16-bit integer. - * Grouping identifiers can be betweenInclusive 0 and 255. Setting any other value will unset the - * grouping identify and set the value to -1. + * @param value Grouping identifier for the current instance. Must be a 8-bit unsigned integer. + * Setting to `undefined` will remove the grouping identity header and ID */ - public set groupId(value: number) { - Guards.short(value, "value"); - if (value >= 0x00 && value <= 0xFF) { - this._groupId = value; + public set groupId(value: number | undefined) { + Guards.optionalByte(value, "value"); + this._groupId = value; + if (value !== undefined) { this.flags |= Id3v2FrameFlags.GroupingIdentity; } else { this.flags &= ~Id3v2FrameFlags.GroupingIdentity; @@ -194,10 +190,10 @@ export abstract class Frame { } this._header.frameSize = fieldData.length; - const headerData = this._header.render(version); - headerData.addByteVector(fieldData); - - return headerData; + return ByteVector.concatenate( + this._header.render(version), + fieldData + ); } // #region Protected Methods @@ -240,7 +236,7 @@ export abstract class Frame { } if ((this.flags & Id3v2FrameFlags.GroupingIdentity) !== 0) { - if (frameData.length >= dataOffset) { + if (frameData.length <= dataOffset) { throw new CorruptFileError("Frame data incomplete"); } this.groupId = frameData.get(dataOffset++); @@ -248,7 +244,7 @@ export abstract class Frame { } if ((this.flags & Id3v2FrameFlags.Encryption) !== 0) { - if (frameData.length >= dataOffset) { + if (frameData.length <= dataOffset) { throw new CorruptFileError("Frame data incomplete"); } this._encryptionId = frameData.get(dataOffset++); diff --git a/src/id3v2/frames/frameHeader.ts b/src/id3v2/frames/frameHeader.ts index 45d52698..105bbbd2 100644 --- a/src/id3v2/frames/frameHeader.ts +++ b/src/id3v2/frames/frameHeader.ts @@ -69,8 +69,8 @@ export class Id3v2FrameHeader { Guards.uint(frameSize, "frameSize"); this._frameId = id; - this._flags = flags; this._frameSize = frameSize; + this.flags = flags; // Force the compression/encryption checks } /** diff --git a/src/utils.ts b/src/utils.ts index 4f0720b5..50ed64d3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -31,6 +31,13 @@ export class Guards { } } + public static optionalByte(value: number | undefined, name: string): void { + if (value === undefined) { + return; + } + Guards.byte(value, name); + } + public static short(value: number, name: string): void { if (!Number.isSafeInteger(value) || value > 32767 || value < -32768) { throw new Error(`Argument out of range: ${name} must be a 16-bit integer`); diff --git a/test/id3v2/frameTests.ts b/test/id3v2/frameTests.ts new file mode 100644 index 00000000..142b3463 --- /dev/null +++ b/test/id3v2/frameTests.ts @@ -0,0 +1,209 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import PropertyTests from "../utilities/propertyTests"; +import SyncData from "../../src/id3v2/syncData"; +import {ByteVector} from "../../src/byteVector"; +import {Frame, FrameClassType} from "../../src/id3v2/frames/frame"; +import {Id3v2FrameFlags, Id3v2FrameHeader} from "../../src/id3v2/frames/frameHeader"; +import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +class TestFrame extends Frame { + public static renderFieldData = ByteVector.concatenate( + ByteVector.fromSize(10, 0x00), + 0xFF, 0xE0, + ByteVector.fromSize(10, 0x00) + ); + + public constructor(header: Id3v2FrameHeader) { + super(header); + } + + get frameClassType(): FrameClassType { + return undefined; + } + + public callFieldData(data: ByteVector, offset: number, version: number): ByteVector { + return this.fieldData(data, offset, version); + } + + public clone(): Frame { + return undefined; + } + + protected parseFields(data: ByteVector, version: number): void { + } + + protected renderFields(version: number): ByteVector { + return ByteVector.fromByteVector(TestFrame.renderFieldData); + } +} + +@suite(timeout(3000), slow(1000)) +class FrameTests { + // NOTE: We're mostly ignoring test cases that were already covered by concrete frame classes + + @test + public encryptionId_invalidValues() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.TXXX); + const frame = new TestFrame(header); + const set = (v: number) => { frame.encryptionId = v; }; + + // Act / Assert + PropertyTests.propertyThrows(set, 0x100); + PropertyTests.propertyThrows(set, -1); + PropertyTests.propertyThrows(set, 1.23); + } + + @test + public encryptionId() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.TXXX, Id3v2FrameFlags.FileAlterPreservation); + const frame = new TestFrame(header); + const get = () => frame.encryptionId; + const set = (v: number) => { frame.encryptionId = v; }; + + // Act / Assert + assert.isUndefined(frame.encryptionId); + + PropertyTests.propertyThrows(set, 0x88); + assert.strictEqual(frame.flags, Id3v2FrameFlags.FileAlterPreservation); + + PropertyTests.propertyRoundTrip(set, get, undefined); + assert.strictEqual(frame.flags, Id3v2FrameFlags.FileAlterPreservation); + } + + @test + public groupId_invalidValues() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.TXXX); + const frame = new TestFrame(header); + const set = (v: number) => { frame.groupId = v; }; + + // Act / Assert + PropertyTests.propertyThrows(set, 0x100); + PropertyTests.propertyThrows(set, -1); + PropertyTests.propertyThrows(set, 1.23); + } + + @test + public groupid() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.TXXX, Id3v2FrameFlags.FileAlterPreservation); + const frame = new TestFrame(header); + const get = () => frame.groupId; + const set = (v: number) => { frame.groupId = v; }; + + // Act / Assert + assert.isUndefined(frame.groupId); + + PropertyTests.propertyRoundTrip(set, get, 0x88); + assert.strictEqual(frame.flags, Id3v2FrameFlags.FileAlterPreservation | Id3v2FrameFlags.GroupingIdentity); + + PropertyTests.propertyRoundTrip(set, get, undefined); + assert.strictEqual(frame.flags, Id3v2FrameFlags.FileAlterPreservation); + } + + @test + public render_dataLengthIndicator() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.TXXX, Id3v2FrameFlags.DataLengthIndicator); + const frame = new TestFrame(header); + + // Act + const output = frame.render(4); + + // Assert + const expected = ByteVector.concatenate( + header.render(4), + ByteVector.fromUInt(TestFrame.renderFieldData.length), + TestFrame.renderFieldData + ); + assert.isTrue(ByteVector.equal(output, expected)); + } + + @test + public render_groupingIdentity() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.TXXX, Id3v2FrameFlags.GroupingIdentity); + const frame = new TestFrame(header); + frame.groupId = 0x88; + + // Act + const output = frame.render(4); + + // Assert + const expected = ByteVector.concatenate( + header.render(4), + 0x88, + TestFrame.renderFieldData + ); + } + + @test + public fieldData_dataLengthIndicator() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.TXXX, Id3v2FrameFlags.DataLengthIndicator); + header.frameSize = TestFrame.renderFieldData.length + 4; + const frame = new TestFrame(header); + const data = ByteVector.concatenate( + header.render(4), + ByteVector.fromUInt(TestFrame.renderFieldData.length), + TestFrame.renderFieldData + ); + + // Act + const output = frame.callFieldData(data, 0, 4); + + // Assert + assert.isTrue(ByteVector.equal(output, TestFrame.renderFieldData)); + } + + @test + public fieldData_groupingIdentify() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.TXXX, Id3v2FrameFlags.GroupingIdentity); + header.frameSize = TestFrame.renderFieldData.length + 1; + const frame = new TestFrame(header); + const data = ByteVector.concatenate( + header.render(4), + 0x88, + TestFrame.renderFieldData + ); + + // Act + const output = frame.callFieldData(data, 0, 4); + + // Assert + assert.isTrue(ByteVector.equal(output, TestFrame.renderFieldData)); + assert.strictEqual(frame.groupId, 0x88); + } + + @test + public fieldData_desynchronized() { + // Arrange + const fieldData = ByteVector.fromByteVector(TestFrame.renderFieldData); + SyncData.unsyncByteVector(fieldData); + + const header = new Id3v2FrameHeader(FrameIdentifiers.TXXX, Id3v2FrameFlags.Desynchronized); + header.frameSize = fieldData.length; + const frame = new TestFrame(header); + const data = ByteVector.concatenate( + header.render(4), + fieldData + ); + + // Act + const output = frame.callFieldData(data, 0, 4); + + // Assert + SyncData.resyncByteVector(fieldData); + assert.isTrue(ByteVector.equal(output, fieldData)); + } +} From 9fe38b6c64b0fb6c0e55fda78726c2971663d442 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sat, 4 Apr 2020 15:24:09 -0400 Subject: [PATCH 60/71] Finishing up tests for byte vector class --- src/byteVector.ts | 295 +-- src/utils.ts | 12 + test/byteVectorConstructorTests.ts | 953 +++++----- test/byteVectorConversionTests.ts | 596 +++--- ...ests.ts => byteVectorStaticMethodTests.ts} | 82 +- test/byteVectorVoidMethodTests.ts | 1681 +++++++++++++++++ test/byteVectorVoidOperationTests.ts | 870 --------- 7 files changed, 2683 insertions(+), 1806 deletions(-) rename test/{byteVectorStaticOperationTests.ts => byteVectorStaticMethodTests.ts} (90%) create mode 100644 test/byteVectorVoidMethodTests.ts delete mode 100644 test/byteVectorVoidOperationTests.ts diff --git a/src/byteVector.ts b/src/byteVector.ts index d9b1288a..2f4ce37f 100644 --- a/src/byteVector.ts +++ b/src/byteVector.ts @@ -227,6 +227,9 @@ export class ByteVector { return result; } + /** + * Creates an empty {@see ByteVector} + */ public static empty(): ByteVector { return this.fromSize(0); } @@ -638,13 +641,8 @@ export class ByteVector { * @param byte Value to add to the end of the ByteVector. Must be positive integer <=0xFF. */ public addByte(byte: number): void { - if (this._isReadOnly) { - throw new Error("Not supported: Cannot edit readonly byte vectors"); - } - - if (!Number.isInteger(byte) || byte < 0 || byte > 0xFF) { - throw new Error("Argument out of range: byte is not a valid byte"); - } + this.throwIfReadOnly(); + Guards.byte(byte, "byte"); this.addByteArray(new Uint8Array([byte])); } @@ -654,12 +652,9 @@ export class ByteVector { * @param data Array of bytes to add to the end of the ByteVector */ public addByteArray(data: Uint8Array): void { - if (this._isReadOnly) { - throw new Error("Not supported: Cannot edit readonly byte vectors"); - } - if (data === undefined || data === null) { - throw new Error("Argument null: data not provided"); - } + this.throwIfReadOnly(); + Guards.truthy(data, "data"); + if (data.length === 0) { return; } @@ -675,9 +670,8 @@ export class ByteVector { * @param data ByteVector to add to the end of this ByteVector */ public addByteVector(data: ByteVector): void { - if (data === undefined || data === null) { - throw new Error("Argument null: data not provided"); - } + this.throwIfReadOnly(); + Guards.truthy(data, "data"); this.addByteArray(data._data); } @@ -688,39 +682,30 @@ export class ByteVector { * existing references to {@see ByteVector.data} will remain unchanged. */ public clear(): void { - if (this._isReadOnly) { - throw new Error("Invalid Operation Exception: Cannot edit readonly objects"); - } + this.throwIfReadOnly(); this._data = new Uint8Array(0); } - public contains(byte: number): boolean { - Guards.byte(byte, "byte"); - return this._data.indexOf(byte) >= 0; - } - + /** + * Determines if {@paramref pattern} exists at a certain {@paramref offset} in this byte vector. + * @param pattern ByteVector to search for at in this byte vector + * @param offset Position in this byte vector to search for the pattern. If omitted, defaults + * to `0` + * @param patternOffset Position in {@paramref pattern} to begin matching. If omitted, defaults + * to `0` + * @param patternLength Bytes of {@paramref pattern} to match. If omitted, defaults to all bytes + * in the pattern minus the offset + */ public containsAt( pattern: ByteVector, offset: number = 0, patternOffset: number = 0, - patternLength: number = Number.MAX_SAFE_INTEGER + patternLength: number = pattern.length - patternOffset ): boolean { - if (!pattern) { - throw new Error("Argument null exception: pattern is null"); - } - if (!Number.isInteger(offset)) { - throw new Error("Argument out of range exception: offset is invalid"); - } - if (!Number.isInteger(patternOffset)) { - throw new Error("Argument out of range exception: patternOffset is invalid"); - } - if (!Number.isInteger(patternLength)) { - throw new Error("Argument out of range exception: patternLength is invalid"); - } - - if (pattern.length < patternLength) { - patternLength = pattern.length; - } + Guards.truthy(pattern, "pattern"); + Guards.int(offset, "offset"); + Guards.int(patternOffset, "patternOffset"); + Guards.int(patternLength, "patternLength"); // Do some sanity checking -- all of these things are needed for the search to be valid if ( @@ -743,10 +728,12 @@ export class ByteVector { return true; } + /** + * Compares this byte vector to a different byte vector. Returns a numeric value + * @param other ByteVector to compare to this byte vector + */ public compareTo(other: ByteVector): number { - if (!other) { - throw new Error("Argument null exception: other is null"); - } + Guards.truthy(other, "other"); let diff = this.length - other.length; @@ -757,18 +744,23 @@ export class ByteVector { return diff; } + /** + * Determines whether or not this byte vector ends with the provided {@paramref pattern}. + * @param pattern ByteVector to look for at the end of this byte vector + */ public endsWith(pattern: ByteVector): boolean { - if (!pattern) { - throw new Error("Argument null exception: pattern is null"); - } - + Guards.truthy(pattern, "pattern"); return this.containsAt(pattern, this.length - pattern.length); } + /** + * Determines whether or not this byte vector ends with a part of the {@paramref pattern}. + * NOTE: if this byte vector ends with {@paramref pattern} perfectly, it must end with n-1 or + * less bytes + * @param pattern ByteVector to look for at the end of this byte vector + */ public endsWithPartialMatch(pattern: ByteVector): number { - if (!pattern) { - throw new Error("Argument null exception: pattern is null"); - } + Guards.truthy(pattern, "pattern"); if (pattern.length > this.length) { return -1; @@ -779,7 +771,7 @@ export class ByteVector { // Try to match the last n-1bytes from the vector (where n is the pattern size) // continue trying to match n-2, n-3...1 bytes for (let i = 1; i < pattern.length; i++) { - if (this.containsAt(pattern, startIndex)) { + if (this.containsAt(pattern, startIndex + i, 0, pattern.length - i)) { return startIndex + i; } } @@ -787,16 +779,22 @@ export class ByteVector { return -1; } + /** + * Searches this instance for the {@paramref pattern}. Returns the index of the first instance + * of the pattern, or `-1` if it was not found. Providing a {@paramref byteAlign} requires the + * pattern to appear at an index that is a multiple of the byteAlign parameter. + * Example: searching "abcd" for "ab" with byteAlign 1 will return 0. Searching "abcd" for + * "ab" with byteAlign 2 will return 1. Searching "00ab" for "ab" with byteAlign 2 will return + * 2. Searching "0abc" with byteAlign 2 will return -1. + * @param pattern Pattern of bytes to search this instance for + * @param offset Optional, offset into this instance to start searching + * @param byteAlign Optional, byte alignment the pattern much align to + */ public find(pattern: ByteVector, offset: number = 0, byteAlign: number = 1): number { - if (!pattern) { - throw new Error("Argument null exception: pattern is null"); - } - if (!Number.isInteger(offset) || offset < 0) { - throw new Error("Argument out of range exception: offset is invalid"); - } - if (!Number.isInteger(byteAlign) || byteAlign < 1) { - throw new Error("Argument out of range exception: byteAlign is invalid"); - } + Guards.truthy(pattern, "pattern"); + Guards.uint(offset, "offset"); + Guards.uint(byteAlign, "byteAlign"); + Guards.greaterThanInclusive(byteAlign, 1, "byteAlign"); if (pattern.length > this.length - offset) { return -1; @@ -838,10 +836,13 @@ export class ByteVector { return -1; } + /** + * Gets the byte at the given {@paramref index} + * @param index Index into the byte vector to return + */ public get(index: number): number { - if (!Number.isInteger(index) || index > this._data.length) { - throw new Error("Argument out of range exception: index is invalid"); - } + Guards.uint(index, "index"); + Guards.lessThanInclusive(index, this.length - 1, "index"); return this._data[index]; } @@ -862,15 +863,10 @@ export class ByteVector { * @param byte Value to insert into the ByteVector. Must be a positive integer <=0xFF */ public insertByte(index: number, byte: number): void { - if (this._isReadOnly) { - throw new Error("Not supported: Cannot edit readonly byte vectors"); - } - if (!Number.isInteger(index) || index < 0 || index > this.length) { - throw new Error("Argument out of range: index is invalid"); - } - if (!Number.isInteger(byte) || byte < 0 || byte > 0xFF) { - throw new Error("Argument out of range: byte is invalid"); - } + this.throwIfReadOnly(); + Guards.uint(index, "index"); + Guards.lessThanInclusive(index, this.length, "index"); + Guards.byte(byte, "byte"); const oldData = this._data; this._data = new Uint8Array(oldData.length + 1); @@ -891,15 +887,11 @@ export class ByteVector { * @param other Array of bytes to insert into the ByteVector. */ public insertByteArray(index: number, other: Uint8Array): void { - if (this._isReadOnly) { - throw new Error("Not supported: Cannot edit readonly byte vectors"); - } - if (!Number.isInteger(index) || index < 0 || index > this.length) { - throw new Error("Argument out of range: index is invalid"); - } - if (!other) { - throw new Error("Argument null: other was not provided"); - } + this.throwIfReadOnly(); + Guards.uint(index, "index"); + Guards.lessThanInclusive(index, this.length, "index"); + Guards.truthy(other, "other"); + if (other.length === 0 ) { return; } @@ -923,25 +915,28 @@ export class ByteVector { * @param other ByteVector to insert into this ByteVector. */ public insertByteVector(index: number, other: ByteVector): void { - if (!other) { - throw new Error("Argument null: other was not provided"); - } - + Guards.truthy(other, "other"); this.insertByteArray(index, other._data); } + /** + * Returns a subarray of the current instance. This operation returns a new instance and does + * not alter the current instance. + * @param startIndex Index into the array to begin + * @param length Number of elements from the array to include. If omitted, defaults to the + * remainder of the array + */ public mid(startIndex: number, length: number = this._data.length - startIndex): ByteVector { - if (!Number.isInteger(startIndex) || startIndex < 0 || startIndex > this.length) { - throw new Error("Argument out of range exception: startIndex is invalid"); - } - if (!Number.isInteger(length) || length < 0 || startIndex + length > this.length) { - throw new Error("Argument out of range exception: length is invalid"); - } + Guards.uint(startIndex, "startIndex"); + Guards.uint(length, "length"); if (length === 0) { return ByteVector.fromSize(0); } + Guards.lessThanInclusive(startIndex, this.length - 1, "startIndex"); + Guards.lessThanInclusive(length, this.length - startIndex, "length"); + return ByteVector.fromByteArray(this._data.subarray(startIndex, startIndex + length)); } @@ -950,12 +945,9 @@ export class ByteVector { * @param index Index that will be removed from the ByteVector */ public removeAtIndex(index: number) { - if (this._isReadOnly) { - throw new Error("Not supported: Cannot edit readonly byte vectors"); - } - if (!Number.isInteger(index) || index < 0 || index >= this.length) { - throw new Error("Argument out of range: index is invalid"); - } + this.throwIfReadOnly(); + Guards.uint(index, "index"); + Guards.lessThanInclusive(index, this.length - 1, "index"); const oldData = this._data; this._data = new Uint8Array(oldData.length - 1); @@ -973,15 +965,10 @@ export class ByteVector { * @param count Number of bytes to remove from this ByteVector */ public removeRange(index: number, count: number) { - if (this._isReadOnly) { - throw new Error("Not supported: Cannot edit readonly byte vectors"); - } - if (!Number.isInteger(index) || index < 0 || index >= this.length) { - throw new Error("Argument out of range: index is invalid"); - } - if (!Number.isInteger(count) || count < 0) { - throw new Error("Argument out of range: count is invalid"); - } + this.throwIfReadOnly(); + Guards.uint(index, "index"); + Guards.lessThanInclusive(index, this.length - 1, "index"); + Guards.uint(count, "count"); if (index + count > this.length) { count = this.length - index; @@ -997,39 +984,49 @@ export class ByteVector { } } + /** + * Resizes this instance to the length specified in {@paramref size}. If the desired size is + * longer than the current length, it will be filled with the byte value in + * {@paramref padding}. If the desired size is shorter than the current length, bytes will be + * removed. + * @param size Length of the byte vector after resizing. Must be unsigned 32-bit integer + * @param padding Byte to fill any excess space created after resizing + */ public resize(size: number, padding: number = 0x0): ByteVector { - if (this._isReadOnly) { - throw new Error("Not supported: Cannot edit readonly byte vectors"); - } - if (!Number.isInteger(size) || size < 0) { - throw new Error("Argument out of range: size is invalid"); - } - if (!Number.isInteger(padding) || padding > 255 || padding < 0) { - throw new Error("Argument out of range: padding is invalid"); - } + this.throwIfReadOnly(); + Guards.uint(size, "size"); + Guards.byte(padding, "padding"); if (this.length > size) { this.removeRange(size, this.length - size); + } else if (this.length < size) { + const oldData = this._data; + this._data = new Uint8Array(size); + this._data.set(oldData); + this._data.fill(padding, oldData.length); } - - const oldData = this._data; - this._data = new Uint8Array(size); - this._data.set(oldData); - this._data.fill(padding, oldData.length); + // Do nothing on same size return this; } + /** + * Finds a byte vector by searching from the end of this instance and working towards the + * beginning of this instance. Returns the index of the first instance of the pattern, or `-1` + * if it was not found. Providing a {@paramref byteAlign} requires the pattern to appear at an + * index that is a multiple of the byteAlign parameter. + * Example: searching "abcd" for "ab" with byteAlign 1 will return 0. Searching "abcd" for + * "ab" with byteAlign 2 will return 1. Searching "00ab" for "ab" with byteAlign 2 will return + * 2. Searching "0abc" with byteAlign 2 will return -1. + * @param pattern Pattern of bytes to search this instance for + * @param offset Optional, offset into this instance to start searching + * @param byteAlign Optional, byte alignment the pattern much align to + */ public rFind(pattern: ByteVector, offset: number = 0, byteAlign: number = 1): number { - if (!pattern) { - throw new Error("Argument null exception: pattern is null"); - } - if (!Number.isInteger(offset) || offset < 0) { - throw new Error("Argument out of range exception: offset is invalid"); - } - if (!Number.isInteger(byteAlign) || byteAlign < 1) { - throw new Error("Argument out of range exception: byteAlign is invalid"); - } + Guards.truthy(pattern, "pattern"); + Guards.uint(offset, "offset"); + Guards.uint(byteAlign, "byteAlign"); + Guards.greaterThanInclusive(byteAlign, 1, "byteAlign"); if (pattern.length === 0 || pattern.length > this.length - offset) { return -1; @@ -1050,7 +1047,10 @@ export class ByteVector { firstOccurrence.fill(pattern.length); for (let i = pattern.length - 1; i > 0; --i) { - i -= firstOccurrence[pattern._data[i]]; + firstOccurrence[pattern.get(i)] = i; + } + + for (let i = this.length - offset - pattern.length; i >= 0; i -= firstOccurrence[this.get(i)]) { if ((offset - i) % byteAlign === 0 && this.containsAt(pattern, i)) { return i; } @@ -1065,15 +1065,10 @@ export class ByteVector { * @param value Value to set at the index. Must be a valid integer betweenInclusive 0x0 and 0xff */ public set(index: number, value: number): void { - if (this._isReadOnly) { - throw new Error("Invalid operation exception: Cannot edit readonly objects"); - } - if (!Number.isInteger(index) || index > this._data.length) { - throw new Error("Argument out of range exception: index is invalid"); - } - if (!Number.isInteger(value) || value < 0 || value > 0xff) { - throw new Error("Argument out of range exception: value is not a valid byte"); - } + this.throwIfReadOnly(); + Guards.uint(index, "index"); + Guards.lessThanInclusive(index, this.length, "index"); + Guards.byte(value, "value"); this._data[index] = value; } @@ -1089,16 +1084,18 @@ export class ByteVector { */ public split(separator: ByteVector, byteAlign: number = 1, max: number = 0): ByteVector[] { Guards.truthy(separator, "separator"); - if (!Number.isSafeInteger(byteAlign) || byteAlign < 1) { - throw new Error("Argument out of range: byteAlign must be at least 1"); - } + Guards.uint(byteAlign, "byteAlign"); + Guards.greaterThanInclusive(byteAlign, 1, "byteAlign"); + Guards.uint(max, "max"); const list: ByteVector[] = []; let previousOffset = 0; - for (let offset = this.find(separator, 0, byteAlign); - offset !== -1 && (max < 1 || max > list.length + 1); - offset = this.find(separator, offset + separator.length, byteAlign)) { + for ( + let offset = this.find(separator, 0, byteAlign); + offset !== -1 && (max < 1 || max > list.length + 1); + offset = this.find(separator, offset + separator.length, byteAlign) + ) { list.push(this.mid(previousOffset, offset - previousOffset)); previousOffset = offset + separator.length; } @@ -1529,5 +1526,11 @@ export class ByteVector { return output; } + private throwIfReadOnly() { + if (this._isReadOnly) { + throw new Error("Not supported: Cannot edit readonly byte vectors"); + } + } + // #endregion } diff --git a/src/utils.ts b/src/utils.ts index 50ed64d3..775fb549 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -19,12 +19,24 @@ export class Guards { } } + public static greaterThanInclusive(value: number, lowerBound: number, name: string) { + if (value < lowerBound) { + throw new Error(`Argument out of range: ${name} must greater than ${lowerBound}`); + } + } + public static int(value: number, name: string): void { if (!Number.isSafeInteger(value)) { throw new Error(`Argument out of range: ${name} must be a 32-bit integer`); } } + public static lessThanInclusive(value: number, upperBound: number, name: string) { + if (value > upperBound) { + throw new Error(`Argument out of range: ${name} must be less than ${upperBound}`); + } + } + public static notNullOrUndefined(value: any, name: string): void { if (value === undefined || value === null) { throw new Error(`Argument null: ${name} was not provided`); diff --git a/test/byteVectorConstructorTests.ts b/test/byteVectorConstructorTests.ts index 6b41a2c1..3ab60bad 100644 --- a/test/byteVectorConstructorTests.ts +++ b/test/byteVectorConstructorTests.ts @@ -2,10 +2,13 @@ import * as BigInt from "big-integer"; import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; import * as StreamBuffers from "stream-buffers"; +import * as TypeMoq from "typemoq"; import {slow, suite, test, timeout} from "mocha-typescript"; import TestConstants from "./testConstants"; +import TestStream from "./utilities/testStream"; import {ByteVector, StringType} from "../src/byteVector"; +import {IFileAbstraction} from "../src/fileAbstraction"; const AB2B = require("arraybuffer-to-buffer"); @@ -14,11 +17,12 @@ Chai.use(ChaiAsPromised); const assert = Chai.assert; @suite(timeout(3000), slow(1000)) -class ByteVector_Concatenate { +class ByteVector_ConstructorTests { private testArray = new Uint8Array([0x80, 0x08, 0x50]); + private testByteVector = ByteVector.fromByteArray(this.testArray); @test - public noData() { + public concatenate_noData() { // Act const bv = ByteVector.concatenate(); @@ -28,7 +32,7 @@ class ByteVector_Concatenate { } @test - public oneByte() { + public concatenate_oneByte() { // Act const bv = ByteVector.concatenate(0x08); @@ -39,7 +43,7 @@ class ByteVector_Concatenate { } @test - public twoBytes() { + public concatenate_twoBytes() { // Act const bv = ByteVector.concatenate(0x80, 0x08); @@ -51,7 +55,7 @@ class ByteVector_Concatenate { } @test - public oneArray() { + public concatenate_oneArray() { // Act const bv = ByteVector.concatenate(this.testArray); @@ -59,15 +63,67 @@ class ByteVector_Concatenate { assert.ok(bv); assert.equal(bv.length, this.testArray.length); for (let i = 0; i < bv.length; i++) { - assert.equal(bv.get(i), this.testArray[i]); + assert.equal(bv.data[i], this.testArray[i]); } } -} -@suite(timeout(3000), slow(1000)) -class ByteVector_FromByteArray { @test - public NoData() { + public concatenate_twoArrays() { + // Act + const bv = ByteVector.concatenate( + this.testArray, + this.testArray + ); + + // Assert + assert.ok(bv); + assert.strictEqual(bv.length, this.testArray.length * 2); + for (let i = 0; i < bv.length; i++) { + assert.strictEqual(bv.data[i], this.testArray[i % this.testArray.length]); + } + } + + @test + public concatenate_oneVector() { + // Act + const bv = ByteVector.concatenate(this.testByteVector); + + // Assert + assert.ok(bv); + assert.strictEqual(bv.length, this.testByteVector.length); + for (let i = 0; i < bv.length; i++) { + assert.strictEqual(bv.data[i], this.testByteVector.data[i]); + } + } + + @test + public concatenate_twoVectors() { + // Act + const bv = ByteVector.concatenate( + this.testByteVector, + this.testByteVector + ); + + // Assert + assert.ok(bv); + assert.strictEqual(bv.length, this.testByteVector.length * 2); + for (let i = 0; i < bv.length; i++) { + assert.strictEqual(bv.data[i], this.testByteVector.data[i % this.testByteVector.length]); + } + } + + @test + public empty() { + // Act + const bv = ByteVector.empty(); + + // Assert + assert.isOk(bv); + assert.strictEqual(bv.length, 0); + } + + @test + public fromByteArray_noData() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromByteArray(undefined); @@ -78,7 +134,7 @@ class ByteVector_FromByteArray { } @test - public EmptyData() { + public fromByteArray_emptyData() { // Arrange, Act const data = new Uint8Array(); const bv = ByteVector.fromByteArray(data); @@ -92,7 +148,7 @@ class ByteVector_FromByteArray { } @test - public WithData() { + public fromByteArray_withData() { // Arrange, Act const data = new Uint8Array([0x0, 0x1, 0x2, 0x3, 0x4]); const bv = ByteVector.fromByteArray(data); @@ -106,7 +162,7 @@ class ByteVector_FromByteArray { } @test - public WithLength() { + public fromByteArray_withLength() { // Arrange, Act const data = new Uint8Array([0x0, 0x1, 0x2, 0x3, 0x4]); const bv = ByteVector.fromByteArray(data, 3); @@ -120,7 +176,7 @@ class ByteVector_FromByteArray { } @test - public ReadOnly() { + public fromByteArray_readOnly() { // Arrange, Act const data = new Uint8Array([0x0, 0x1, 0x2, 0x3, 0x4]); const bv = ByteVector.fromByteArray(data, undefined, true); @@ -132,12 +188,9 @@ class ByteVector_FromByteArray { assert.isTrue(bv.isReadOnly); assert.deepEqual(bv.data, data); } -} -@suite(timeout(3000), slow(1000)) -class ByteVector_FromByteVector { @test - public NoVector() { + public fromByteVector_noVector() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromByteVector(null); @@ -148,7 +201,7 @@ class ByteVector_FromByteVector { } @test - public WithData() { + public fromByteVector_withData() { // Arrange, Act const data = new Uint8Array([0x1, 0x2]); const original = ByteVector.fromByteArray(data); @@ -163,7 +216,7 @@ class ByteVector_FromByteVector { } @test - public ReadOnly() { + public fromByteVector_readOnly() { // Arrange, Act const data = new Uint8Array([0x1, 0x2]); const original = ByteVector.fromByteArray(data); @@ -176,12 +229,39 @@ class ByteVector_FromByteVector { assert.isTrue(bv.isReadOnly); assert.deepEqual(bv.data, data); } -} -@suite(timeout(3000), slow(1000)) -class ByteVector_FromInt { @test - public BadInteger() { + public fromFileAbstraction_badAbstraction() { + // Act / Assert + assert.throws(() => { ByteVector.fromFileAbstraction(undefined); }); + assert.throws(() => { ByteVector.fromFileAbstraction(null); }); + } + + @test + public fromFileAbstraction() { + // Arrange + const bytes = new Uint8Array(TestConstants.testFileContents); + const mockStream = new TestStream(ByteVector.fromByteArray(bytes), false); + + const mockFileAbstraction = TypeMoq.Mock.ofType(); + mockFileAbstraction.setup((a) => a.readStream).returns(() => mockStream); + mockFileAbstraction.setup((a) => a.closeStream(TypeMoq.It.isAny())); + + // Act + const bv = ByteVector.fromFileAbstraction(mockFileAbstraction.object); + + // Assert + assert.isOk(bv); + assert.strictEqual(bv.length, bytes.length); + assert.isFalse(bv.isEmpty); + assert.isFalse(bv.isReadOnly); + assert.deepEqual(bv.data, bytes); + + mockFileAbstraction.verify((a) => a.closeStream(TypeMoq.It.isValue(mockStream)), TypeMoq.Times.once()); + } + + @test + public fromInt_badInteger() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromInt(undefined); }); assert.throws(() => { ByteVector.fromInt(null); }); @@ -190,7 +270,7 @@ class ByteVector_FromInt { } @test - public Overflow() { + public fromInt_overflow() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromInt(0x10000000000); }); assert.throws(() => { ByteVector.fromInt(0xFFFFFFFF); }); @@ -198,8 +278,8 @@ class ByteVector_FromInt { } @test - public Zero_BigEndian() { - ByteVector_FromInt.TestInt( + public fromInt_zero_bigEndian() { + this.testInt( 0x0, [0x0, 0x0, 0x0, 0x0], undefined, @@ -208,8 +288,8 @@ class ByteVector_FromInt { } @test - public Zero_LittleEndian() { - ByteVector_FromInt.TestInt( + public fromInt_zero_littleEndian() { + this.testInt( 0x0, [0x0, 0x0, 0x0, 0x0], undefined, @@ -218,8 +298,8 @@ class ByteVector_FromInt { } @test - public Positive1Byte_BigEndian() { - ByteVector_FromInt.TestInt( + public fromInt_positive1Byte_bigEndian() { + this.testInt( 0x12, [0x00, 0x00, 0x00, 0x12], undefined, @@ -228,8 +308,8 @@ class ByteVector_FromInt { } @test - public Positive1Byte_LittleEndian() { - ByteVector_FromInt.TestInt( + public fromInt_positive1Byte_littleEndian() { + this.testInt( 0x12, [0x12, 0x00, 0x00, 0x00], undefined, @@ -238,8 +318,8 @@ class ByteVector_FromInt { } @test - public Positive2Byte_BigEndian() { - ByteVector_FromInt.TestInt( + public fromInt_positive2Byte_bigEndian() { + this.testInt( 0x1234, [0x00, 0x00, 0x12, 0x34], undefined, @@ -248,8 +328,8 @@ class ByteVector_FromInt { } @test - public Positive2Byte_LittleEndian() { - ByteVector_FromInt.TestInt( + public fromInt_positive2Byte_littleEndian() { + this.testInt( 0x1234, [0x34, 0x12, 0x00, 0x00], undefined, @@ -258,8 +338,8 @@ class ByteVector_FromInt { } @test - public Positive3Byte_BigEndian() { - ByteVector_FromInt.TestInt( + public fromInt_positive3Byte_bigEndian() { + this.testInt( 0x123456, [0x00, 0x12, 0x34, 0x56], undefined, @@ -268,8 +348,8 @@ class ByteVector_FromInt { } @test - public Positive3Byte_LittleEndian() { - ByteVector_FromInt.TestInt( + public fromInt_positive3Byte_littleEndian() { + this.testInt( 0x123456, [0x56, 0x34, 0x12, 0x00], undefined, @@ -278,8 +358,8 @@ class ByteVector_FromInt { } @test - public Positive4Byte_BigEndian() { - ByteVector_FromInt.TestInt( + public fromInt_positive4Byte_bigEndian() { + this.testInt( 0x12345678, [0x12, 0x34, 0x56, 0x78], undefined, @@ -288,8 +368,8 @@ class ByteVector_FromInt { } @test - public Positive4Byte_LittleEndian() { - ByteVector_FromInt.TestInt( + public fromInt_positive4Byte_littleEndian() { + this.testInt( 0x12345678, [0x78, 0x56, 0x34, 0x12], undefined, @@ -298,8 +378,8 @@ class ByteVector_FromInt { } @test - public Negative1Byte_BigEndian() { - ByteVector_FromInt.TestInt( + public fromInt_negative1Byte_bigEndian() { + this.testInt( -0x12, [0xFF, 0xFF, 0xFF, 0xEE], undefined, @@ -308,8 +388,8 @@ class ByteVector_FromInt { } @test - public Negative1Byte_LittleEndian() { - ByteVector_FromInt.TestInt( + public fromInt_negative1Byte_littleEndian() { + this.testInt( -0x12, [0xEE, 0xFF, 0xFF, 0xFF], undefined, @@ -318,8 +398,8 @@ class ByteVector_FromInt { } @test - public Negative2Byte_BigEndian() { - ByteVector_FromInt.TestInt( + public fromInt_negative2Byte_bigEndian() { + this.testInt( -0x1234, [0xFF, 0xFF, 0xED, 0xCC], undefined, @@ -328,8 +408,8 @@ class ByteVector_FromInt { } @test - public Negative2Byte_LittleEndian() { - ByteVector_FromInt.TestInt( + public fromInt_negative2Byte_littleEndian() { + this.testInt( -0x1234, [0xCC, 0xED, 0xFF, 0xFF], undefined, @@ -338,8 +418,8 @@ class ByteVector_FromInt { } @test - public Negative3Byte_BigEndian() { - ByteVector_FromInt.TestInt( + public fromInt_negative3Byte_bigEndian() { + this.testInt( -0x123456, [0xFF, 0xED, 0xCB, 0xAA], undefined, @@ -348,8 +428,8 @@ class ByteVector_FromInt { } @test - public Negative3Byte_LittleEndian() { - ByteVector_FromInt.TestInt( + public fromInt_negative3Byte_littleEndian() { + this.testInt( -0x123456, [0xAA, 0xCB, 0xED, 0xFF], undefined, @@ -358,8 +438,8 @@ class ByteVector_FromInt { } @test - public Negative4Byte_BigEndian() { - ByteVector_FromInt.TestInt( + public fromInt_negative4Byte_bigEndian() { + this.testInt( -0x12345678, [0xED, 0xCB, 0xA9, 0x88], undefined, @@ -368,8 +448,8 @@ class ByteVector_FromInt { } @test - public Negative4Byte_LittleEndian() { - ByteVector_FromInt.TestInt( + public fromInt_negative4Byte_littleEndian() { + this.testInt( -0x12345678, [0x88, 0xA9, 0xCB, 0xED], undefined, @@ -378,8 +458,8 @@ class ByteVector_FromInt { } @test - public ReadOnly() { - ByteVector_FromInt.TestInt( + public fromInt_readOnly() { + this.testInt( 0, [0x00, 0x00, 0x00, 0x00], true, @@ -387,42 +467,63 @@ class ByteVector_FromInt { ); } - private static TestInt(value: number, expectedData: number[], isReadOnly: boolean, bigEndian: boolean): void { - // Arrange, Act - const bv = ByteVector.fromInt(value, bigEndian, isReadOnly); + @test + public fromInternalStream_badStream() { + // Act / Assert + assert.throws(() => { ByteVector.fromInternalStream(undefined); }); + assert.throws(() => { ByteVector.fromInternalStream(null); }); + } + + @test + public fromInternalStream_empty() { + // Arrange - Create a stream with no data in it + const stream = new TestStream(ByteVector.empty(), false); + + // Act + const bv = ByteVector.fromInternalStream(stream); // Assert assert.isOk(bv); - assert.strictEqual(bv.length, 4); + assert.strictEqual(bv.length, 0); + assert.isTrue(bv.isEmpty); + assert.isFalse(bv.isReadOnly); + assert.deepEqual(bv.data, new Uint8Array([])); + } + + @test + public fromInternalStream_hasBytes() { + // Arrange - Create a stream with some data in it + const bytes = new Uint8Array(TestConstants.testFileContents); + const stream = new TestStream(ByteVector.fromByteArray(bytes), false); + + // Act - Get the promise, end the stream, await the promise + const bv = ByteVector.fromInternalStream(stream); + + // Assert + assert.isOk(bv); + assert.strictEqual(bv.length, bytes.length); assert.isFalse(bv.isEmpty); - if (isReadOnly !== undefined) { - assert.strictEqual(bv.isReadOnly, isReadOnly); - } else { - assert.isFalse(bv.isReadOnly); - } - assert.deepEqual(bv.data, new Uint8Array(expectedData)); + assert.isFalse(bv.isReadOnly); + assert.deepEqual(bv.data, bytes); } -} -@suite(timeout(3000), slow(1000)) -class ByteVector_FromLong { @test - public BadValue() { + public fromLong_badValue() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromLong(undefined); }); assert.throws(() => { ByteVector.fromLong(null); }); } @test - public Overflow() { + public fromLong_overflow() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromLong(BigInt("9223372036854775808")); }); assert.throws(() => { ByteVector.fromLong(BigInt("-9223372036854775809")); }); } @test - public Positive1Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive1Byte_bigEndian() { + this.testLong( BigInt("0x12"), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12], undefined, @@ -431,8 +532,8 @@ class ByteVector_FromLong { } @test - public Positive1Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive1Byte_littleEndian() { + this.testLong( BigInt("0x12"), [0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -441,8 +542,8 @@ class ByteVector_FromLong { } @test - public Positive2Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive2Byte_bigEndian() { + this.testLong( BigInt("0x1234"), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34], undefined, @@ -451,8 +552,8 @@ class ByteVector_FromLong { } @test - public Positive2Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive2Byte_littleEndian() { + this.testLong( BigInt("0x1234"), [0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -461,8 +562,8 @@ class ByteVector_FromLong { } @test - public Positive3Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive3Byte_bigEndian() { + this.testLong( BigInt("0x123456"), [0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56], undefined, @@ -471,8 +572,8 @@ class ByteVector_FromLong { } @test - public Positive3Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive3Byte_littleEndian() { + this.testLong( BigInt("0x123456"), [0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -481,8 +582,8 @@ class ByteVector_FromLong { } @test - public Positive4Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive4Byte_bigEndian() { + this.testLong( BigInt("0x12345678"), [0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78], undefined, @@ -491,8 +592,8 @@ class ByteVector_FromLong { } @test - public Positive4Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive4Byte_littleEndian() { + this.testLong( BigInt("0x12345678"), [0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00], undefined, @@ -501,8 +602,8 @@ class ByteVector_FromLong { } @test - public Positive5Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive5Byte_bigEndian() { + this.testLong( BigInt("0x123456789A"), [0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9A], undefined, @@ -511,8 +612,8 @@ class ByteVector_FromLong { } @test - public Positive5Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive5Byte_littleEndian() { + this.testLong( BigInt("0x123456789A"), [0x9A, 0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00], undefined, @@ -521,8 +622,8 @@ class ByteVector_FromLong { } @test - public Positive6Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive6Byte_bigEndian() { + this.testLong( BigInt("0x123456789ABC"), [0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC], undefined, @@ -531,8 +632,8 @@ class ByteVector_FromLong { } @test - public Positive6Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive6Byte_littleEndian() { + this.testLong( BigInt("0x123456789ABC"), [0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12, 0x00, 0x00], undefined, @@ -541,8 +642,8 @@ class ByteVector_FromLong { } @test - public Positive7Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive7Byte_bigEndian() { + this.testLong( BigInt("0x123456789ABCDE"), [0x00, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE], undefined, @@ -551,8 +652,8 @@ class ByteVector_FromLong { } @test - public Positive7Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive7Byte_littleEndian() { + this.testLong( BigInt("0x123456789ABCDE"), [0xDE, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12, 0x00], undefined, @@ -561,8 +662,8 @@ class ByteVector_FromLong { } @test - public Positive8Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive8Byte_bigEndian() { + this.testLong( BigInt("123456789ABCDEF0", 16), [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0], undefined, @@ -571,8 +672,8 @@ class ByteVector_FromLong { } @test - public Positive8Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_positive8Byte_littleEndian() { + this.testLong( BigInt("123456789ABCDEF0", 16), [0xF0, 0xDE, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12], undefined, @@ -581,8 +682,8 @@ class ByteVector_FromLong { } @test - public Negative1Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative1Byte_bigEndian() { + this.testLong( BigInt("-12", 16), [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEE], undefined, @@ -591,8 +692,8 @@ class ByteVector_FromLong { } @test - public Negative1Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative1Byte_littleEndian() { + this.testLong( BigInt("-12", 16), [0xEE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -601,8 +702,8 @@ class ByteVector_FromLong { } @test - public Negative2Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative2Byte_bigEndian() { + this.testLong( BigInt("-1234", 16), [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xED, 0xCC], undefined, @@ -611,8 +712,8 @@ class ByteVector_FromLong { } @test - public Negative2Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative2Byte_littleEndian() { + this.testLong( BigInt("-1234", 16), [0xCC, 0xED, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -621,8 +722,8 @@ class ByteVector_FromLong { } @test - public Negative3Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative3Byte_bigEndian() { + this.testLong( BigInt("-123456", 16), [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xED, 0xCB, 0xAA], undefined, @@ -631,8 +732,8 @@ class ByteVector_FromLong { } @test - public Negative3Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative3Byte_littleEndian() { + this.testLong( BigInt("-123456", 16), [0xAA, 0xCB, 0xED, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -641,8 +742,8 @@ class ByteVector_FromLong { } @test - public Negative4Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative4Byte_bigEndian() { + this.testLong( BigInt("-12345678", 16), [0xFF, 0xFF, 0xFF, 0xFF, 0xED, 0xCB, 0xA9, 0x88], undefined, @@ -651,8 +752,8 @@ class ByteVector_FromLong { } @test - public Negative4Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative4Byte_littleEndian() { + this.testLong( BigInt("-12345678", 16), [0x88, 0xA9, 0xCB, 0xED, 0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -661,8 +762,8 @@ class ByteVector_FromLong { } @test - public Negative5Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative5Byte_bigEndian() { + this.testLong( BigInt("-123456789A", 16), [0xFF, 0xFF, 0xFF, 0xED, 0xCB, 0xA9, 0x87, 0x66], undefined, @@ -671,8 +772,8 @@ class ByteVector_FromLong { } @test - public Negative5Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative5Byte_littleEndian() { + this.testLong( BigInt("-123456789A", 16), [0x66, 0x87, 0xA9, 0xCB, 0xED, 0xFF, 0xFF, 0xFF], undefined, @@ -681,8 +782,8 @@ class ByteVector_FromLong { } @test - public Negative6Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative6Byte_bigEndian() { + this.testLong( BigInt("-123456789ABC", 16), [0xFF, 0xFF, 0xED, 0xCB, 0xA9, 0x87, 0x65, 0x44], undefined, @@ -691,8 +792,8 @@ class ByteVector_FromLong { } @test - public Negative6Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative6Byte_littleEndian() { + this.testLong( BigInt("-123456789ABC", 16), [0x44, 0x65, 0x87, 0xA9, 0xCB, 0xED, 0xFF, 0xFF], undefined, @@ -701,8 +802,8 @@ class ByteVector_FromLong { } @test - public Negative7Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative7Byte_bigEndian() { + this.testLong( BigInt("-123456789ABCDE", 16), [0xFF, 0xED, 0xCB, 0xA9, 0x87, 0x65, 0x43, 0x22], undefined, @@ -711,8 +812,8 @@ class ByteVector_FromLong { } @test - public Negative7Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative7Byte_littleEndian() { + this.testLong( BigInt("-123456789ABCDE", 16), [0x22, 0x43, 0x65, 0x87, 0xA9, 0xCB, 0xED, 0xFF], undefined, @@ -721,8 +822,8 @@ class ByteVector_FromLong { } @test - public Negative8Byte_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative8Byte_bigEndian() { + this.testLong( BigInt("-123456789ABCDEF0", 16), [0xED, 0xCB, 0xA9, 0x87, 0x65, 0x43, 0x21, 0x10], undefined, @@ -731,8 +832,8 @@ class ByteVector_FromLong { } @test - public Negative8Byte_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_negative8Byte_littleEndian() { + this.testLong( BigInt("-123456789ABCDEF0", 16), [0x10, 0x21, 0x43, 0x65, 0x87, 0xA9, 0xCB, 0xED], undefined, @@ -741,8 +842,8 @@ class ByteVector_FromLong { } @test - public Zero_BigEndian() { - ByteVector_FromLong.TestLong( + public fromLong_zero_bigEndian() { + this.testLong( BigInt(0), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -751,8 +852,8 @@ class ByteVector_FromLong { } @test - public Zero_LittleEndian() { - ByteVector_FromLong.TestLong( + public fromLong_zero_littleEndian() { + this.testLong( BigInt(0), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -761,8 +862,8 @@ class ByteVector_FromLong { } @test - public ReadOnly() { - ByteVector_FromLong.TestLong( + public fromLong_readOnly() { + this.testLong( BigInt(0), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], true, @@ -770,32 +871,8 @@ class ByteVector_FromLong { ); } - private static TestLong( - value: BigInt.BigInteger, - expectedData: number[], - isReadOnly: boolean, - bigEndian: boolean - ): void { - // Arrange, Act - const bv = ByteVector.fromLong(value, bigEndian, isReadOnly); - - // Assert - assert.isOk(bv); - assert.strictEqual(bv.length, 8); - assert.isFalse(bv.isEmpty); - if (isReadOnly !== undefined) { - assert.strictEqual(bv.isReadOnly, isReadOnly); - } else { - assert.isFalse(bv.isReadOnly); - } - assert.deepEqual(bv.data, new Uint8Array(expectedData)); - } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_FromPath { @test - public NoPath() { + public fromPath_noPath() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromPath(undefined); }); assert.throws(() => { ByteVector.fromPath(null); }); @@ -803,7 +880,7 @@ class ByteVector_FromPath { } @test - public WithPath() { + public fromPath_withPath() { // Arrange, Act const bv = ByteVector.fromPath(TestConstants.testFilePath); @@ -816,7 +893,7 @@ class ByteVector_FromPath { } @test - public ReadOnly() { + public fromPath_readOnly() { // Arrange, Act const bv = ByteVector.fromPath(TestConstants.testFilePath, true); @@ -827,12 +904,9 @@ class ByteVector_FromPath { assert.isTrue(bv.isReadOnly); assert.deepEqual(bv.data, new Uint8Array(TestConstants.testFileContents)); } -} -@suite(timeout(3000), slow(1000)) -class ByteVector_FromShort { @test - public BadIntShort() { + public fromShort_badIntShort() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromShort(undefined); }); assert.throws(() => { ByteVector.fromShort(null); }); @@ -841,15 +915,15 @@ class ByteVector_FromShort { } @test - public Overflow() { + public fromShort_overflow() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromShort(0x1000000); }); assert.throws(() => { ByteVector.fromShort(-0x1000000); }); } @test - public Zero_BigEndian() { - ByteVector_FromShort.TestShort( + public fromShort_zero_bigEndian() { + this.testShort( 0x0, [0x0, 0x0], undefined, @@ -858,8 +932,8 @@ class ByteVector_FromShort { } @test - public Zero_LittleEndian() { - ByteVector_FromShort.TestShort( + public fromShort_zero_littleEndian() { + this.testShort( 0x0, [0x0, 0x0], undefined, @@ -868,8 +942,8 @@ class ByteVector_FromShort { } @test - public Positive1Byte_BigEndian() { - ByteVector_FromShort.TestShort( + public fromShort_positive1Byte_bigEndian() { + this.testShort( 0x12, [0x00, 0x12], undefined, @@ -878,8 +952,8 @@ class ByteVector_FromShort { } @test - public Positive1Byte_LittleEndian() { - ByteVector_FromShort.TestShort( + public fromShort_positive1Byte_littleEndian() { + this.testShort( 0x12, [0x12, 0x00], undefined, @@ -888,8 +962,8 @@ class ByteVector_FromShort { } @test - public Positive2Byte_BigEndian() { - ByteVector_FromShort.TestShort( + public fromShort_positive2Byte_bigEndian() { + this.testShort( 0x1234, [0x12, 0x34], undefined, @@ -898,8 +972,8 @@ class ByteVector_FromShort { } @test - public Positive2Byte_LittleEndian() { - ByteVector_FromShort.TestShort( + public fromShort_positive2Byte_littleEndian() { + this.testShort( 0x1234, [0x34, 0x12], undefined, @@ -908,8 +982,8 @@ class ByteVector_FromShort { } @test - public Negative1Byte_BigEndian() { - ByteVector_FromShort.TestShort( + public fromShort_negative1Byte_bigEndian() { + this.testShort( -0x12, [0xFF, 0xEE], undefined, @@ -918,8 +992,8 @@ class ByteVector_FromShort { } @test - public Negative1Byte_LittleEndian() { - ByteVector_FromShort.TestShort( + public fromShort_negative1Byte_littleEndian() { + this.testShort( -0x12, [0xEE, 0xFF], undefined, @@ -928,8 +1002,8 @@ class ByteVector_FromShort { } @test - public Negative2Byte_BigEndian() { - ByteVector_FromShort.TestShort( + public fromShort_negative2Byte_bigEndian() { + this.testShort( -0x1234, [0xED, 0xCC], undefined, @@ -938,8 +1012,8 @@ class ByteVector_FromShort { } @test - public Negative2Byte_LittleEndian() { - ByteVector_FromShort.TestShort( + public fromShort_negative2Byte_littleEndian() { + this.testShort( -0x1234, [0xCC, 0xED], undefined, @@ -948,8 +1022,8 @@ class ByteVector_FromShort { } @test - public ReadOnly() { - ByteVector_FromShort.TestShort( + public fromShort_readOnly() { + this.testShort( 0x0, [0x0, 0x0], true, @@ -957,27 +1031,8 @@ class ByteVector_FromShort { ); } - private static TestShort(value: number, expectedData: number[], isReadOnly: boolean, bigEndian: boolean): void { - // Arrange, Act - const bv = ByteVector.fromShort(value, bigEndian, isReadOnly); - - // Assert - assert.isOk(bv); - assert.strictEqual(bv.length, 2); - assert.isFalse(bv.isEmpty); - if (isReadOnly !== undefined) { - assert.strictEqual(bv.isReadOnly, isReadOnly); - } else { - assert.isFalse(bv.isReadOnly); - } - assert.deepEqual(bv.data, new Uint8Array(expectedData)); - } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_FromSize { @test - public BadSize() { + public fromSize_badSize() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromSize(undefined); }); assert.throws(() => { ByteVector.fromSize(null); }); @@ -987,7 +1042,7 @@ class ByteVector_FromSize { } @test - public BadFillValue() { + public fromSize_badFillValue() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromSize(1, 0.1); }); assert.throws(() => { ByteVector.fromSize(1, -1); }); @@ -995,7 +1050,7 @@ class ByteVector_FromSize { } @test - public ZeroSize() { + public fromSize_zeroSize() { // Arrange, Act const bv = ByteVector.fromSize(0); @@ -1008,7 +1063,7 @@ class ByteVector_FromSize { } @test - public WithoutFill() { + public fromSize_withoutFill() { // Arrange, Act const bv = ByteVector.fromSize(4); @@ -1021,7 +1076,7 @@ class ByteVector_FromSize { } @test - public WithFill() { + public fromSize_withFill() { // Arrange, Act const bv = ByteVector.fromSize(4, 0xEE); @@ -1034,7 +1089,7 @@ class ByteVector_FromSize { } @test - public ReadOnly() { + public fromSize_readOnly() { // Arrange, Act const bv = ByteVector.fromSize(4, undefined, true); @@ -1045,12 +1100,9 @@ class ByteVector_FromSize { assert.isTrue(bv.isReadOnly); assert.deepEqual(bv.data, new Uint8Array([0x00, 0x00, 0x00, 0x00])); } -} -@suite(timeout(3000), slow(1000)) -class ByteVector_FromStream { @test - public async NoStream() { + public async fromStream_noStream() { await Promise.all([ assert.isRejected(ByteVector.fromStream(undefined)), assert.isRejected(ByteVector.fromStream(null)) @@ -1058,7 +1110,7 @@ class ByteVector_FromStream { } @test - public async Empty() { + public async fromStream_empty() { // Arrange - Create a stream with no data in it const stream = new StreamBuffers.ReadableStreamBuffer(); @@ -1076,7 +1128,7 @@ class ByteVector_FromStream { } @test - public async ReadWrite() { + public async fromStream_readWrite() { // Arrange - Create a stream with some data in it const stream = new StreamBuffers.ReadableStreamBuffer(); const bytes = new Uint8Array(TestConstants.testFileContents); @@ -1096,7 +1148,7 @@ class ByteVector_FromStream { } @test - public async ReadOnly() { + public async fromStream_readOnly() { // Arrange - Create a stream with some data in it const stream = new StreamBuffers.ReadableStreamBuffer(); const bytes = new Uint8Array(TestConstants.testFileContents); @@ -1114,12 +1166,9 @@ class ByteVector_FromStream { assert.isTrue(bv.isReadOnly); assert.deepEqual(bv.data, bytes); } -} -@suite(timeout(3000), slow(1000)) -class ByteVector_FromString { @test - public InvalidLength() { + public fromString_invalidLength() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromString("", undefined, 0.1); }); assert.throws(() => { ByteVector.fromString("", undefined, Number.MAX_SAFE_INTEGER + 1); }); @@ -1127,8 +1176,8 @@ class ByteVector_FromString { } @test - public Utf8Full() { - ByteVector_FromString.TestString( + public fromString_utf8Full() { + this.testString( TestConstants.testStrings.UTF8.str, TestConstants.testStrings.UTF8.bytes, undefined, @@ -1138,8 +1187,8 @@ class ByteVector_FromString { } @test - public Utf8Partial() { - ByteVector_FromString.TestString( + public fromString_utf8Partial() { + this.testString( TestConstants.testStrings.UTF8.str, TestConstants.testStrings.UTF8.bytes.slice(0, 9), undefined, @@ -1149,8 +1198,8 @@ class ByteVector_FromString { } @test - public Utf8Empty() { - ByteVector_FromString.TestString( + public fromString_utf8Empty() { + this.testString( "", [], undefined, @@ -1160,9 +1209,9 @@ class ByteVector_FromString { } @test - public Utf16LittleEndianFull() { + public fromString_utf16LittleEndianFull() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; - ByteVector_FromString.TestString( + this.testString( TestConstants.testStrings.UTF16LE.str, TestConstants.testStrings.UTF16LE.bytes, StringType.UTF16LE, @@ -1173,9 +1222,9 @@ class ByteVector_FromString { } @test - public Utf16LittleEndianPartial() { + public fromString_utf16LittleEndianPartial() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; - ByteVector_FromString.TestString( + this.testString( TestConstants.testStrings.UTF16LE.str, TestConstants.testStrings.UTF16LE.bytes.slice(0, 12), StringType.UTF16LE, @@ -1186,9 +1235,9 @@ class ByteVector_FromString { } @test - public Utf16LittleEndianEmpty() { + public fromString_utf16LittleEndianEmpty() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; - ByteVector_FromString.TestString( + this.testString( "", [], StringType.UTF16LE, @@ -1199,9 +1248,9 @@ class ByteVector_FromString { } @test - public Utf16BigEndianFull() { + public fromString_utf16BigEndianFull() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; - ByteVector_FromString.TestString( + this.testString( TestConstants.testStrings.UTF16BE.str, TestConstants.testStrings.UTF16BE.bytes, StringType.UTF16BE, @@ -1212,9 +1261,9 @@ class ByteVector_FromString { } @test - public Utf16BigEndianPartial() { + public fromString_utf16BigEndianPartial() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; - ByteVector_FromString.TestString( + this.testString( TestConstants.testStrings.UTF16BE.str, TestConstants.testStrings.UTF16BE.bytes.slice(0, 12), StringType.UTF16BE, @@ -1225,9 +1274,9 @@ class ByteVector_FromString { } @test - public Utf16BigEndianEmpty() { + public fromString_utf16BigEndianEmpty() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; - ByteVector_FromString.TestString( + this.testString( "", [], StringType.UTF16BE, @@ -1238,8 +1287,8 @@ class ByteVector_FromString { } @test - public Latin1Full() { - ByteVector_FromString.TestString( + public fromString_latin1Full() { + this.testString( TestConstants.testStrings.Latin1.str, TestConstants.testStrings.Latin1.bytes, StringType.Latin1, @@ -1249,8 +1298,8 @@ class ByteVector_FromString { } @test - public Latin1Partial() { - ByteVector_FromString.TestString( + public fromString_latin1Partial() { + this.testString( TestConstants.testStrings.Latin1.str, TestConstants.testStrings.Latin1.bytes.slice(0, 6), StringType.Latin1, @@ -1260,8 +1309,8 @@ class ByteVector_FromString { } @test - public Latin1Empty() { - ByteVector_FromString.TestString( + public fromString_latin1Empty() { + this.testString( "", [], StringType.Latin1, @@ -1271,12 +1320,12 @@ class ByteVector_FromString { } @test - public Utf16Full() { + public fromString_utf16Full() { // This test will change the last used utf16 encoding, so we'll restore it afterward const originalLastEncoding = ByteVector.lastUtf16Encoding; ByteVector.lastUtf16Encoding = "something bogus"; - ByteVector_FromString.TestString( + this.testString( TestConstants.testStrings.UTF16LEWithBOM.str, TestConstants.testStrings.UTF16LEWithBOM.bytes, StringType.UTF16, @@ -1290,12 +1339,12 @@ class ByteVector_FromString { } @test - public Utf16Partial() { + public fromString_utf16Partial() { // This test will change the last used utf16 encoding, so we'll restore it afterward const originalLastEncoding = ByteVector.lastUtf16Encoding; ByteVector.lastUtf16Encoding = "something bogus"; - ByteVector_FromString.TestString( + this.testString( TestConstants.testStrings.UTF16LEWithBOM.str, TestConstants.testStrings.UTF16LEWithBOM.bytes.slice(0, 14), StringType.UTF16, @@ -1309,12 +1358,12 @@ class ByteVector_FromString { } @test - public Utf16Empty() { + public fromString_utf16Empty() { // This test will change the last used utf16 encoding, so we'll restore it afterward const originalLastEncoding = ByteVector.lastUtf16Encoding; ByteVector.lastUtf16Encoding = "something bogus"; - ByteVector_FromString.TestString( + this.testString( "", TestConstants.testStrings.UTF16LEWithBOM.bytes.slice(0, 2), StringType.UTF16, @@ -1328,8 +1377,8 @@ class ByteVector_FromString { } @test - public ReadOnly() { - ByteVector_FromString.TestString( + public fromString_readOnly() { + this.testString( "", [], StringType.Latin1, @@ -1338,32 +1387,8 @@ class ByteVector_FromString { ); } - private static TestString( - str: string, - expectedData: number[], - stringType: StringType, - inputLength: number, - isReadOnly: boolean - ) { - // Arrange, Act - const bv = ByteVector.fromString(str, stringType, inputLength, isReadOnly); - - // Assert - assert.isOk(bv); - assert.strictEqual(bv.isEmpty, expectedData.length === 0); - if (isReadOnly !== undefined) { - assert.strictEqual(bv.isReadOnly, isReadOnly); - } else { - assert.isFalse(bv.isReadOnly); - } - assert.deepEqual(bv.data, new Uint8Array(expectedData)); - } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_FromUInt { @test - public BadInteger() { + public fromUInt_badInteger() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromUInt(undefined); }); assert.throws(() => { ByteVector.fromUInt(null); }); @@ -1373,14 +1398,14 @@ class ByteVector_FromUInt { } @test - public Overflow() { + public fromUInt_overflow() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromUInt(0x10000000000); }); } @test - public Zero_BigEndian() { - ByteVector_FromUInt.TestUInt( + public fromUInt_zero_bigEndian() { + this.testUInt( 0x0, [0x0, 0x0, 0x0, 0x0], undefined, @@ -1389,8 +1414,8 @@ class ByteVector_FromUInt { } @test - public Zero_LittleEndian() { - ByteVector_FromUInt.TestUInt( + public fromUInt_zero_littleEndian() { + this.testUInt( 0x0, [0x0, 0x0, 0x0, 0x0], undefined, @@ -1399,8 +1424,8 @@ class ByteVector_FromUInt { } @test - public Positive1Byte_BigEndian() { - ByteVector_FromUInt.TestUInt( + public fromUInt_positive1Byte_bigEndian() { + this.testUInt( 0x12, [0x00, 0x00, 0x00, 0x12], undefined, @@ -1409,8 +1434,8 @@ class ByteVector_FromUInt { } @test - public Positive1Byte_LittleEndian() { - ByteVector_FromUInt.TestUInt( + public fromUInt_positive1Byte_littleEndian() { + this.testUInt( 0x12, [0x12, 0x00, 0x00, 0x00], undefined, @@ -1419,8 +1444,8 @@ class ByteVector_FromUInt { } @test - public Positive2Byte_BigEndian() { - ByteVector_FromUInt.TestUInt( + public fromUInt_positive2Byte_bigEndian() { + this.testUInt( 0x1234, [0x00, 0x00, 0x12, 0x34], undefined, @@ -1429,8 +1454,8 @@ class ByteVector_FromUInt { } @test - public Positive2Byte_LittleEndian() { - ByteVector_FromUInt.TestUInt( + public fromUInt_positive2Byte_littleEndian() { + this.testUInt( 0x1234, [0x34, 0x12, 0x00, 0x00], undefined, @@ -1439,8 +1464,8 @@ class ByteVector_FromUInt { } @test - public Positive3Byte_BigEndian() { - ByteVector_FromUInt.TestUInt( + public fromUInt_positive3Byte_bigEndian() { + this.testUInt( 0x123456, [0x00, 0x12, 0x34, 0x56], undefined, @@ -1449,8 +1474,8 @@ class ByteVector_FromUInt { } @test - public Positive3Byte_LittleEndian() { - ByteVector_FromUInt.TestUInt( + public fromUInt_positive3Byte_littleEndian() { + this.testUInt( 0x123456, [0x56, 0x34, 0x12, 0x00], undefined, @@ -1459,8 +1484,8 @@ class ByteVector_FromUInt { } @test - public Positive4Byte_BigEndian() { - ByteVector_FromUInt.TestUInt( + public fromUInt_positive4Byte_bigEndian() { + this.testUInt( 0x12345678, [0x12, 0x34, 0x56, 0x78], undefined, @@ -1469,8 +1494,8 @@ class ByteVector_FromUInt { } @test - public Positive4Byte_LittleEndian() { - ByteVector_FromUInt.TestUInt( + public fromUInt_positive4Byte_littleEndian() { + this.testUInt( 0x12345678, [0x78, 0x56, 0x34, 0x12], undefined, @@ -1479,8 +1504,8 @@ class ByteVector_FromUInt { } @test - public UnsignedRange_BigEndian() { - ByteVector_FromUInt.TestUInt( + public fromUInt_unsignedRange_bigEndian() { + this.testUInt( 0xFFFFFFFF, [0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -1489,8 +1514,8 @@ class ByteVector_FromUInt { } @test - public UnsignedRange_LittleEndian() { - ByteVector_FromUInt.TestUInt( + public fromUInt_unsignedRange_littleEndian() { + this.testUInt( 0xFFFFFFFF, [0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -1499,8 +1524,8 @@ class ByteVector_FromUInt { } @test - public ReadOnly() { - ByteVector_FromUInt.TestUInt( + public fromUInt_readOnly() { + this.testUInt( 0, [0x00, 0x00, 0x00, 0x00], true, @@ -1508,42 +1533,23 @@ class ByteVector_FromUInt { ); } - private static TestUInt(value: number, expectedData: number[], isReadOnly: boolean, bigEndian: boolean): void { - // Arrange, Act - const bv = ByteVector.fromUInt(value, bigEndian, isReadOnly); - - // Assert - assert.isOk(bv); - assert.strictEqual(bv.length, 4); - assert.isFalse(bv.isEmpty); - if (isReadOnly !== undefined) { - assert.strictEqual(bv.isReadOnly, isReadOnly); - } else { - assert.isFalse(bv.isReadOnly); - } - assert.deepEqual(bv.data, new Uint8Array(expectedData)); - } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_FromULong { @test - public BadValue() { + public fromULong_badValue() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromULong(undefined); }); assert.throws(() => { ByteVector.fromULong(null); }); } @test - public Overflow() { + public fromULong_overflow() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromULong(BigInt("18446744073709551616")); }); assert.throws(() => { ByteVector.fromULong(BigInt("-1")); }); } @test - public Positive1Byte_BigEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive1Byte_bigEndian() { + this.testULong( BigInt("0x12"), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12], undefined, @@ -1552,8 +1558,8 @@ class ByteVector_FromULong { } @test - public Positive1Byte_LittleEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive1Byte_littleEndian() { + this.testULong( BigInt("0x12"), [0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -1562,8 +1568,8 @@ class ByteVector_FromULong { } @test - public Positive2Byte_BigEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive2Byte_bigEndian() { + this.testULong( BigInt("0x1234"), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34], undefined, @@ -1572,8 +1578,8 @@ class ByteVector_FromULong { } @test - public Positive2Byte_LittleEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive2Byte_littleEndian() { + this.testULong( BigInt("0x1234"), [0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -1582,8 +1588,8 @@ class ByteVector_FromULong { } @test - public Positive3Byte_BigEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive3Byte_bigEndian() { + this.testULong( BigInt("0x123456"), [0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56], undefined, @@ -1592,8 +1598,8 @@ class ByteVector_FromULong { } @test - public Positive3Byte_LittleEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive3Byte_littleEndian() { + this.testULong( BigInt("0x123456"), [0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -1602,8 +1608,8 @@ class ByteVector_FromULong { } @test - public Positive4Byte_BigEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive4Byte_bigEndian() { + this.testULong( BigInt("0x12345678"), [0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78], undefined, @@ -1612,8 +1618,8 @@ class ByteVector_FromULong { } @test - public Positive4Byte_LittleEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive4Byte_littleEndian() { + this.testULong( BigInt("0x12345678"), [0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00], undefined, @@ -1622,8 +1628,8 @@ class ByteVector_FromULong { } @test - public Positive5Byte_BigEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive5Byte_bigEndian() { + this.testULong( BigInt("0x123456789A"), [0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9A], undefined, @@ -1632,8 +1638,8 @@ class ByteVector_FromULong { } @test - public Positive5Byte_LittleEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive5Byte_littleEndian() { + this.testULong( BigInt("0x123456789A"), [0x9A, 0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00], undefined, @@ -1642,8 +1648,8 @@ class ByteVector_FromULong { } @test - public Positive6Byte_BigEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive6Byte_bigEndian() { + this.testULong( BigInt("0x123456789ABC"), [0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC], undefined, @@ -1652,8 +1658,8 @@ class ByteVector_FromULong { } @test - public Positive6Byte_LittleEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive6Byte_littleEndian() { + this.testULong( BigInt("0x123456789ABC"), [0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12, 0x00, 0x00], undefined, @@ -1662,8 +1668,8 @@ class ByteVector_FromULong { } @test - public Positive7Byte_BigEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive7Byte_bigEndian() { + this.testULong( BigInt("0x123456789ABCDE"), [0x00, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE], undefined, @@ -1672,8 +1678,8 @@ class ByteVector_FromULong { } @test - public Positive7Byte_LittleEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive7Byte_littleEndian() { + this.testULong( BigInt("0x123456789ABCDE"), [0xDE, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12, 0x00], undefined, @@ -1682,8 +1688,8 @@ class ByteVector_FromULong { } @test - public Positive8Byte_BigEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive8Byte_bigEndian() { + this.testULong( BigInt("123456789ABCDEF0", 16), [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0], undefined, @@ -1692,8 +1698,8 @@ class ByteVector_FromULong { } @test - public Positive8Byte_LittleEndian() { - ByteVector_FromULong.TestULong( + public fromULong_positive8Byte_littleEndian() { + this.testULong( BigInt("123456789ABCDEF0", 16), [0xF0, 0xDE, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12], undefined, @@ -1702,8 +1708,8 @@ class ByteVector_FromULong { } @test - public UnsignedRange_BigEndian() { - ByteVector_FromULong.TestULong( + public fromULong_unsignedRange_bigEndian() { + this.testULong( BigInt("FFFFFFFFFFFFFFFF", 16), [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -1712,8 +1718,8 @@ class ByteVector_FromULong { } @test - public UnsignedRange_LittleEndian() { - ByteVector_FromULong.TestULong( + public fromULong_unsignedRange_littleEndian() { + this.testULong( BigInt("FFFFFFFFFFFFFFFF", 16), [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], undefined, @@ -1722,8 +1728,8 @@ class ByteVector_FromULong { } @test - public Zero_BigEndian() { - ByteVector_FromULong.TestULong( + public fromULong_zero_bigEndian() { + this.testULong( BigInt(0), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -1732,8 +1738,8 @@ class ByteVector_FromULong { } @test - public Zero_LittleEndian() { - ByteVector_FromULong.TestULong( + public fromULong_zero_littleEndian() { + this.testULong( BigInt(0), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], undefined, @@ -1742,8 +1748,8 @@ class ByteVector_FromULong { } @test - public ReadOnly() { - ByteVector_FromULong.TestULong( + public fromULong_readOnly() { + this.testULong( BigInt(0), [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], true, @@ -1751,32 +1757,8 @@ class ByteVector_FromULong { ); } - private static TestULong( - value: BigInt.BigInteger, - expectedData: number[], - isReadOnly: boolean, - bigEndian: boolean - ): void { - // Arrange, Act - const bv = ByteVector.fromULong(value, bigEndian, isReadOnly); - - // Assert - assert.isOk(bv); - assert.strictEqual(bv.length, 8); - assert.isFalse(bv.isEmpty); - if (isReadOnly !== undefined) { - assert.strictEqual(bv.isReadOnly, isReadOnly); - } else { - assert.isFalse(bv.isReadOnly); - } - assert.deepEqual(bv.data, new Uint8Array(expectedData)); - } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_FromUShort { @test - public BadShort() { + public fromUShort_badShort() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromUShort(undefined); }); assert.throws(() => { ByteVector.fromUShort(null); }); @@ -1785,15 +1767,15 @@ class ByteVector_FromUShort { } @test - public Overflow() { + public fromUShort_overflow() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromUShort(0x1000000); }); assert.throws(() => { ByteVector.fromUShort(-0x1000000); }); } @test - public Zero_BigEndian() { - ByteVector_FromUShort.TestUShort( + public fromUShort_zero_bigEndian() { + this.testUShort( 0x0, [0x0, 0x0], undefined, @@ -1802,8 +1784,8 @@ class ByteVector_FromUShort { } @test - public Zero_LittleEndian() { - ByteVector_FromUShort.TestUShort( + public fromUShort_zero_littleEndian() { + this.testUShort( 0x0, [0x0, 0x0], undefined, @@ -1812,8 +1794,8 @@ class ByteVector_FromUShort { } @test - public Positive1Byte_BigEndian() { - ByteVector_FromUShort.TestUShort( + public fromUShort_positive1Byte_bigEndian() { + this.testUShort( 0x12, [0x00, 0x12], undefined, @@ -1822,8 +1804,8 @@ class ByteVector_FromUShort { } @test - public Positive1Byte_LittleEndian() { - ByteVector_FromUShort.TestUShort( + public fromUShort_positive1Byte_littleEndian() { + this.testUShort( 0x12, [0x12, 0x00], undefined, @@ -1832,8 +1814,8 @@ class ByteVector_FromUShort { } @test - public Positive2Byte_BigEndian() { - ByteVector_FromUShort.TestUShort( + public fromUShort_positive2Byte_bigEndian() { + this.testUShort( 0x1234, [0x12, 0x34], undefined, @@ -1842,8 +1824,8 @@ class ByteVector_FromUShort { } @test - public Positive2Byte_LittleEndian() { - ByteVector_FromUShort.TestUShort( + public fromUShort_positive2Byte_littleEndian() { + this.testUShort( 0x1234, [0x34, 0x12], undefined, @@ -1852,8 +1834,8 @@ class ByteVector_FromUShort { } @test - public UnsignedRange_BigEndian() { - ByteVector_FromUShort.TestUShort( + public fromUShort_unsignedRange_bigEndian() { + this.testUShort( 0xFFFF, [0xFF, 0xFF], undefined, @@ -1862,8 +1844,8 @@ class ByteVector_FromUShort { } @test - public UnsignedRange_LittleEndian() { - ByteVector_FromUShort.TestUShort( + public fromUShort_unsignedRange_littleEndian() { + this.testUShort( 0xFFFF, [0xFF, 0xFF], undefined, @@ -1872,16 +1854,127 @@ class ByteVector_FromUShort { } @test - public ReadOnly() { - ByteVector_FromUShort.TestUShort( + public fromUShort_readOnly() { + this.testUShort( 0x0, [0x0, 0x0], true, undefined ); } + + private testInt(value: number, expectedData: number[], isReadOnly: boolean, bigEndian: boolean): void { + // Arrange, Act + const bv = ByteVector.fromInt(value, bigEndian, isReadOnly); + + // Assert + assert.isOk(bv); + assert.strictEqual(bv.length, 4); + assert.isFalse(bv.isEmpty); + if (isReadOnly !== undefined) { + assert.strictEqual(bv.isReadOnly, isReadOnly); + } else { + assert.isFalse(bv.isReadOnly); + } + assert.deepEqual(bv.data, new Uint8Array(expectedData)); + } + + private testLong( + value: BigInt.BigInteger, + expectedData: number[], + isReadOnly: boolean, + bigEndian: boolean + ): void { + // Arrange, Act + const bv = ByteVector.fromLong(value, bigEndian, isReadOnly); + + // Assert + assert.isOk(bv); + assert.strictEqual(bv.length, 8); + assert.isFalse(bv.isEmpty); + if (isReadOnly !== undefined) { + assert.strictEqual(bv.isReadOnly, isReadOnly); + } else { + assert.isFalse(bv.isReadOnly); + } + assert.deepEqual(bv.data, new Uint8Array(expectedData)); + } + + private testShort(value: number, expectedData: number[], isReadOnly: boolean, bigEndian: boolean): void { + // Arrange, Act + const bv = ByteVector.fromShort(value, bigEndian, isReadOnly); + + // Assert + assert.isOk(bv); + assert.strictEqual(bv.length, 2); + assert.isFalse(bv.isEmpty); + if (isReadOnly !== undefined) { + assert.strictEqual(bv.isReadOnly, isReadOnly); + } else { + assert.isFalse(bv.isReadOnly); + } + assert.deepEqual(bv.data, new Uint8Array(expectedData)); + } + + private testString( + str: string, + expectedData: number[], + stringType: StringType, + inputLength: number, + isReadOnly: boolean + ) { + // Arrange, Act + const bv = ByteVector.fromString(str, stringType, inputLength, isReadOnly); + + // Assert + assert.isOk(bv); + assert.strictEqual(bv.isEmpty, expectedData.length === 0); + if (isReadOnly !== undefined) { + assert.strictEqual(bv.isReadOnly, isReadOnly); + } else { + assert.isFalse(bv.isReadOnly); + } + assert.deepEqual(bv.data, new Uint8Array(expectedData)); + } + + private testUInt(value: number, expectedData: number[], isReadOnly: boolean, bigEndian: boolean): void { + // Arrange, Act + const bv = ByteVector.fromUInt(value, bigEndian, isReadOnly); + + // Assert + assert.isOk(bv); + assert.strictEqual(bv.length, 4); + assert.isFalse(bv.isEmpty); + if (isReadOnly !== undefined) { + assert.strictEqual(bv.isReadOnly, isReadOnly); + } else { + assert.isFalse(bv.isReadOnly); + } + assert.deepEqual(bv.data, new Uint8Array(expectedData)); + } + + private testULong( + value: BigInt.BigInteger, + expectedData: number[], + isReadOnly: boolean, + bigEndian: boolean + ): void { + // Arrange, Act + const bv = ByteVector.fromULong(value, bigEndian, isReadOnly); + + // Assert + assert.isOk(bv); + assert.strictEqual(bv.length, 8); + assert.isFalse(bv.isEmpty); + if (isReadOnly !== undefined) { + assert.strictEqual(bv.isReadOnly, isReadOnly); + } else { + assert.isFalse(bv.isReadOnly); + } + assert.deepEqual(bv.data, new Uint8Array(expectedData)); + } - private static TestUShort(value: number, expectedData: number[], isReadOnly: boolean, bigEndian: boolean): void { + private testUShort(value: number, expectedData: number[], isReadOnly: boolean, bigEndian: boolean): void { // Arrange, Act const bv = ByteVector.fromUShort(value, bigEndian, isReadOnly); diff --git a/test/byteVectorConversionTests.ts b/test/byteVectorConversionTests.ts index a964566a..029d4385 100644 --- a/test/byteVectorConversionTests.ts +++ b/test/byteVectorConversionTests.ts @@ -8,16 +8,76 @@ import {ByteVector, StringType} from "../src/byteVector"; const assert = Chai.assert; @suite(timeout(3000), slow(1000)) -class ByteVector_ToDoubleTests { - private static readonly PositiveBV = ByteVector.fromByteArray( // 56.12 +class ByteVector_ConversionTests { + private readonly doublePositiveBV = ByteVector.fromByteArray( // 56.12 new Uint8Array([0x8F, 0xC2, 0xF5, 0x28, 0x5C, 0x0F, 0x4C, 0x40, 0xAA]) ); - private static readonly NegativeBV = ByteVector.fromByteArray( // -12.34 + private readonly doubleNegativeBV = ByteVector.fromByteArray( // -12.34 new Uint8Array([0xAE, 0x47, 0xE1, 0x7A, 0x14, 0xAE, 0x28, 0xC0, 0xAA]) ); + private readonly floatPositiveBV = ByteVector.fromByteArray( // 56.12 + new Uint8Array([0xE1, 0x7A, 0x60, 0x42, 0xAA]) + ); + private readonly floatNegativeBV = ByteVector.fromByteArray( // -12.34 + new Uint8Array([0xA4, 0x70, 0x45, 0xC1, 0xAA]) + ); + private readonly intNegativeCompleteBV = ByteVector.fromByteArray( + new Uint8Array([0xFE, 0xFD, 0xFC, 0xFC, 0xAA]) + ); + private readonly intNegativeIncompleteBV = ByteVector.fromByteArray( + new Uint8Array([0xFC, 0xFD]) + ); + private readonly intPositiveCompleteBV = ByteVector.fromByteArray( + new Uint8Array([0x01, 0x02, 0x03, 0x04, 0xAA]) + ); + private readonly intPositiveIncompleteBV = ByteVector.fromByteArray( + new Uint8Array([0x01, 0x02]) + ); + private readonly longNegativeCompleteBV = ByteVector.fromByteArray( + new Uint8Array([0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xF8, 0xAA]) + ); + private readonly longNegativeIncompleteBV = ByteVector.fromByteArray( + new Uint8Array([0xFE, 0xFD, 0xFC, 0xFC]) + ); + private readonly longPositiveCompleteBV = ByteVector.fromByteArray( + new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0xAA]) + ); + private readonly longPositiveIncompleteBV = ByteVector.fromByteArray( + new Uint8Array([0x01, 0x02, 0x03, 0x04]) + ); + private readonly shortNegativeCompleteBV = ByteVector.fromByteArray( + new Uint8Array([0xFE, 0xFD, 0xAA]) + ); + private readonly shortNegativeIncompleteBV = ByteVector.fromByteArray( + new Uint8Array([0xFC]) + ); + private readonly shortPositiveCompleteBV = ByteVector.fromByteArray( + new Uint8Array([0x01, 0x02, 0xAA]) + ); + private readonly shortPositiveIncompleteBV = ByteVector.fromByteArray( + new Uint8Array([0x01]) + ); + private readonly uintPositiveCompleteBV = ByteVector.fromByteArray( + new Uint8Array([0x01, 0x02, 0x03, 0x04, 0xAA]) + ); + private readonly uintPositiveIncompleteBV = ByteVector.fromByteArray( + new Uint8Array([0x01, 0x02]) + ); + private readonly ulongPositiveCompleteBV = ByteVector.fromByteArray( + new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0xAA]) + ); + private readonly ulongPositiveIncompleteBV = ByteVector.fromByteArray( + new Uint8Array([0x01, 0x02, 0x03, 0x04]) + ); + private readonly ushortPositiveCompleteBV = ByteVector.fromByteArray( + new Uint8Array([0x01, 0x02, 0xAA]) + ); + private readonly ushortPositiveIncompleteBV = ByteVector.fromByteArray( + new Uint8Array([0x01]) + ); @test - public InvalidSize() { + public toDouble_invalidSize() { assert.throws(() => { ByteVector.fromSize(0).toDouble(); }); assert.throws(() => { ByteVector.fromSize(1).toDouble(); }); assert.throws(() => { ByteVector.fromSize(2).toDouble(); }); @@ -29,61 +89,51 @@ class ByteVector_ToDoubleTests { } @test - public Zero_Complete() { + public toDouble_zero_complete() { const float = ByteVector.fromByteArray(new Uint8Array([0x00, 0x00, 0x00, 0x00])).toFloat(); assert.strictEqual(float, 0); } @test - public PositiveBigEndian() { - const double = ByteVector_ToDoubleTests.PositiveBV.toDouble(); + public toDouble_positiveBigEndian() { + const double = this.doublePositiveBV.toDouble(); assert.closeTo(double, -9.5397675953257207e-233, 0.000000000001e-233); } @test - public PositiveLittleEndian() { - const double = ByteVector_ToDoubleTests.PositiveBV.toDouble(false); + public toDouble_positiveLittleEndian() { + const double = this.doublePositiveBV.toDouble(false); assert.closeTo(double, 56.12, 0.01); } @test - public NegativeBigEndian() { - const double = ByteVector_ToDoubleTests.NegativeBV.toDouble(); + public toDouble_negativeBigEndian() { + const double = this.doubleNegativeBV.toDouble(); assert.closeTo(double, -9.6037214055410557e-86, 0.00000000001e-86); } @test - public NegativeLittleEndian() { - const double = ByteVector_ToDoubleTests.NegativeBV.toDouble(false); + public toDouble_negativeLittleEndian() { + const double = this.doubleNegativeBV.toDouble(false); assert.closeTo(double, -12.34, 0.01); } @test - public DoubleRangeBigEndian() { + public toDouble_doubleRangeBigEndian() { const double = ByteVector.fromByteArray(new Uint8Array([0x7F, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])) .toDouble(); assert.closeTo(double, 1.7976931348623157e308, 0.000000000001e308); } @test - public DoubleRangeLittleEndian() { + public toDouble_doubleRangeLittleEndian() { const double = ByteVector.fromByteArray(new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0x7F])) .toDouble(false); assert.closeTo(double, 1.7976931348623157e308, 0.000000000001e308); } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_ToFloatTests { - private static readonly PositiveBV = ByteVector.fromByteArray( // 56.12 - new Uint8Array([0xE1, 0x7A, 0x60, 0x42, 0xAA]) - ); - private static readonly NegativeBV = ByteVector.fromByteArray( // -12.34 - new Uint8Array([0xA4, 0x70, 0x45, 0xC1, 0xAA]) - ); @test - public InvalidSize() { + public toFloat_invalidSize() { assert.throws(() => { ByteVector.fromSize(0).toFloat(); }); assert.throws(() => { ByteVector.fromSize(1).toFloat(); }); assert.throws(() => { ByteVector.fromSize(2).toFloat(); }); @@ -91,141 +141,109 @@ class ByteVector_ToFloatTests { } @test - public Zero_Complete() { + public toFloat_zero_complete() { const float = ByteVector.fromByteArray(new Uint8Array([0x00, 0x00, 0x00, 0x00])).toFloat(); assert.strictEqual(float, 0); } @test - public PositiveBigEndian() { - const float = ByteVector_ToFloatTests.PositiveBV.toFloat(); + public toFloat_positiveBigEndian() { + const float = this.floatPositiveBV.toFloat(); assert.closeTo(float, -2.88663883e20, 0.00000001e20); } @test - public PositiveLittleEndian() { - const float = ByteVector_ToFloatTests.PositiveBV.toFloat(false); + public toFloat_positiveLittleEndian() { + const float = this.floatPositiveBV.toFloat(false); assert.closeTo(float, 56.12, 0.01); } @test - public NegativeBigEndian() { - const float = ByteVector_ToFloatTests.NegativeBV.toFloat(); + public toFloat_negativeBigEndian() { + const float = this.floatNegativeBV.toFloat(); assert.closeTo(float, -5.21007881e-17, 0.00000001e-17); } @test - public NegativeLittleEndian() { - const float = ByteVector_ToFloatTests.NegativeBV.toFloat(false); + public toFloat_negativeLittleEndian() { + const float = this.floatNegativeBV.toFloat(false); assert.closeTo(float, -12.34, 0.01); } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_ToIntTests { - private static readonly NegativeCompleteBV = ByteVector.fromByteArray( - new Uint8Array([0xFE, 0xFD, 0xFC, 0xFC, 0xAA]) - ); - private static readonly NegativeIncompleteBV = ByteVector.fromByteArray( - new Uint8Array([0xFC, 0xFD]) - ); - private static readonly PositiveCompleteBV = ByteVector.fromByteArray( - new Uint8Array([0x01, 0x02, 0x03, 0x04, 0xAA]) - ); - private static readonly PositiveIncompleteBV = ByteVector.fromByteArray( - new Uint8Array([0x01, 0x02]) - ); @test - public Empty() { + public toInt_empty() { const int = ByteVector.fromSize(0).toInt(); assert.strictEqual(int, 0); } @test - public Zero_Complete() { + public toInt_zero_complete() { const int = ByteVector.fromByteArray(new Uint8Array([0x0, 0x0, 0x0, 0x0, 0xAA])).toInt(); assert.strictEqual(int, 0); } @test - public Zero_Incomplete() { + public toInt_zero_incomplete() { const int = ByteVector.fromByteArray(new Uint8Array([0x0, 0x0])).toInt(); assert.strictEqual(int, 0); } @test - public PositiveBigEndian_Complete() { - const int = ByteVector_ToIntTests.PositiveCompleteBV.toInt(); + public toInt_positiveBigEndian_complete() { + const int = this.intPositiveCompleteBV.toInt(); assert.strictEqual(int, 0x01020304); } @test - public PositiveBigEndian_Incomplete() { - const int = ByteVector_ToIntTests.PositiveIncompleteBV.toInt(); + public toInt_positiveBigEndian_incomplete() { + const int = this.intPositiveIncompleteBV.toInt(); assert.strictEqual(int, 0x00000102); } @test - public PositiveLittleEndian_Complete() { - const int = ByteVector_ToIntTests.PositiveCompleteBV.toInt(false); + public toInt_positiveLittleEndian_complete() { + const int = this.intPositiveCompleteBV.toInt(false); assert.strictEqual(int, 0x04030201); } @test - public PositiveLittleEndian_Incomplete() { - const int = ByteVector_ToIntTests.PositiveIncompleteBV.toInt(false); + public toInt_positiveLittleEndian_incomplete() { + const int = this.intPositiveIncompleteBV.toInt(false); assert.strictEqual(int, 0x00000201); } @test - public NegativeBigEndian_Complete() { - const int = ByteVector_ToIntTests.NegativeCompleteBV.toInt(); + public toInt_negativeBigEndian_complete() { + const int = this.intNegativeCompleteBV.toInt(); assert.strictEqual(int, -0x01020304); } @test - public NegativeBigEndian_Incomplete() { - const int = ByteVector_ToIntTests.NegativeIncompleteBV.toInt(); + public toInt_negativeBigEndian_incomplete() { + const int = this.intNegativeIncompleteBV.toInt(); assert.strictEqual(int, 0x0000FCFD); } @test - public NegativeLittleEndian_Complete() { - const int = ByteVector_ToIntTests.NegativeCompleteBV.toInt(false); + public toInt_negativeLittleEndian_complete() { + const int = this.intNegativeCompleteBV.toInt(false); assert.strictEqual(int, -0x03030202); } @test - public NegativeLittleEndian_Incomplete() { - const int = ByteVector_ToIntTests.NegativeIncompleteBV.toInt(false); + public toInt_negativeLittleEndian_incomplete() { + const int = this.intNegativeIncompleteBV.toInt(false); assert.strictEqual(int, 0x0000FDFC); } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_ToLongTests { - private static readonly NegativeCompleteBV = ByteVector.fromByteArray( - new Uint8Array([0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xF8, 0xAA]) - ); - private static readonly NegativeIncompleteBV = ByteVector.fromByteArray( - new Uint8Array([0xFE, 0xFD, 0xFC, 0xFC]) - ); - private static readonly PositiveCompleteBV = ByteVector.fromByteArray( - new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0xAA]) - ); - private static readonly PositiveIncompleteBV = ByteVector.fromByteArray( - new Uint8Array([0x01, 0x02, 0x03, 0x04]) - ); @test - public Empty() { + public toLong_empty() { const long = ByteVector.fromSize(0).toLong(); assert.isTrue(long.equals(BigInt(0))); } @test - public Zero_Complete() { + public toLong_zero_complete() { const long = ByteVector.fromByteArray( new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA]) ).toLong(); @@ -233,7 +251,7 @@ class ByteVector_ToLongTests { } @test - public Zero_Incomplete() { + public toLong_zero_incomplete() { const long = ByteVector.fromByteArray( new Uint8Array([0x00, 0x00, 0x00, 0x00]) ).toLong(); @@ -241,140 +259,121 @@ class ByteVector_ToLongTests { } @test - public PositiveBigEndian_Complete() { - const long = ByteVector_ToLongTests.PositiveCompleteBV.toLong(); + public toLong_positiveBigEndian_complete() { + const long = this.longPositiveCompleteBV.toLong(); assert.isTrue(long.equals(BigInt("0102030405060708", 16))); } @test - public PositiveBigEndian_Incomplete() { - const long = ByteVector_ToLongTests.PositiveIncompleteBV.toLong(); + public toLong_positiveBigEndian_incomplete() { + const long = this.longPositiveIncompleteBV.toLong(); assert.isTrue(long.equals(BigInt("01020304", 16))); } @test - public PositiveLittleEndian_Complete() { - const long = ByteVector_ToLongTests.PositiveCompleteBV.toLong(false); + public toLong_positiveLittleEndian_complete() { + const long = this.longPositiveCompleteBV.toLong(false); assert.isTrue(long.equals(BigInt("0807060504030201", 16))); } @test - public PositiveLittleEndian_Incomplete() { - const long = ByteVector_ToLongTests.PositiveIncompleteBV.toLong(false); + public toLong_positiveLittleEndian_incomplete() { + const long = this.longPositiveIncompleteBV.toLong(false); assert.isTrue(long.equals(BigInt("04030201", 16))); } @test - public NegativeBigEndian_Complete() { - const long = ByteVector_ToLongTests.NegativeCompleteBV.toLong(); + public toLong_negativeBigEndian_complete() { + const long = this.longNegativeCompleteBV.toLong(); assert.isTrue(long.equals(BigInt("-0102030405060708", 16))); } @test - public NegativeBigEndian_Incomplete() { - const long = ByteVector_ToLongTests.NegativeIncompleteBV.toLong(); + public toLong_negativeBigEndian_incomplete() { + const long = this.longNegativeIncompleteBV.toLong(); assert.isTrue(long.equals(BigInt("FEFDFCFC", 16))); } @test - public NegativeLittleEndian_Complete() { - const long = ByteVector_ToLongTests.NegativeCompleteBV.toLong(false); + public toLong_negativeLittleEndian_complete() { + const long = this.longNegativeCompleteBV.toLong(false); assert.isTrue(long.equals(BigInt("-0707060504030202", 16))); } @test - public NegativeLittleEndian_Incomplete() { - const long = ByteVector_ToLongTests.NegativeIncompleteBV.toLong(false); + public toLong_negativeLittleEndian_incomplete() { + const long = this.longNegativeIncompleteBV.toLong(false); assert.isTrue(long.equals(BigInt("FCFCFDFE", 16))); } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_ToShortTests { - private static readonly NegativeCompleteBV = ByteVector.fromByteArray( - new Uint8Array([0xFE, 0xFD, 0xAA]) - ); - private static readonly NegativeIncompleteBV = ByteVector.fromByteArray( - new Uint8Array([0xFC]) - ); - private static readonly PositiveCompleteBV = ByteVector.fromByteArray( - new Uint8Array([0x01, 0x02, 0xAA]) - ); - private static readonly PositiveIncompleteBV = ByteVector.fromByteArray( - new Uint8Array([0x01]) - ); @test - public Empty() { + public toShort_empty() { const int = ByteVector.fromSize(0).toShort(); assert.strictEqual(int, 0); } @test - public Zero_Complete() { + public toShort_zero_complete() { const int = ByteVector.fromByteArray(new Uint8Array([0x0, 0x0, 0x0, 0x0, 0xAA])).toShort(); assert.strictEqual(int, 0); } @test - public Zero_Incomplete() { + public toShort_zero_incomplete() { const int = ByteVector.fromByteArray(new Uint8Array([0x0, 0x0])).toShort(); assert.strictEqual(int, 0); } @test - public PositiveBigEndian_Complete() { - const int = ByteVector_ToShortTests.PositiveCompleteBV.toShort(); + public toShort_positiveBigEndian_complete() { + const int = this.shortPositiveCompleteBV.toShort(); assert.strictEqual(int, 0x0102); } @test - public PositiveBigEndian_Incomplete() { - const int = ByteVector_ToShortTests.PositiveIncompleteBV.toShort(); + public toShort_positiveBigEndian_incomplete() { + const int = this.shortPositiveIncompleteBV.toShort(); assert.strictEqual(int, 0x01); } @test - public PositiveLittleEndian_Complete() { - const int = ByteVector_ToShortTests.PositiveCompleteBV.toShort(false); + public toShort_positiveLittleEndian_complete() { + const int = this.shortPositiveCompleteBV.toShort(false); assert.strictEqual(int, 0x0201); } @test - public PositiveLittleEndian_Incomplete() { - const int = ByteVector_ToShortTests.PositiveIncompleteBV.toShort(false); + public toShort_positiveLittleEndian_incomplete() { + const int = this.shortPositiveIncompleteBV.toShort(false); assert.strictEqual(int, 0x01); } @test - public NegativeBigEndian_Complete() { - const int = ByteVector_ToShortTests.NegativeCompleteBV.toShort(); + public toShort_negativeBigEndian_complete() { + const int = this.shortNegativeCompleteBV.toShort(); assert.strictEqual(int, -0x0103); } @test - public NegativeBigEndian_Incomplete() { - const int = ByteVector_ToShortTests.NegativeIncompleteBV.toShort(); + public toShort_negativeBigEndian_incomplete() { + const int = this.shortNegativeIncompleteBV.toShort(); assert.strictEqual(int, 0x00FC); } @test - public NegativeLittleEndian_Complete() { - const int = ByteVector_ToShortTests.NegativeCompleteBV.toShort(false); + public toShort_negativeLittleEndian_complete() { + const int = this.shortNegativeCompleteBV.toShort(false); assert.strictEqual(int, -0x0202); } @test - public NegativeLittleEndian_Incomplete() { - const int = ByteVector_ToShortTests.NegativeIncompleteBV.toShort(false); + public toShort_negativeLittleEndian_incomplete() { + const int = this.shortNegativeIncompleteBV.toShort(false); assert.strictEqual(int, 0x00FC); } -} -@suite(timeout(3000), slow(1000)) -class ByteVector_ToStringTests { @test - public InvalidOffset() { + public toString_invalidOffset() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromSize(0).toString(0.1); }); assert.throws(() => { ByteVector.fromSize(0).toString(-1); }); @@ -382,7 +381,7 @@ class ByteVector_ToStringTests { } @test - public InvalidCount() { + public toString_invalidCount() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromSize(1).toString(0, undefined, 0.1); }); assert.throws(() => { ByteVector.fromSize(1).toString(0, undefined, -1); }); @@ -390,14 +389,14 @@ class ByteVector_ToStringTests { } @test - public Utf8Full() { + public toString_utf8Full() { const str = ByteVector.fromString(TestConstants.testStrings.UTF8.str) .toString(TestConstants.testStrings.UTF8.bytes.length); assert.strictEqual(str, TestConstants.testStrings.UTF8.str); } @test - public Utf8Partial() { + public toString_utf8Partial() { const str = ByteVector.fromString(TestConstants.testStrings.UTF8.str + TestConstants.testStrings.UTF8.str) .toString( TestConstants.testStrings.UTF8.bytes.length, @@ -408,13 +407,13 @@ class ByteVector_ToStringTests { } @test - public Utf8Empty() { + public toString_utf8Empty() { const str = ByteVector.fromSize(0).toString(0); assert.strictEqual(str, ""); } @test - public Utf16LittleEndianFull() { + public toString_utf16LittleEndianFull() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; const str = ByteVector.fromString(TestConstants.testStrings.UTF16LE.str, StringType.UTF16LE) .toString(TestConstants.testStrings.UTF16LE.bytes.length, StringType.UTF16LE); @@ -423,12 +422,12 @@ class ByteVector_ToStringTests { } @test - public Utf16LittleEndianPartial() { + public toString_utf16LittleEndianPartial() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; const str = ByteVector.fromString( TestConstants.testStrings.UTF16LE.str + TestConstants.testStrings.UTF16LE.str, StringType.UTF16LE - ) + ) .toString( TestConstants.testStrings.UTF16LE.bytes.length, StringType.UTF16LE, @@ -439,13 +438,13 @@ class ByteVector_ToStringTests { } @test - public Utf16LittleEndianEmpty() { + public toString_utf16LittleEndianEmpty() { const str = ByteVector.fromSize(0).toString(0, StringType.UTF16LE); assert.strictEqual(str, ""); } @test - public Utf16BigEndianFull() { + public toString_utf16BigEndianFull() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; const str = ByteVector.fromString(TestConstants.testStrings.UTF16BE.str, StringType.UTF16BE) .toString(TestConstants.testStrings.UTF16BE.bytes.length, StringType.UTF16BE); @@ -454,7 +453,7 @@ class ByteVector_ToStringTests { } @test - public Utf16BigEndianPartial() { + public toString_utf16BigEndianPartial() { const originalLastUtf16Encoding = ByteVector.lastUtf16Encoding; const str = ByteVector.fromString( TestConstants.testStrings.UTF16BE.str + TestConstants.testStrings.UTF16BE.str, @@ -470,24 +469,24 @@ class ByteVector_ToStringTests { } @test - public Utf16BigEndianEmpty() { + public toString_utf16BigEndianEmpty() { const str = ByteVector.fromSize(0).toString(0, StringType.UTF16BE); assert.strictEqual(str, ""); } @test - public Latin1Full() { + public toString_latin1Full() { const str = ByteVector.fromString(TestConstants.testStrings.Latin1.str, StringType.Latin1) .toString(TestConstants.testStrings.Latin1.bytes.length, StringType.Latin1); assert.strictEqual(str, TestConstants.testStrings.Latin1.str); } @test - public Latin1Partial() { + public toString_latin1Partial() { const str = ByteVector.fromString( TestConstants.testStrings.Latin1.str + TestConstants.testStrings.Latin1.str, - StringType.Latin1 - ) + StringType.Latin1 + ) .toString( TestConstants.testStrings.Latin1.bytes.length, StringType.Latin1, @@ -497,66 +496,69 @@ class ByteVector_ToStringTests { } @test - public Latin1Empty() { + public toString_latin1Empty() { const str = ByteVector.fromSize(0).toString(0, StringType.Latin1); assert.strictEqual(str, ""); } @test - public Utf16Full() { + public toString_utf16Full() { // This test will change the last used utf16 encoding, so we'll restore it afterward const originalLastEncoding = ByteVector.lastUtf16Encoding; ByteVector.lastUtf16Encoding = "something bogus"; - const str = ByteVector.fromString(TestConstants.testStrings.UTF16LEWithBOM.str, StringType.UTF16) - .toString(TestConstants.testStrings.UTF16LEWithBOM.bytes.length, StringType.UTF16); - assert.strictEqual(str, TestConstants.testStrings.UTF16LEWithBOM.str); - assert.strictEqual(ByteVector.lastUtf16Encoding, "utf16-le"); - - // Cleanup - ByteVector.lastUtf16Encoding = originalLastEncoding; + try { + const str = ByteVector.fromString(TestConstants.testStrings.UTF16LEWithBOM.str, StringType.UTF16) + .toString(TestConstants.testStrings.UTF16LEWithBOM.bytes.length, StringType.UTF16); + assert.strictEqual(str, TestConstants.testStrings.UTF16LEWithBOM.str); + assert.strictEqual(ByteVector.lastUtf16Encoding, "utf16-le"); + } finally { + // Cleanup + ByteVector.lastUtf16Encoding = originalLastEncoding; + } } @test - public Utf16Partial() { + public toString_utf16Partial() { // This test will change the last used utf16 encoding, so we'll restore it afterward const originalLastEncoding = ByteVector.lastUtf16Encoding; ByteVector.lastUtf16Encoding = "something bogus"; - const str = ByteVector.fromString( + try { + const str = ByteVector.fromString( TestConstants.testStrings.UTF16LEWithBOM.str, StringType.UTF16 ) - .toString( - TestConstants.testStrings.UTF16LEWithBOM.bytes.length, - StringType.UTF16 - ); - assert.strictEqual(str, TestConstants.testStrings.UTF16LEWithBOM.str); - assert.strictEqual(ByteVector.lastUtf16Encoding, "utf16-le"); - - // Cleanup - ByteVector.lastUtf16Encoding = originalLastEncoding; + .toString( + TestConstants.testStrings.UTF16LEWithBOM.bytes.length, + StringType.UTF16 + ); + assert.strictEqual(str, TestConstants.testStrings.UTF16LEWithBOM.str); + assert.strictEqual(ByteVector.lastUtf16Encoding, "utf16-le"); + } finally { + // Cleanup + ByteVector.lastUtf16Encoding = originalLastEncoding; + } } @test - public Utf16Empty() { + public toString_utf16Empty() { // This test will change the last used utf16 encoding, so we'll restore it afterward const originalLastEncoding = ByteVector.lastUtf16Encoding; ByteVector.lastUtf16Encoding = "utf16-le"; - const str = ByteVector.fromSize(0).toString(0, StringType.UTF16); - assert.strictEqual(str, ""); - assert.strictEqual(ByteVector.lastUtf16Encoding, "utf16-le"); - - // Cleanup - ByteVector.lastUtf16Encoding = originalLastEncoding; + try { + const str = ByteVector.fromSize(0).toString(0, StringType.UTF16); + assert.strictEqual(str, ""); + assert.strictEqual(ByteVector.lastUtf16Encoding, "utf16-le"); + } finally { + // Cleanup + ByteVector.lastUtf16Encoding = originalLastEncoding; + } } -} -@suite(timeout(3000), slow(1000)) -class ByteVector_ToStringsTests { @test - public InvalidOffset() { + public toStrings_invalidOffset() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromSize(0).toString(0.1); }); assert.throws(() => { ByteVector.fromSize(0).toString(-1); }); @@ -564,15 +566,15 @@ class ByteVector_ToStringsTests { } @test - public InvalidCount() { + public toStrings_invalidCount() { // Arrange, Act, Assert assert.throws(() => { ByteVector.fromSize(1).toString(0, undefined, 0.1); }); assert.throws(() => { ByteVector.fromSize(1).toString(0, undefined, -1); }); } @test - public Utf8Single() { - ByteVector_ToStringsTests.TestString( + public toStrings_utf8Single() { + this.testStrings( "\0\0" + TestConstants.testStrings.UTF8.str, StringType.UTF8, 2, @@ -582,8 +584,8 @@ class ByteVector_ToStringsTests { } @test - public Utf8Multiple() { - ByteVector_ToStringsTests.TestString( + public toStrings_utf8Multiple() { + this.testStrings( TestConstants.testStrings.UTF8.str + "\0\0" + TestConstants.testStrings.UTF8.str, StringType.UTF8, 0, @@ -593,8 +595,8 @@ class ByteVector_ToStringsTests { } @test - public Utf16LittleEndianSingle() { - ByteVector_ToStringsTests.TestString( + public toStrings_utf16LittleEndianSingle() { + this.testStrings( "\0\0" + TestConstants.testStrings.UTF16LE.str, StringType.UTF16LE, 2, @@ -604,8 +606,8 @@ class ByteVector_ToStringsTests { } @test - public Ut16LittleEndianMultiple() { - ByteVector_ToStringsTests.TestString( + public toStrings_ut16LittleEndianMultiple() { + this.testStrings( TestConstants.testStrings.UTF16LE.str + "\0\0" + TestConstants.testStrings.UTF16LE.str, StringType.UTF16LE, 0, @@ -615,8 +617,8 @@ class ByteVector_ToStringsTests { } @test - public Utf16BigEndianSingle() { - ByteVector_ToStringsTests.TestString( + public toStrings_utf16BigEndianSingle() { + this.testStrings( "\0\0" + TestConstants.testStrings.UTF16BE.str, StringType.UTF16BE, 2, @@ -626,8 +628,8 @@ class ByteVector_ToStringsTests { } @test - public Ut16BigEndianMultiple() { - ByteVector_ToStringsTests.TestString( + public toStrings_ut16BigEndianMultiple() { + this.testStrings( TestConstants.testStrings.UTF16BE.str + "\0\0" + TestConstants.testStrings.UTF16BE.str, StringType.UTF16BE, 0, @@ -637,136 +639,103 @@ class ByteVector_ToStringsTests { } @test - public Utf16Single() { + public toStrings_utf16Single() { // This test will change the last used utf16 encoding, so we'll restore it afterward const originalLastEncoding = ByteVector.lastUtf16Encoding; ByteVector.lastUtf16Encoding = "utf16-le"; - ByteVector_ToStringsTests.TestString( - "\0\0" + TestConstants.testStrings.UTF16LEWithBOM.str, - StringType.UTF16, - 2, - undefined, - ["", "", TestConstants.testStrings.UTF16LEWithBOM.str] - ); - assert.strictEqual(ByteVector.lastUtf16Encoding, "utf16-le"); - - // Cleanup - ByteVector.lastUtf16Encoding = originalLastEncoding; + try { + this.testStrings( + "\0\0" + TestConstants.testStrings.UTF16LEWithBOM.str, + StringType.UTF16, + 2, + undefined, + ["", "", TestConstants.testStrings.UTF16LEWithBOM.str] + ); + assert.strictEqual(ByteVector.lastUtf16Encoding, "utf16-le"); + } finally { + // Cleanup + ByteVector.lastUtf16Encoding = originalLastEncoding; + } } @test - public Utf16Multiple() { + public toStrings_utf16Multiple() { // This test will change the last used utf16 encoding, so we'll restore it afterward const originalLastEncoding = ByteVector.lastUtf16Encoding; ByteVector.lastUtf16Encoding = "utf16-le"; - ByteVector_ToStringsTests.TestString( - TestConstants.testStrings.UTF16LEWithBOM.str + "\0\0" + TestConstants.testStrings.UTF16LEWithBOM.str, - StringType.UTF16, - 0, - undefined, - [TestConstants.testStrings.UTF16LEWithBOM.str, "", TestConstants.testStrings.UTF16LEWithBOM.str] - ); - assert.strictEqual(ByteVector.lastUtf16Encoding, "utf16-le"); - - // Cleanup - ByteVector.lastUtf16Encoding = originalLastEncoding; - } - - private static TestString( - textInput: string, - stringType: StringType, - offset: number, - count: number, - expected: string[] - ) { - // Arrange - const bv = ByteVector.fromString(textInput, stringType); - - // Act - const strs = bv.toStrings(stringType, offset, count); - - // Assert - assert.deepEqual(strs, expected); + try { + this.testStrings( + TestConstants.testStrings.UTF16LEWithBOM.str + "\0\0" + TestConstants.testStrings.UTF16LEWithBOM.str, + StringType.UTF16, + 0, + undefined, + [TestConstants.testStrings.UTF16LEWithBOM.str, "", TestConstants.testStrings.UTF16LEWithBOM.str] + ); + assert.strictEqual(ByteVector.lastUtf16Encoding, "utf16-le"); + } finally { + // Cleanup + ByteVector.lastUtf16Encoding = originalLastEncoding; + } } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_ToUIntTests { - private static readonly PositiveCompleteBV = ByteVector.fromByteArray( - new Uint8Array([0x01, 0x02, 0x03, 0x04, 0xAA]) - ); - private static readonly PositiveIncompleteBV = ByteVector.fromByteArray( - new Uint8Array([0x01, 0x02]) - ); @test - public Empty() { + public toUInt_empty() { const int = ByteVector.fromSize(0).toUInt(); assert.strictEqual(int, 0); } @test - public Zero_Complete() { + public toUInt_zero_complete() { const int = ByteVector.fromByteArray(new Uint8Array([0x0, 0x0, 0x0, 0x0, 0xAA])).toUInt(); assert.strictEqual(int, 0); } @test - public Zero_Incomplete() { + public toUInt_zero_incomplete() { const int = ByteVector.fromByteArray(new Uint8Array([0x0, 0x0])).toUInt(); assert.strictEqual(int, 0); } @test - public PositiveBigEndian_Complete() { - const int = ByteVector_ToUIntTests.PositiveCompleteBV.toUInt(); + public toUInt_positiveBigEndian_complete() { + const int = this.uintPositiveCompleteBV.toUInt(); assert.strictEqual(int, 0x01020304); } @test - public PositiveBigEndian_Incomplete() { - const int = ByteVector_ToUIntTests.PositiveIncompleteBV.toUInt(); + public toUInt_positiveBigEndian_incomplete() { + const int = this.uintPositiveIncompleteBV.toUInt(); assert.strictEqual(int, 0x00000102); } @test - public PositiveLittleEndian_Complete() { - const int = ByteVector_ToUIntTests.PositiveCompleteBV.toUInt(false); + public toUInt_positiveLittleEndian_complete() { + const int = this.uintPositiveCompleteBV.toUInt(false); assert.strictEqual(int, 0x04030201); } @test - public PositiveLittleEndian_Incomplete() { - const int = ByteVector_ToUIntTests.PositiveIncompleteBV.toUInt(false); + public toUInt_positiveLittleEndian_incomplete() { + const int = this.uintPositiveIncompleteBV.toUInt(false); assert.strictEqual(int, 0x00000201); } @test - public UnsignedRange_Complete() { + public toUInt_unsignedRange_complete() { const int = ByteVector.fromByteArray(new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF])).toUInt(); assert.strictEqual(int, 0xFFFFFFFF); } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_ToULongTests { - private static readonly PositiveCompleteBV = ByteVector.fromByteArray( - new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0xAA]) - ); - private static readonly PositiveIncompleteBV = ByteVector.fromByteArray( - new Uint8Array([0x01, 0x02, 0x03, 0x04]) - ); @test - public Empty() { + public toULong_empty() { const long = ByteVector.fromSize(0).toULong(); assert.isTrue(long.equals(BigInt(0))); } @test - public Zero_Complete() { + public toULong_zero_complete() { const long = ByteVector.fromByteArray( new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA]) ).toULong(); @@ -774,7 +743,7 @@ class ByteVector_ToULongTests { } @test - public Zero_Incomplete() { + public toULong_zero_incomplete() { const long = ByteVector.fromByteArray( new Uint8Array([0x00, 0x00, 0x00, 0x00]) ).toULong(); @@ -782,92 +751,99 @@ class ByteVector_ToULongTests { } @test - public PositiveBigEndian_Complete() { - const long = ByteVector_ToULongTests.PositiveCompleteBV.toULong(); + public toULong_positiveBigEndian_complete() { + const long = this.ulongPositiveCompleteBV.toULong(); assert.isTrue(long.equals(BigInt("0102030405060708", 16))); } @test - public PositiveBigEndian_Incomplete() { - const long = ByteVector_ToULongTests.PositiveIncompleteBV.toULong(); + public toULong_positiveBigEndian_incomplete() { + const long = this.ulongPositiveIncompleteBV.toULong(); assert.isTrue(long.equals(BigInt("01020304", 16))); } @test - public PositiveLittleEndian_Complete() { - const long = ByteVector_ToULongTests.PositiveCompleteBV.toULong(false); + public toULong_positiveLittleEndian_complete() { + const long = this.ulongPositiveCompleteBV.toULong(false); assert.isTrue(long.equals(BigInt("0807060504030201", 16))); } @test - public PositiveLittleEndian_Incomplete() { - const long = ByteVector_ToULongTests.PositiveIncompleteBV.toULong(false); + public toULong_positiveLittleEndian_incomplete() { + const long = this.ulongPositiveIncompleteBV.toULong(false); assert.isTrue(long.equals(BigInt("04030201", 16))); } @test - public UnsignedRange_Complete() { + public toULong_unsignedRange_complete() { const long = ByteVector.fromByteArray( new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) ).toULong(); assert.isTrue(long.equals(BigInt("FFFFFFFFFFFFFFFF", 16))); } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_ToUShortTests { - private static readonly PositiveCompleteBV = ByteVector.fromByteArray( - new Uint8Array([0x01, 0x02, 0xAA]) - ); - private static readonly PositiveIncompleteBV = ByteVector.fromByteArray( - new Uint8Array([0x01]) - ); @test - public Empty() { + public toUSong_empty() { const int = ByteVector.fromSize(0).toUShort(); assert.strictEqual(int, 0); } @test - public Zero_Complete() { + public toUSong_zero_complete() { const int = ByteVector.fromByteArray(new Uint8Array([0x0, 0x0, 0x0, 0x0, 0xAA])).toUShort(); assert.strictEqual(int, 0); } @test - public Zero_Incomplete() { + public toUSong_zero_incomplete() { const int = ByteVector.fromByteArray(new Uint8Array([0x0, 0x0])).toUShort(); assert.strictEqual(int, 0); } @test - public PositiveBigEndian_Complete() { - const int = ByteVector_ToUShortTests.PositiveCompleteBV.toUShort(); + public toUSong_positiveBigEndian_complete() { + const int = this.ushortPositiveCompleteBV.toUShort(); assert.strictEqual(int, 0x0102); } @test - public PositiveBigEndian_Incomplete() { - const int = ByteVector_ToUShortTests.PositiveIncompleteBV.toUShort(); + public toUSong_positiveBigEndian_incomplete() { + const int = this.ushortPositiveIncompleteBV.toUShort(); assert.strictEqual(int, 0x01); } @test - public PositiveLittleEndian_Complete() { - const int = ByteVector_ToUShortTests.PositiveCompleteBV.toUShort(false); + public toUSong_positiveLittleEndian_complete() { + const int = this.ushortPositiveCompleteBV.toUShort(false); assert.strictEqual(int, 0x0201); } @test - public PositiveLittleEndian_Incomplete() { - const int = ByteVector_ToUShortTests.PositiveIncompleteBV.toUShort(false); + public toUSong_positiveLittleEndian_incomplete() { + const int = this.ushortPositiveIncompleteBV.toUShort(false); assert.strictEqual(int, 0x01); } @test - public UnsignedRange_Complete() { + public toUSong_unsignedRange_complete() { const short = ByteVector.fromByteArray(new Uint8Array([0xFF, 0xFF])).toUShort(); assert.strictEqual(short, 0xFFFF); } + + private testStrings( + textInput: string, + stringType: StringType, + offset: number, + count: number, + expected: string[] + ) { + // Arrange + const bv = ByteVector.fromString(textInput, stringType); + + // Act + const strs = bv.toStrings(stringType, offset, count); + + // Assert + assert.deepEqual(strs, expected); + } } diff --git a/test/byteVectorStaticOperationTests.ts b/test/byteVectorStaticMethodTests.ts similarity index 90% rename from test/byteVectorStaticOperationTests.ts rename to test/byteVectorStaticMethodTests.ts index 28671434..743a6a4d 100644 --- a/test/byteVectorStaticOperationTests.ts +++ b/test/byteVectorStaticMethodTests.ts @@ -9,9 +9,9 @@ Chai.use(ChaiAsPromised); const assert = Chai.assert; @suite(timeout(3000), slow(1000)) -class ByteVector_AddTests { +class ByteVector_StaticMethodTests { @test - public InvalidParameters() { + public add_invalidParameters() { // Arrange const bv = ByteVector.fromSize(1, 0x00); @@ -25,7 +25,7 @@ class ByteVector_AddTests { } @test - public AddEmptyAndEmpty() { + public add_addEmptyAndEmpty() { // Arrange const bv = ByteVector.fromSize(0); @@ -42,7 +42,7 @@ class ByteVector_AddTests { } @test - public AddSomethingAndEmpty() { + public add_addSomethingAndEmpty() { // Arrange const something = ByteVector.fromSize(1, 0x00); const empty = ByteVector.fromSize(0); @@ -61,7 +61,7 @@ class ByteVector_AddTests { } @test - public AddEmptyAndSomething() { + public add_addEmptyAndSomething() { // Arrange const something = ByteVector.fromSize(1, 0x00); const empty = ByteVector.fromSize(0); @@ -80,7 +80,7 @@ class ByteVector_AddTests { } @test - public AddSomethingAndSomething() { + public equal_add_addSomethingAndSomething() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x03, 0x04])); @@ -102,26 +102,23 @@ class ByteVector_AddTests { assert.strictEqual(result2.length, 4); assert.deepEqual(result2.data, new Uint8Array([0x03, 0x04, 0x01, 0x02])); } -} -@suite(timeout(3000), slow(1000)) -class ByteVector_EqualTests { @test - public BothNullUndefined() { + public equal_bothNullUndefined() { // Arrange, Act, Assert assert.isTrue(ByteVector.equal(undefined, undefined)); assert.isTrue(ByteVector.equal(null, null)); } @test - public MixedNullUndefined() { + public equal_mixedNullUndefined() { // Arrange, Act, Assert assert.isFalse(ByteVector.equal(undefined, null)); assert.isFalse(ByteVector.equal(null, undefined)); } @test - public MixedFalsySomething() { + public equal_mixedFalsySomething() { // Arrange const bv = ByteVector.fromSize(1); @@ -133,7 +130,7 @@ class ByteVector_EqualTests { } @test - public Equal() { + public equal_equal() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); @@ -143,7 +140,7 @@ class ByteVector_EqualTests { } @test - public NotEqual() { + public equal_notEqual() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x03, 0x04])); @@ -151,26 +148,23 @@ class ByteVector_EqualTests { // Act, Assert assert.isFalse(ByteVector.equal(bv1, bv2)); } -} -@suite(timeout(3000), slow(1000)) -class ByteVector_NotEqualTests { @test - public BothNullUndefined() { + public notEqual_bothNullUndefined() { // Arrange, Act, Assert assert.isFalse(ByteVector.notEqual(undefined, undefined)); assert.isFalse(ByteVector.notEqual(null, null)); } @test - public MixedNullUndefined() { + public notEqual_mixedNullUndefined() { // Arrange, Act, Assert assert.isTrue(ByteVector.notEqual(undefined, null)); assert.isTrue(ByteVector.notEqual(null, undefined)); } @test - public MixedFalsySomething() { + public notEqual_mixedFalsySomething() { // Arrange const bv = ByteVector.fromSize(1); @@ -182,7 +176,7 @@ class ByteVector_NotEqualTests { } @test - public Equal() { + public notEqual_equal() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); @@ -192,7 +186,7 @@ class ByteVector_NotEqualTests { } @test - public NotEqual() { + public notEqual_notEqual() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x03, 0x04])); @@ -200,12 +194,9 @@ class ByteVector_NotEqualTests { // Act, Assert assert.isTrue(ByteVector.notEqual(bv1, bv2)); } -} -@suite(timeout(3000), slow(1000)) -class ByteVector_GreaterThanTests { @test - public InvalidParameters() { + public greaterThan_invalidParameters() { // Arrange const bv = ByteVector.fromSize(0); @@ -219,7 +210,7 @@ class ByteVector_GreaterThanTests { } @test - public GreaterThan() { + public greaterThan_greaterThan() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x03])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); @@ -230,7 +221,7 @@ class ByteVector_GreaterThanTests { } @test - public LessThan() { + public greaterThan_lessThan() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x00])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); @@ -241,7 +232,7 @@ class ByteVector_GreaterThanTests { } @test - public Equal() { + public greaterThan_equal() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); @@ -250,12 +241,9 @@ class ByteVector_GreaterThanTests { assert.isFalse(ByteVector.greaterThan(bv1, bv2)); // bv1 > bv2 -> false assert.isFalse(ByteVector.greaterThan(bv2, bv1)); // bv2 > bv1 -> false } -} -@suite(timeout(3000), slow(1000)) -class ByteVector_GreaterThanEqualTests { @test - public InvalidParameters() { + public greaterThanEqual_invalidParameters() { // Arrange const bv = ByteVector.fromSize(0); @@ -269,7 +257,7 @@ class ByteVector_GreaterThanEqualTests { } @test - public GreaterThan() { + public greaterThanEqual_greaterThan() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x03])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); @@ -280,7 +268,7 @@ class ByteVector_GreaterThanEqualTests { } @test - public LessThan() { + public greaterThanEqual_lessThan() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x00])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); @@ -291,7 +279,7 @@ class ByteVector_GreaterThanEqualTests { } @test - public Equal() { + public greaterThanEqual_equal() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); @@ -300,12 +288,9 @@ class ByteVector_GreaterThanEqualTests { assert.isTrue(ByteVector.greaterThanEqual(bv1, bv2)); // bv1 > bv2 -> true assert.isTrue(ByteVector.greaterThanEqual(bv2, bv1)); // bv2 > bv1 -> true } -} -@suite(timeout(3000), slow(1000)) -class ByteVector_LessThanTests { @test - public InvalidParameters() { + public lessThan_invalidParameters() { // Arrange const bv = ByteVector.fromSize(0); @@ -319,7 +304,7 @@ class ByteVector_LessThanTests { } @test - public GreaterThan() { + public lessThan_greaterThan() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x03])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); @@ -330,7 +315,7 @@ class ByteVector_LessThanTests { } @test - public LessThan() { + public lessThan_lessThan() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x00])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); @@ -341,7 +326,7 @@ class ByteVector_LessThanTests { } @test - public Equal() { + public lessThan_equal() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); @@ -350,12 +335,9 @@ class ByteVector_LessThanTests { assert.isFalse(ByteVector.lessThan(bv1, bv2)); // bv1 < bv2 -> false assert.isFalse(ByteVector.lessThan(bv2, bv1)); // bv2 < bv1 -> false } -} -@suite(timeout(3000), slow(1000)) -class ByteVector_LessThanEqualTests { @test - public InvalidParameters() { + public lessThanEqual_invalidParameters() { // Arrange const bv = ByteVector.fromSize(0); @@ -369,7 +351,7 @@ class ByteVector_LessThanEqualTests { } @test - public GreaterThan() { + public lessThanEqual_greaterThan() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x03])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); @@ -380,7 +362,7 @@ class ByteVector_LessThanEqualTests { } @test - public LessThan() { + public lessThanEqual_lessThan() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x00])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); @@ -391,7 +373,7 @@ class ByteVector_LessThanEqualTests { } @test - public Equal() { + public lessThanEqual_equal() { // Arrange const bv1 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); const bv2 = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); diff --git a/test/byteVectorVoidMethodTests.ts b/test/byteVectorVoidMethodTests.ts new file mode 100644 index 00000000..4e55b10c --- /dev/null +++ b/test/byteVectorVoidMethodTests.ts @@ -0,0 +1,1681 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import {ByteVector} from "../src/byteVector"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(timeout(3000), slow(1000)) +class ByteVector_VoidMethodTests { + private readonly vectorToAdd: ByteVector = ByteVector.fromByteArray( + new Uint8Array([0xAA, 0xBB]) + ); + + @test + public iterator() { + // Arrange + const array = [0x01, 0x02, 0x03, 0x04]; + const bv = ByteVector.fromByteArray(new Uint8Array(array)); + + // Act + const output = []; + for (const b of bv) { + output.push(b); + } + + // Assert + assert.deepStrictEqual(output, array); + } + + @test + public addByte_ReadOnly() { + // Arrange - Create readonly ByteVector + const bv = ByteVector.fromSize(1, 0x0, true); + + // Act, Assert - AddByte should fail, ByteVector should be unchanged + assert.throws(() => { bv.addByte(0x01); }); + assert.isTrue(bv.isReadOnly); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public addByte_InvalidByte() { + // Arrange - Create ByteVector + const bv = ByteVector.fromSize(1); + + // Act, Assert - AddByte should fail, ByteVector should be unchanged + assert.throws(() => { bv.addByte(0.1); }); + assert.throws(() => { bv.addByte(-1); }); + assert.throws(() => { bv.addByte(0x1FF); }); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public addByte_AddToEmpty() { + // Arrange - Create ByteVector that is empty + const bv = ByteVector.fromSize(0); + + // Act - Add a byte to it + bv.addByte(0x01); + + // Assert - ByteVector should contain the new byte + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x01])); + } + + @test + public addByte_AddToExisting() { + // Arrange - Create ByteVector with something in it + const bv = ByteVector.fromSize(1); + + // Act - Add a byte to it + bv.addByte(0x01); + + // Assert - ByteVector should contain the new byte + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 2); + assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01])); + } + + @test + public addByteArray_ReadOnly() { + // Arrange - Create readonly ByteVector + const bv = ByteVector.fromSize(1, 0x0, true); + + // Act, Assert - AddByte should fail, ByteVector should be unchanged + assert.throws(() => { bv.addByteArray(new Uint8Array(0x01)); }); + assert.isTrue(bv.isReadOnly); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public addByteArray_InvalidByteArray() { + // Arrange + const bv = ByteVector.fromSize(1, 0x0, true); + + // Act, Assert + assert.throws(() => { bv.addByteArray(null); }); + assert.throws(() => { bv.addByteArray(undefined); }); + } + + @test + public addByteArray_AddEmptyToEmpty() { + // Arrange - Create ByteVector that is empty + const bv = ByteVector.fromSize(0); + + // Act - Add nothing to it + bv.addByteArray(new Uint8Array()); + + // Assert - ByteVector should contain the new byte + assert.isTrue(bv.isEmpty); + assert.strictEqual(bv.length, 0); + assert.deepEqual(bv.data, new Uint8Array()); + } + + @test + public addByteArray_AddSingleToEmpty() { + // Arrange - Create ByteVector that is empty + const bv = ByteVector.fromSize(0); + + // Act - Add a byte to it + bv.addByteArray(new Uint8Array([0x01])); + + // Assert - ByteVector should contain the new byte + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x01])); + } + + @test + public addByteArray_AddMultipleToEmpty() { + // Arrange - Create ByteVector that is empty + const bv = ByteVector.fromSize(0); + + // Act - Add two bytes to it + bv.addByteArray(new Uint8Array([0x01, 0x02])); + + // Assert - ByteVector should contain the new bytes + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 2); + assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02])); + } + + @test + public addByteArray_AddEmptyToExisting() { + // Arrange - Create ByteVector that has bytes + const bv = ByteVector.fromSize(1); + + // Act - Add nothing to it + bv.addByteArray(new Uint8Array()); + + // Assert - ByteVector should contain the new byte + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public addByteArray_AddSingleToExisting() { + // Arrange - Create ByteVector that has bytes + const bv = ByteVector.fromSize(1); + + // Act - Add a byte to it + bv.addByteArray(new Uint8Array([0x01])); + + // Assert - ByteVector should contain the new byte + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 2); + assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01])); + } + + @test + public addByteArray_AddMultipleToExisting() { + // Arrange - Create ByteVector that is empty + const bv = ByteVector.fromSize(1); + + // Act - Add two bytes to it + bv.addByteArray(new Uint8Array([0x01, 0x02])); + + // Assert - ByteVector should contain the new bytes + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 3); + assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02])); + } + + @test + public addByteVector_ReadOnly() { + // Arrange - Create readonly ByteVector + const bv = ByteVector.fromSize(1, 0x0, true); + const add = ByteVector.fromSize(1, 0x1); + + // Act, Assert - AddByte should fail, ByteVector should be unchanged + assert.throws(() => { bv.addByteVector(add); }); + assert.isTrue(bv.isReadOnly); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public addByteVector_InvalidByteVector() { + // Arrange + const bv = ByteVector.fromSize(1, 0x0, true); + + // Act, Assert + assert.throws(() => { bv.addByteVector(null); }); + assert.throws(() => { bv.addByteVector(undefined); }); + } + + @test + public addByteVector_AddEmptyToEmpty() { + // Arrange - Create ByteVector that is empty + const bv = ByteVector.fromSize(0); + + // Act - Add nothing to it + bv.addByteVector(ByteVector.fromSize(0)); + + // Assert - ByteVector should contain the new byte + assert.isTrue(bv.isEmpty); + assert.strictEqual(bv.length, 0); + assert.deepEqual(bv.data, new Uint8Array()); + } + + @test + public addByteVector_AddSingleToEmpty() { + // Arrange - Create ByteVector that is empty + const bv = ByteVector.fromSize(0); + const add = ByteVector.fromSize(1, 0x1); + + // Act - Add a byte to it + bv.addByteVector(add); + + // Assert - ByteVector should contain the new byte + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x01])); + } + + @test + public addByteVector_AddMultipleToEmpty() { + // Arrange - Create ByteVector that is empty + const bv = ByteVector.fromSize(0); + const add = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); + + // Act - Add two bytes to it + bv.addByteVector(add); + + // Assert - ByteVector should contain the new bytes + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 2); + assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02])); + } + + @test + public addByteVector_AddSingleToExisting() { + // Arrange - Create ByteVector that has bytes + const bv = ByteVector.fromSize(1); + const add = ByteVector.fromByteArray(new Uint8Array([0x01])); + + // Act - Add a byte to it + bv.addByteVector(add); + + // Assert - ByteVector should contain the new byte + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 2); + assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01])); + } + + @test + public addByteVector_AddMultipleToExisting() { + // Arrange - Create ByteVector that is empty + const bv = ByteVector.fromSize(1); + const add = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); + + // Act - Add two bytes to it + bv.addByteVector(add); + + // Assert - ByteVector should contain the new bytes + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 3); + assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02])); + } + + @test + public clear_ReadOnly() { + // Arrange - Create readonly bytevector + const bv = ByteVector.fromSize(1, 0x00, true); + + // Act, Assert - Should throw, bytevector should be unchanged + assert.throws(() => { bv.clear(); } ); + assert.isTrue(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public clear_Empty() { + // Arrange - Create empty ByteVector + const bv = ByteVector.fromSize(0); + + // Act + bv.clear(); + + // Assert + assert.isTrue(bv.isEmpty); + assert.strictEqual(bv.length, 0); + assert.deepEqual(bv.data, new Uint8Array()); + } + + @test + public clear_Existing() { + // Arrange - Create ByteVector with some data + const bv = ByteVector.fromSize(1, 0x00); + + // Act + bv.clear(); + + // Assert + assert.isTrue(bv.isEmpty); + assert.strictEqual(bv.length, 0); + assert.deepEqual(bv.data, new Uint8Array()); + } + + @test + public containsAt_invalidParameters() { + // Arrange + const bv = ByteVector.empty(); + const pattern = ByteVector.empty(); + + // Act / Assert + assert.throws(() => { bv.containsAt(null, 0, 0, 0); }); + assert.throws(() => { bv.containsAt(undefined, 0, 0, 0); }); + assert.throws(() => { bv.containsAt(pattern, 1.23, 0, 0); }); + assert.throws(() => { bv.containsAt(pattern, Number.MAX_SAFE_INTEGER + 1, 0, 0); }); + assert.throws(() => { bv.containsAt(pattern, 0, 1.23, 0); }); + assert.throws(() => { bv.containsAt(pattern, 0, Number.MAX_SAFE_INTEGER + 1, 0); }); + assert.throws(() => { bv.containsAt(pattern, 0, 0, 1.23); }); + assert.throws(() => { bv.containsAt(pattern, 0, 0, Number.MAX_SAFE_INTEGER + 1); }); + } + + @test + public containsAt_sanityCheckFailures() { + // Arrange + const bv = ByteVector.fromSize(5); + + // Act / Assert + assert.isFalse(bv.containsAt(ByteVector.fromSize(10))); // Pattern longer than source + assert.isFalse(bv.containsAt(ByteVector.fromSize(1), 10)); // Offset longer than source + assert.isFalse(bv.containsAt(ByteVector.fromSize(1), 5)); // Offset equal to source + assert.isFalse(bv.containsAt(ByteVector.fromSize(1), 0, 5)); // Pattern offset greater than pattern + assert.isFalse(bv.containsAt(ByteVector.fromSize(1), 0, 1)); // Pattern offset equal to pattern + assert.isFalse(bv.containsAt(ByteVector.empty())); // Pattern is empty + assert.isFalse(bv.containsAt(ByteVector.fromSize(1), 0, 0, 0)); // Pattern length is 0 + assert.isFalse(bv.containsAt(ByteVector.fromSize(1), 0, 0, -1)); // Pattern length is negative + assert.isFalse(bv.containsAt(ByteVector.fromSize(1), -1)); // Offset is negative + } + + @test + public containsAt_doesNotContainAnywhere() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("qux"); + + // Act + const output = bv.containsAt(pattern); + + // Assert + assert.isFalse(output); + } + + @test + public containsAt_doesNotContainAt() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("qux"); + + // Act + const output = bv.containsAt(pattern); + + // Assert + assert.isFalse(output); + } + + @test + public containsAt_containsPartial() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("foobux"); + + // Act + const output = bv.containsAt(pattern); + + // Assert + assert.isFalse(output); + } + + @test + public containsAt_containsAll() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("foo"); + + // Act + const output = bv.containsAt(pattern); + + // Assert + assert.isTrue(output); + } + + @test + public containsAt_containsAllWithOffset() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("0bar0"); + + // Act + const output = bv.containsAt(pattern, 3, 1, 3); + + // Assert + assert.isTrue(output); + } + + @test + public compareTo_invalidParameter() { + // Arrange + const bv = ByteVector.fromSize(1); + + // Act / Assert + assert.throws(() => { bv.compareTo(null); }); + assert.throws(() => { bv.compareTo(undefined); }); + } + + @test + public compareTo_lessThan() { + // Arrange + const bv = ByteVector.concatenate(0x00, 0x05); + const other = ByteVector.concatenate(0x00, 0x06); + + // Act + const output = bv.compareTo(other); + + // Assert + assert.strictEqual(output, -1); + } + + @test + public compareTo_greaterThan() { + // Arrange + const bv = ByteVector.concatenate(0x00, 0x05); + const other = ByteVector.concatenate(0x00, 0x04); + + // Act + const output = bv.compareTo(other); + + // Assert + assert.strictEqual(output, 1); + } + + @test + public compareTo_equalTo() { + // Arrange + const bv = ByteVector.concatenate(0x00, 0x05); + const other = ByteVector.concatenate(0x00, 0x05); + + // Act + const output = bv.compareTo(other); + + // Assert + assert.strictEqual(output, 0); + } + + @test + public compareTo_inequalSizes() { + // Arrange + const bv = ByteVector.concatenate(0x00, 0x05); + const other = ByteVector.concatenate(0x00); + + // Act + const output = bv.compareTo(other); + + // Assert + assert.strictEqual(output, 1); + } + + @test + public endsWith_invalidParam() { + // Arrange + const bv = ByteVector.empty(); + + // Act / Assert + assert.throws(() => { bv.endsWith(undefined); }); + assert.throws(() => { bv.endsWith(null); }); + } + + @test + public endsWith_doesEndsWith() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("baz"); + + // Act / Assert + assert.isTrue(bv.endsWith(pattern)); + } + + @test + public endsWith_doesNotEndWith() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("bux"); + + // Act / Assert + assert.isFalse(bv.endsWith(pattern)); + } + + @test + public endsWithPartialMatch_invalidProperty() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + + // Act / Assert + assert.throws(() => { bv.endsWithPartialMatch(null); }); + assert.throws(() => { bv.endsWithPartialMatch(undefined); }); + } + + @test + public endsWithPartialMatch_patternTooLong() { + // Arrange + const bv = ByteVector.fromSize(5); + const pattern = ByteVector.fromSize(10); + + // Act / Assert + assert.strictEqual(bv.endsWithPartialMatch(pattern), -1); + } + + @test + public endsWithPartialMatch_doesNotEndWith() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("qux"); + + // Act / Assert + assert.strictEqual(bv.endsWithPartialMatch(pattern), -1); + } + + @test + public endsWithPartialMatch_endsWithPartial() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("bazqqq"); + + // Act / Assert + assert.strictEqual(bv.endsWithPartialMatch(pattern), 6); + } + + @test + public endsWithPartialMatch_endsWithFull() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("bazq"); + + // Act / Assert + assert.strictEqual(bv.endsWithPartialMatch(pattern), 6); + } + + @test + public find_invalidParameters() { + // Arrange + const bv = ByteVector.empty(); + const pattern = ByteVector.empty(); + + // Act / Assert + assert.throws(() => { bv.find(undefined, 0, 1); }); + assert.throws(() => { bv.find(null, 0, 1); }); + assert.throws(() => { bv.find(pattern, -1, 1); }); + assert.throws(() => { bv.find(pattern, 1.23, 1); }); + assert.throws(() => { bv.find(pattern, Number.MAX_SAFE_INTEGER + 1, 1); }); + assert.throws(() => { bv.find(pattern, 0, -1); }); + assert.throws(() => { bv.find(pattern, 0, 0); }); + assert.throws(() => { bv.find(pattern, 0, 1.23); }); + assert.throws(() => { bv.find(pattern, 0, Number.MAX_SAFE_INTEGER + 1); }); + } + + @test + public find_patternTooLong() { + // Arrange + const bv = ByteVector.fromSize(5); + const pattern = ByteVector.fromSize(10); + + // Act / Assert + assert.strictEqual(bv.find(pattern), -1); + } + + @test + public find_singleBytePattern_noMatch() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromByteArray(new Uint8Array(0x05)); + + // Act / Assert + assert.strictEqual(bv.find(pattern), -1); + } + + @test + public find_singleBytePattern_withMatch() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("b"); + + // Act / Assert + assert.strictEqual(bv.find(pattern), 3); + } + + @test + public find_multiBytePattern_noMatch() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("qux"); + + // Act / Assert + assert.strictEqual(bv.find(pattern), -1); + } + + @test + public find_multiBytePattern_singleMatch() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("bar"); + + // Act / Assert + assert.strictEqual(bv.find(pattern), 3); + } + + @test + public find_multiBytePattern_multiMatch() { + // Arrange + const bv = ByteVector.fromString("foofoofoo"); + const pattern = ByteVector.fromString("foo"); + + // Act / Assert + assert.strictEqual(bv.find(pattern), 0); + } + + @test + public find_multiByteWithOffset_noMatch() { + // Arrange + const bv = ByteVector.fromString("fooabc"); + const pattern = ByteVector.fromString("foo"); + + // Act / Assert + assert.strictEqual(bv.find(pattern, 1), -1); + } + + @test + public find_multiByteWithOffset_withMatch() { + // Arrange + const bv = ByteVector.fromString("foofoofoo"); + const pattern = ByteVector.fromString("foo"); + + // Act / Assert + assert.strictEqual(bv.find(pattern, 1), 3); + } + + @test + public find_multiByteWithAlign_noMatch() { + // Arrange + const bv = ByteVector.fromString("0abc"); + const pattern = ByteVector.fromString("ab"); + + // Act / Assert + assert.strictEqual(bv.find(pattern, 0, 2), -1); + } + + @test + public find_multiByteWithAlign_withMatch() { + // Arrange + const bv = ByteVector.fromString("00abc"); + const pattern = ByteVector.fromString("ab"); + + // Act / Assert + assert.strictEqual(bv.find(pattern, 0, 2), 2); + } + + @test + public get_invalidParameter() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + + // Act / Assert + assert.throws(() => bv.get(-1)); + assert.throws(() => bv.get(1.23)); + assert.throws(() => bv.get(Number.MAX_SAFE_INTEGER + 1)); + assert.throws(() => bv.get(bv.length)); + } + + @test + public get_validParameter() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + + // Act + const output = bv.get(5); + + // Assert + assert.strictEqual(output, "r".codePointAt(0)); + } + + @test + public indexOf_exists() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([1, 2, 3])); + + // Act + const output = bv.indexOf(2); + + // Assert + assert.strictEqual(output, 1); + } + + @test + public indexOf_doesNotExist() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([1, 2, 3])); + + // Act + const output = bv.indexOf(888); + + // Assert + assert.strictEqual(output, -1); + } + + @test + public insertByte_ReadOnly() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00, true); + + // Act, Assert + assert.throws(() => { bv.insertByte(0, 0x00); }); + + assert.isTrue(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public insertByte_IndexOutOfRange() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00); + + // Act, Assert + assert.throws(() => { bv.insertByte(0.1, 0x01); }); + assert.throws(() => { bv.insertByte(-1, 0x01); }); + assert.throws(() => { bv.insertByte(2, 0x01); }); + + assert.isFalse(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public insertByte_ByteOutOfRange() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00); + + // Act, Assert + assert.throws(() => { bv.insertByte(0, 0.1); }); + assert.throws(() => { bv.insertByte(0, -1); }); + assert.throws(() => { bv.insertByte(0, 0x1FF); }); + + assert.isFalse(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public insertByte_InsertIntoEmpty() { + // Arrange + const bv = ByteVector.fromSize(0); + + // Act + bv.insertByte(0, 0x01); + + // Assert + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x01])); + } + + @test + public insertByte_InsertAtFront() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); + + // Act + bv.insertByte(0, 0xAA); + + // Assert + assert.strictEqual(bv.length, 5); + assert.deepEqual(bv.data, new Uint8Array([0xAA, 0x01, 0x02, 0x03, 0x04])); + } + + @test + public insertByte_InsertAtBack() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); + + // Act + bv.insertByte(bv.length, 0xAA); + + // Assert + assert.strictEqual(bv.length, 5); + assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02, 0x03, 0x04, 0xAA])); + } + + @test + public insertByte_InsertInMiddle() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); + + // Act + bv.insertByte(2, 0xAA); + + // Assert + assert.strictEqual(bv.length, 5); + assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02, 0xAA, 0x03, 0x04])); + } + + @test + public insertByteArray_ReadOnly() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00, true); + + // Act, Assert + assert.throws(() => { bv.insertByteArray(0, new Uint8Array()); }); + + assert.isTrue(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public insertByteArray_IndexOutOfRange() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00); + + // Act, Assert + assert.throws(() => { bv.insertByteArray(0.1, new Uint8Array()); }); + assert.throws(() => { bv.insertByteArray(-1, new Uint8Array()); }); + assert.throws(() => { bv.insertByteArray(2, new Uint8Array()); }); + + assert.isFalse(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public insertByteArray_ByteArrayInvalid() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00); + + // Act, Assert + assert.throws(() => { bv.insertByteArray(0, null); }); + assert.throws(() => { bv.insertByteArray(0, undefined); }); + + assert.isFalse(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public insertByteArray_InsertEmpty() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00); + + // Act + bv.insertByteArray(0, new Uint8Array()); + + // Assert + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public insertByteArray_InsertIntoEmpty() { + // Arrange + const bv = ByteVector.fromSize(0); + + // Act + bv.insertByteArray(0, new Uint8Array([0x01, 0x02])); + + // Assert + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 2); + assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02])); + } + + @test + public insertByteArray_InsertAtFront() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); + + // Act + bv.insertByteArray(0, new Uint8Array([0xAA, 0xBB])); + + // Assert + assert.strictEqual(bv.length, 6); + assert.deepEqual(bv.data, new Uint8Array([0xAA, 0xBB, 0x01, 0x02, 0x03, 0x04])); + } + + @test + public insertByteArray_InsertAtBack() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); + + // Act + bv.insertByteArray(bv.length, new Uint8Array([0xAA, 0xBB])); + + // Assert + assert.strictEqual(bv.length, 6); + assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02, 0x03, 0x04, 0xAA, 0xBB])); + } + + @test + public insertByteArray_InsertInMiddle() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); + + // Act + bv.insertByteArray(2, new Uint8Array([0xAA, 0xBB])); + + // Assert + assert.strictEqual(bv.length, 6); + assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02, 0xAA, 0xBB, 0x03, 0x04])); + } + + @test + public insertByteVector_ReadOnly() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00, true); + + // Act, Assert + assert.throws(() => { bv.insertByteVector(0, this.vectorToAdd); }); + + assert.isTrue(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public insertByteVector_IndexOutOfRange() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00); + + // Act, Assert + assert.throws(() => { bv.insertByteVector(0.1, this.vectorToAdd); }); + assert.throws(() => { bv.insertByteVector(-1, this.vectorToAdd); }); + assert.throws(() => { bv.insertByteVector(2, this.vectorToAdd); }); + + assert.isFalse(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public insertByteVector_ByteArrayInvalid() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00); + + // Act, Assert + assert.throws(() => { bv.insertByteVector(0, null); }); + assert.throws(() => { bv.insertByteVector(0, undefined); }); + + assert.isFalse(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public insertByteVector_InsertEmpty() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00); + + // Act + bv.insertByteVector(0, ByteVector.fromSize(0)); + + // Assert + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public insertByteVector_InsertIntoEmpty() { + // Arrange + const bv = ByteVector.fromSize(0); + + // Act + bv.insertByteVector(0, this.vectorToAdd); + + // Assert + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 2); + assert.deepEqual(bv.data, new Uint8Array([0xAA, 0xBB])); + } + + @test + public insertByteVector_InsertAtFront() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); + + // Act + bv.insertByteVector(0, this.vectorToAdd); + + // Assert + assert.strictEqual(bv.length, 6); + assert.deepEqual(bv.data, new Uint8Array([0xAA, 0xBB, 0x01, 0x02, 0x03, 0x04])); + } + + @test + public insertByteVector_InsertAtBack() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); + + // Act + bv.insertByteVector(bv.length, this.vectorToAdd); + + // Assert + assert.strictEqual(bv.length, 6); + assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02, 0x03, 0x04, 0xAA, 0xBB])); + } + + @test + public insertByteVector_InsertInMiddle() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); + + // Act + bv.insertByteVector(2, this.vectorToAdd); + + // Assert + assert.strictEqual(bv.length, 6); + assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02, 0xAA, 0xBB, 0x03, 0x04])); + } + + @test + public mid_invalidParameters() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + + // Act / Assert + assert.throws(() => bv.mid(-1)); + assert.throws(() => bv.mid(1.23)); + assert.throws(() => bv.mid(Number.MAX_SAFE_INTEGER + 1)); + assert.throws(() => bv.mid(1, -1)); + assert.throws(() => bv.mid(1, 1.23)); + assert.throws(() => bv.mid(1, Number.MAX_SAFE_INTEGER + 1)); + assert.throws(() => bv.mid(1, bv.length)); + } + + @test + public mid_entireVector() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + + // Act + const output = bv.mid(0); + + // Assert + assert.notStrictEqual(output, bv); + assert.isTrue(ByteVector.equal(output, bv)); + } + + @test + public mid_partialVector() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + + // Act + const output = bv.mid(3, 3); + + // Assert + assert.isTrue(ByteVector.equal(output, ByteVector.fromString("bar"))); + } + + @test + public mid_emptyVector() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + + // Act + const output = bv.mid(3, 0); + + // Assert + assert.isTrue(output.isEmpty); + } + + @test + public removeAtIndex_ReadOnly() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00, true); + + // Act, Assert + assert.throws(() => { bv.removeAtIndex(0); }); + + assert.isTrue(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public removeAtIndex_IndexOutOfRange() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00, true); + + // Act, Assert + assert.throws(() => { bv.removeAtIndex(0.1); }); + assert.throws(() => { bv.removeAtIndex(-1); }); + assert.throws(() => { bv.removeAtIndex(bv.length); }); + + assert.isTrue(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public removeAtIndex_RemoveFromFront() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0xAA, 0x00, 0x01, 0x02, 0x03])); + + // Act + bv.removeAtIndex(0); + + // Assert + assert.strictEqual(bv.length, 4); + assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02, 0x03])); + } + + @test + public removeAtIndex_RemoveFromBack() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xAA])); + + // Act + bv.removeAtIndex(bv.length - 1); + + // Assert + assert.strictEqual(bv.length, 4); + assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02, 0x03])); + } + + @test + public removeAtIndex_RemoveFromMiddle() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x00, 0x01, 0xAA, 0x02, 0x03])); + + // Act + bv.removeAtIndex(2); + + // Assert + assert.strictEqual(bv.length, 4); + assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02, 0x03])); + } + + @test + public removeRange_ReadOnly() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00, true); + + // Act, Assert + assert.throws(() => { bv.removeRange(0, 1); }); + + assert.isTrue(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public removeRange_IndexOutOfRange() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00, true); + + // Act, Assert + assert.throws(() => { bv.removeRange(0.1, 1); }); + assert.throws(() => { bv.removeRange(-1, 1); }); + assert.throws(() => { bv.removeRange(bv.length, 1); }); + + assert.isTrue(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public removeRange_CountOutOfRange() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00, true); + + // Act, Assert + assert.throws(() => { bv.removeRange(0, 0.1); }); + assert.throws(() => { bv.removeRange(0, -1); }); + + assert.isTrue(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public removeRange_RemoveFromFront() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0xAA, 0xBB, 0x00, 0x01, 0x02, 0x03])); + + // Act + bv.removeRange(0, 2); + + // Assert + assert.strictEqual(bv.length, 4); + assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02, 0x03])); + } + + @test + public removeRange_RemoveFromBack() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xAA, 0xBB])); + + // Act + bv.removeRange(bv.length - 2, 2); + + // Assert + assert.strictEqual(bv.length, 4); + assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02, 0x03])); + } + + @test + public removeRange_RemoveFromMiddle() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x00, 0x01, 0xAA, 0xBB, 0x02, 0x03])); + + // Act + bv.removeRange(2, 2); + + // Assert + assert.strictEqual(bv.length, 4); + assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02, 0x03])); + } + + @test + public resize_invalidParameters() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + + // Act / Assert + assert.throws(() => { bv.resize(-1); }); + assert.throws(() => { bv.resize(1.23); }); + assert.throws(() => { bv.resize(Number.MAX_SAFE_INTEGER + 1); }); + assert.throws(() => { bv.resize(1, -1); }); + assert.throws(() => { bv.resize(1, 1.23); }); + assert.throws(() => { bv.resize(1, 0x100); }); + } + + @test + public resize_shorten() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + + // Act + const output = bv.resize(6); + + // Assert + assert.strictEqual(output, bv); + assert.isTrue(ByteVector.equal(output, ByteVector.fromString("foobar"))); + } + + @test + public resize_sameSize() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + + // Act + const output = bv.resize(9); + + // Assert + assert.strictEqual(output, bv); + assert.isTrue(ByteVector.equal(output, ByteVector.fromString("foobarbaz"))); + } + + @test + public resize_lengthen() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x00, 0x01, 0x02, 0x03])); + + // Act + const output = bv.resize(7); + + // Assert + assert.strictEqual(output, bv); + assert.isTrue(ByteVector.equal( + output, + ByteVector.fromByteArray(new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x00, 0x00, 0x00]))) + ); + } + + @test + public resize_lengthenWithPadding() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x00, 0x01, 0x02, 0x03])); + + // Act + const output = bv.resize(7, 0x12); + + // Assert + assert.strictEqual(output, bv); + assert.isTrue(ByteVector.equal( + output, + ByteVector.fromByteArray(new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x12, 0x12, 0x12])) + )); + } + + @test + public rFind_invalidParameters() { + // Arrange + const bv = ByteVector.empty(); + const pattern = ByteVector.empty(); + + // Act / Assert + assert.throws(() => { bv.rFind(undefined, 0, 1); }); + assert.throws(() => { bv.rFind(null, 0, 1); }); + assert.throws(() => { bv.rFind(pattern, -1, 1); }); + assert.throws(() => { bv.rFind(pattern, 1.23, 1); }); + assert.throws(() => { bv.rFind(pattern, Number.MAX_SAFE_INTEGER + 1, 1); }); + assert.throws(() => { bv.rFind(pattern, 0, -1); }); + assert.throws(() => { bv.rFind(pattern, 0, 0); }); + assert.throws(() => { bv.rFind(pattern, 0, 1.23); }); + assert.throws(() => { bv.rFind(pattern, 0, Number.MAX_SAFE_INTEGER + 1); }); + } + + @test + public rFind_patternTooLong() { + // Arrange + const bv = ByteVector.fromSize(5); + const pattern = ByteVector.fromSize(10); + + // Act / Assert + assert.strictEqual(bv.rFind(pattern), -1); + } + + @test + public rFind_singleBytePattern_noMatch() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromByteArray(new Uint8Array(0x05)); + + // Act / Assert + assert.strictEqual(bv.rFind(pattern), -1); + } + + @test + public rFind_singleBytePattern_withMatch() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("b"); + + // Act / Assert + assert.strictEqual(bv.rFind(pattern), 6); + } + + @test + public rFind_multiBytePattern_noMatch() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("qux"); + + // Act / Assert + assert.strictEqual(bv.rFind(pattern), -1); + } + + @test + public rFind_multiBytePattern_singleMatch() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("bar"); + + // Act / Assert + assert.strictEqual(bv.rFind(pattern), 3); + } + + @test + public rFind_multiBytePattern_multiMatch() { + // Arrange + const bv = ByteVector.fromString("foofoofoo"); + const pattern = ByteVector.fromString("foo"); + + // Act / Assert + assert.strictEqual(bv.rFind(pattern), 6); + } + + @test + public rFind_multiByteWithOffset_noMatch() { + // Arrange + const bv = ByteVector.fromString("abcfoo"); + const pattern = ByteVector.fromString("foo"); + + // Act / Assert + assert.strictEqual(bv.rFind(pattern, 1), -1); + } + + @test + public rFind_multiByteWithOffset_withMatch() { + // Arrange + const bv = ByteVector.fromString("foofoofoo"); + const pattern = ByteVector.fromString("foo"); + + // Act / Assert + assert.strictEqual(bv.rFind(pattern, 1), 3); + } + + @test + public rFind_multiByteWithAlign_noMatch() { + // Arrange + const bv = ByteVector.fromString("0abc"); + const pattern = ByteVector.fromString("ab"); + + // Act / Assert + assert.strictEqual(bv.rFind(pattern, 0, 2), -1); + } + + @test + public rFind_multiByteWithAlign_withMatch() { + // Arrange + const bv = ByteVector.fromString("00abc"); + const pattern = ByteVector.fromString("ab"); + + // Act / Assert + assert.strictEqual(bv.rFind(pattern, 0, 2), 2); + } + + @test + public set_ReadOnly() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00, true); + + // Act, Assert + assert.throws(() => { bv.set(0, 1); }); + + assert.isTrue(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public set_IndexOutOfRange() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00, true); + + // Act, Assert + assert.throws(() => { bv.set(0.1, 1); }); + assert.throws(() => { bv.set(-1, 1); }); + assert.throws(() => { bv.set(bv.length, 1); }); + + assert.isTrue(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public set_ValueOutOfRange() { + // Arrange + const bv = ByteVector.fromSize(1, 0x00, true); + + // Act, Assert + assert.throws(() => { bv.set(0, 0.1); }); + assert.throws(() => { bv.set(0, -1); }); + assert.throws(() => { bv.set(0, 0x1FF); }); + + assert.isTrue(bv.isReadOnly); + assert.isFalse(bv.isEmpty); + assert.strictEqual(bv.length, 1); + assert.deepEqual(bv.data, new Uint8Array([0x00])); + } + + @test + public set_SetValue() { + // Arrange + const bv = ByteVector.fromByteArray(new Uint8Array([0x00, 0x01, 0x02])); + + // Act + bv.set(1, 0xAA); + + // Assert + assert.strictEqual(bv.length, 3); + assert.deepEqual(bv.data, new Uint8Array([0x00, 0xAA, 0x02])); + } + + @test + public split_invalidParameters() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromSize(1); + + // Act / Assert + assert.throws(() => bv.split(null)); + assert.throws(() => bv.split(undefined)); + assert.throws(() => bv.split(pattern, -1)); + assert.throws(() => bv.split(pattern, 0)); + assert.throws(() => bv.split(pattern, 1.23)); + assert.throws(() => bv.split(pattern, Number.MAX_SAFE_INTEGER + 1)); + assert.throws(() => bv.split(pattern, 1, -1)); + assert.throws(() => bv.split(pattern, 1, 1.23)); + assert.throws(() => bv.split(pattern, 1, Number.MAX_SAFE_INTEGER + 1)); + } + + @test + public split_singleByteSplit_noMatch() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString(","); + + // Act + const output = bv.split(pattern); + + // Assert + this.verifySplitOutput(output, ["foobarbaz"]); + } + + @test + public split_singleByteSplit_oneMatch() { + // Arrange + const bv = ByteVector.fromString("foo,baz"); + const pattern = ByteVector.fromString(","); + + // Act + const output = bv.split(pattern); + + // Assert + this.verifySplitOutput(output, ["foo", "baz"]); + } + + @test + public split_singleByteSplit_multipleMatch() { + // Arrange + const bv = ByteVector.fromString("foo,bar,,baz"); + const pattern = ByteVector.fromString(","); + + // Act + const output = bv.split(pattern); + + // Assert + this.verifySplitOutput(output, ["foo", "bar", "", "baz"]); + } + + @test + public split_multiByteSplit_noMatch() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString(",,"); + + // Act + const output = bv.split(pattern); + + // Assert + this.verifySplitOutput(output, ["foobarbaz"]); + } + + @test + public split_multiByteSplit_singleMatch() { + // Arrange + const bv = ByteVector.fromString("foo,,baz"); + const pattern = ByteVector.fromString(",,"); + + // Act + const output = bv.split(pattern); + + // Assert + this.verifySplitOutput(output, ["foo", "baz"]); + } + + @test + public split_multiByteSplit_multipleMatch() { + // Arrange + const bv = ByteVector.fromString("foo,,bar,,,,baz"); + const pattern = ByteVector.fromString(",,"); + + // Act + const output = bv.split(pattern); + + // Assert + this.verifySplitOutput(output, ["foo", "bar", "", "baz"]); + } + + @test + public split_singleByte_maxMatches() { + // Arrange + const bv = ByteVector.fromString("foo,bar,,baz"); + const pattern = ByteVector.fromString(","); + + // Act + const output = bv.split(pattern, undefined, 2); + + // Assert + this.verifySplitOutput(output, ["foo", "bar,,baz"]); + } + + @test + public split_multiByteWithByteAlign_noMatch() { + // Arrange + const bv = ByteVector.fromString("foo,,bar"); + const pattern = ByteVector.fromString(",,"); + + // Act + const output = bv.split(pattern, 2); + + // Assert + this.verifySplitOutput(output, ["foo,,bar"]); + } + + @test + public split_multiByteWithByteAlign_match() { + // Arrange + const bv = ByteVector.fromString("0foo,,bar"); + const pattern = ByteVector.fromString(",,"); + + // Act + const output = bv.split(pattern, 2); + + // Assert + this.verifySplitOutput(output, ["0foo", "bar"]); + } + + @test + public startsWith_invalidParam() { + // Arrange + const bv = ByteVector.empty(); + + // Act / Assert + assert.throws(() => { bv.startsWith(undefined); }); + assert.throws(() => { bv.startsWith(null); }); + } + + @test + public startsWith_doesStartWith() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("foo"); + + // Act / Assert + assert.isTrue(bv.startsWith(pattern)); + } + + @test + public startsWith_doesNotStartWith() { + // Arrange + const bv = ByteVector.fromString("foobarbaz"); + const pattern = ByteVector.fromString("fux"); + + // Act / Assert + assert.isFalse(bv.startsWith(pattern)); + } + + private verifySplitOutput(output: ByteVector[], expected: string[]) { + assert.isOk(output); + assert.strictEqual(output.length, expected.length); + for (let i = 0; i < expected.length; i++) { + assert.isTrue(ByteVector.equal(output[i], ByteVector.fromString(expected[i]))); + } + } +} diff --git a/test/byteVectorVoidOperationTests.ts b/test/byteVectorVoidOperationTests.ts deleted file mode 100644 index 79a8ef8c..00000000 --- a/test/byteVectorVoidOperationTests.ts +++ /dev/null @@ -1,870 +0,0 @@ -import * as Chai from "chai"; -import * as ChaiAsPromised from "chai-as-promised"; -import {slow, suite, test, timeout} from "mocha-typescript"; - -import {ByteVector} from "../src/byteVector"; - -// Setup chai -Chai.use(ChaiAsPromised); -const assert = Chai.assert; - -@suite(timeout(3000), slow(1000)) -class ByteVector_AddByteTests { - @test - public ReadOnly() { - // Arrange - Create readonly ByteVector - const bv = ByteVector.fromSize(1, 0x0, true); - - // Act, Assert - AddByte should fail, ByteVector should be unchanged - assert.throws(() => { bv.addByte(0x01); }); - assert.isTrue(bv.isReadOnly); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public InvalidByte() { - // Arrange - Create ByteVector - const bv = ByteVector.fromSize(1); - - // Act, Assert - AddByte should fail, ByteVector should be unchanged - assert.throws(() => { bv.addByte(0.1); }); - assert.throws(() => { bv.addByte(-1); }); - assert.throws(() => { bv.addByte(0x1FF); }); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public AddToEmpty() { - // Arrange - Create ByteVector that is empty - const bv = ByteVector.fromSize(0); - - // Act - Add a byte to it - bv.addByte(0x01); - - // Assert - ByteVector should contain the new byte - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x01])); - } - - @test - public AddToExisting() { - // Arrange - Create ByteVector with something in it - const bv = ByteVector.fromSize(1); - - // Act - Add a byte to it - bv.addByte(0x01); - - // Assert - ByteVector should contain the new byte - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 2); - assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01])); - } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_AddByteArrayTests { - @test - public ReadOnly() { - // Arrange - Create readonly ByteVector - const bv = ByteVector.fromSize(1, 0x0, true); - - // Act, Assert - AddByte should fail, ByteVector should be unchanged - assert.throws(() => { bv.addByteArray(new Uint8Array(0x01)); }); - assert.isTrue(bv.isReadOnly); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public InvalidByteArray() { - // Arrange - const bv = ByteVector.fromSize(1, 0x0, true); - - // Act, Assert - assert.throws(() => { bv.addByteArray(null); }); - assert.throws(() => { bv.addByteArray(undefined); }); - } - - @test - public AddEmptyToEmpty() { - // Arrange - Create ByteVector that is empty - const bv = ByteVector.fromSize(0); - - // Act - Add nothing to it - bv.addByteArray(new Uint8Array()); - - // Assert - ByteVector should contain the new byte - assert.isTrue(bv.isEmpty); - assert.strictEqual(bv.length, 0); - assert.deepEqual(bv.data, new Uint8Array()); - } - - @test - public AddSingleToEmpty() { - // Arrange - Create ByteVector that is empty - const bv = ByteVector.fromSize(0); - - // Act - Add a byte to it - bv.addByteArray(new Uint8Array([0x01])); - - // Assert - ByteVector should contain the new byte - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x01])); - } - - @test - public AddMultipleToEmpty() { - // Arrange - Create ByteVector that is empty - const bv = ByteVector.fromSize(0); - - // Act - Add two bytes to it - bv.addByteArray(new Uint8Array([0x01, 0x02])); - - // Assert - ByteVector should contain the new bytes - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 2); - assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02])); - } - - @test - public AddEmptyToExisting() { - // Arrange - Create ByteVector that has bytes - const bv = ByteVector.fromSize(1); - - // Act - Add nothing to it - bv.addByteArray(new Uint8Array()); - - // Assert - ByteVector should contain the new byte - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public AddSingleToExisting() { - // Arrange - Create ByteVector that has bytes - const bv = ByteVector.fromSize(1); - - // Act - Add a byte to it - bv.addByteArray(new Uint8Array([0x01])); - - // Assert - ByteVector should contain the new byte - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 2); - assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01])); - } - - @test - public AddMultipleToExisting() { - // Arrange - Create ByteVector that is empty - const bv = ByteVector.fromSize(1); - - // Act - Add two bytes to it - bv.addByteArray(new Uint8Array([0x01, 0x02])); - - // Assert - ByteVector should contain the new bytes - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 3); - assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02])); - } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_AddByteVectorTests { - @test - public ReadOnly() { - // Arrange - Create readonly ByteVector - const bv = ByteVector.fromSize(1, 0x0, true); - const add = ByteVector.fromSize(1, 0x1); - - // Act, Assert - AddByte should fail, ByteVector should be unchanged - assert.throws(() => { bv.addByteVector(add); }); - assert.isTrue(bv.isReadOnly); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public InvalidByteVector() { - // Arrange - const bv = ByteVector.fromSize(1, 0x0, true); - - // Act, Assert - assert.throws(() => { bv.addByteVector(null); }); - assert.throws(() => { bv.addByteVector(undefined); }); - } - - @test - public AddEmptyToEmpty() { - // Arrange - Create ByteVector that is empty - const bv = ByteVector.fromSize(0); - - // Act - Add nothing to it - bv.addByteVector(ByteVector.fromSize(0)); - - // Assert - ByteVector should contain the new byte - assert.isTrue(bv.isEmpty); - assert.strictEqual(bv.length, 0); - assert.deepEqual(bv.data, new Uint8Array()); - } - - @test - public AddSingleToEmpty() { - // Arrange - Create ByteVector that is empty - const bv = ByteVector.fromSize(0); - const add = ByteVector.fromSize(1, 0x1); - - // Act - Add a byte to it - bv.addByteVector(add); - - // Assert - ByteVector should contain the new byte - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x01])); - } - - @test - public AddMultipleToEmpty() { - // Arrange - Create ByteVector that is empty - const bv = ByteVector.fromSize(0); - const add = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); - - // Act - Add two bytes to it - bv.addByteVector(add); - - // Assert - ByteVector should contain the new bytes - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 2); - assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02])); - } - - @test - public AddSingleToExisting() { - // Arrange - Create ByteVector that has bytes - const bv = ByteVector.fromSize(1); - const add = ByteVector.fromByteArray(new Uint8Array([0x01])); - - // Act - Add a byte to it - bv.addByteVector(add); - - // Assert - ByteVector should contain the new byte - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 2); - assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01])); - } - - @test - public AddMultipleToExisting() { - // Arrange - Create ByteVector that is empty - const bv = ByteVector.fromSize(1); - const add = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02])); - - // Act - Add two bytes to it - bv.addByteVector(add); - - // Assert - ByteVector should contain the new bytes - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 3); - assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02])); - } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_ClearTests { - @test - public ReadOnly() { - // Arrange - Create readonly bytevector - const bv = ByteVector.fromSize(1, 0x00, true); - - // Act, Assert - Should throw, bytevector should be unchanged - assert.throws(() => { bv.clear(); } ); - assert.isTrue(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public ClearEmpty() { - // Arrange - Create empty ByteVector - const bv = ByteVector.fromSize(0); - - // Act - bv.clear(); - - // Assert - assert.isTrue(bv.isEmpty); - assert.strictEqual(bv.length, 0); - assert.deepEqual(bv.data, new Uint8Array()); - } - - @test - public ClearExisting() { - // Arrange - Create ByteVector with some data - const bv = ByteVector.fromSize(1, 0x00); - - // Act - bv.clear(); - - // Assert - assert.isTrue(bv.isEmpty); - assert.strictEqual(bv.length, 0); - assert.deepEqual(bv.data, new Uint8Array()); - } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_InsertByteTests { - @test - public ReadOnly() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00, true); - - // Act, Assert - assert.throws(() => { bv.insertByte(0, 0x00); }); - - assert.isTrue(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public IndexOutOfRange() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00); - - // Act, Assert - assert.throws(() => { bv.insertByte(0.1, 0x01); }); - assert.throws(() => { bv.insertByte(-1, 0x01); }); - assert.throws(() => { bv.insertByte(2, 0x01); }); - - assert.isFalse(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public ByteOutOfRange() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00); - - // Act, Assert - assert.throws(() => { bv.insertByte(0, 0.1); }); - assert.throws(() => { bv.insertByte(0, -1); }); - assert.throws(() => { bv.insertByte(0, 0x1FF); }); - - assert.isFalse(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public InsertIntoEmpty() { - // Arrange - const bv = ByteVector.fromSize(0); - - // Act - bv.insertByte(0, 0x01); - - // Assert - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x01])); - } - - @test - public InsertAtFront() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); - - // Act - bv.insertByte(0, 0xAA); - - // Assert - assert.strictEqual(bv.length, 5); - assert.deepEqual(bv.data, new Uint8Array([0xAA, 0x01, 0x02, 0x03, 0x04])); - } - - @test - public InsertAtBack() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); - - // Act - bv.insertByte(bv.length, 0xAA); - - // Assert - assert.strictEqual(bv.length, 5); - assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02, 0x03, 0x04, 0xAA])); - } - - @test - public InsertInMiddle() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); - - // Act - bv.insertByte(2, 0xAA); - - // Assert - assert.strictEqual(bv.length, 5); - assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02, 0xAA, 0x03, 0x04])); - } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_InsertByteArrayTests { - @test - public ReadOnly() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00, true); - - // Act, Assert - assert.throws(() => { bv.insertByteArray(0, new Uint8Array()); }); - - assert.isTrue(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public IndexOutOfRange() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00); - - // Act, Assert - assert.throws(() => { bv.insertByteArray(0.1, new Uint8Array()); }); - assert.throws(() => { bv.insertByteArray(-1, new Uint8Array()); }); - assert.throws(() => { bv.insertByteArray(2, new Uint8Array()); }); - - assert.isFalse(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public ByteArrayInvalid() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00); - - // Act, Assert - assert.throws(() => { bv.insertByteArray(0, null); }); - assert.throws(() => { bv.insertByteArray(0, undefined); }); - - assert.isFalse(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public InsertEmpty() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00); - - // Act - bv.insertByteArray(0, new Uint8Array()); - - // Assert - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public InsertIntoEmpty() { - // Arrange - const bv = ByteVector.fromSize(0); - - // Act - bv.insertByteArray(0, new Uint8Array([0x01, 0x02])); - - // Assert - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 2); - assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02])); - } - - @test - public InsertAtFront() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); - - // Act - bv.insertByteArray(0, new Uint8Array([0xAA, 0xBB])); - - // Assert - assert.strictEqual(bv.length, 6); - assert.deepEqual(bv.data, new Uint8Array([0xAA, 0xBB, 0x01, 0x02, 0x03, 0x04])); - } - - @test - public InsertAtBack() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); - - // Act - bv.insertByteArray(bv.length, new Uint8Array([0xAA, 0xBB])); - - // Assert - assert.strictEqual(bv.length, 6); - assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02, 0x03, 0x04, 0xAA, 0xBB])); - } - - @test - public InsertInMiddle() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); - - // Act - bv.insertByteArray(2, new Uint8Array([0xAA, 0xBB])); - - // Assert - assert.strictEqual(bv.length, 6); - assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02, 0xAA, 0xBB, 0x03, 0x04])); - } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_InsertByteVectorTests { - private static readonly vectorToAdd: ByteVector = ByteVector.fromByteArray( - new Uint8Array([0xAA, 0xBB]) - ); - - @test - public ReadOnly() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00, true); - - // Act, Assert - assert.throws(() => { bv.insertByteVector(0, ByteVector_InsertByteVectorTests.vectorToAdd); }); - - assert.isTrue(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public IndexOutOfRange() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00); - - // Act, Assert - assert.throws(() => { bv.insertByteVector(0.1, ByteVector_InsertByteVectorTests.vectorToAdd); }); - assert.throws(() => { bv.insertByteVector(-1, ByteVector_InsertByteVectorTests.vectorToAdd); }); - assert.throws(() => { bv.insertByteVector(2, ByteVector_InsertByteVectorTests.vectorToAdd); }); - - assert.isFalse(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public ByteArrayInvalid() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00); - - // Act, Assert - assert.throws(() => { bv.insertByteVector(0, null); }); - assert.throws(() => { bv.insertByteVector(0, undefined); }); - - assert.isFalse(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public InsertEmpty() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00); - - // Act - bv.insertByteVector(0, ByteVector.fromSize(0)); - - // Assert - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public InsertIntoEmpty() { - // Arrange - const bv = ByteVector.fromSize(0); - - // Act - bv.insertByteVector(0, ByteVector_InsertByteVectorTests.vectorToAdd); - - // Assert - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 2); - assert.deepEqual(bv.data, new Uint8Array([0xAA, 0xBB])); - } - - @test - public InsertAtFront() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); - - // Act - bv.insertByteVector(0, ByteVector_InsertByteVectorTests.vectorToAdd); - - // Assert - assert.strictEqual(bv.length, 6); - assert.deepEqual(bv.data, new Uint8Array([0xAA, 0xBB, 0x01, 0x02, 0x03, 0x04])); - } - - @test - public InsertAtBack() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); - - // Act - bv.insertByteVector(bv.length, ByteVector_InsertByteVectorTests.vectorToAdd); - - // Assert - assert.strictEqual(bv.length, 6); - assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02, 0x03, 0x04, 0xAA, 0xBB])); - } - - @test - public InsertInMiddle() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04])); - - // Act - bv.insertByteVector(2, ByteVector_InsertByteVectorTests.vectorToAdd); - - // Assert - assert.strictEqual(bv.length, 6); - assert.deepEqual(bv.data, new Uint8Array([0x01, 0x02, 0xAA, 0xBB, 0x03, 0x04])); - } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_RemoveAtIndexTests { - @test - public ReadOnly() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00, true); - - // Act, Assert - assert.throws(() => { bv.removeAtIndex(0); }); - - assert.isTrue(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public IndexOutOfRange() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00, true); - - // Act, Assert - assert.throws(() => { bv.removeAtIndex(0.1); }); - assert.throws(() => { bv.removeAtIndex(-1); }); - assert.throws(() => { bv.removeAtIndex(bv.length); }); - - assert.isTrue(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public RemoveFromFront() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0xAA, 0x00, 0x01, 0x02, 0x03])); - - // Act - bv.removeAtIndex(0); - - // Assert - assert.strictEqual(bv.length, 4); - assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02, 0x03])); - } - - @test - public RemoveFromBack() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xAA])); - - // Act - bv.removeAtIndex(bv.length - 1); - - // Assert - assert.strictEqual(bv.length, 4); - assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02, 0x03])); - } - - @test - public RemoveFromMiddle() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0x00, 0x01, 0xAA, 0x02, 0x03])); - - // Act - bv.removeAtIndex(2); - - // Assert - assert.strictEqual(bv.length, 4); - assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02, 0x03])); - } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_RemoveRangeTests { - @test - public ReadOnly() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00, true); - - // Act, Assert - assert.throws(() => { bv.removeRange(0, 1); }); - - assert.isTrue(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public IndexOutOfRange() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00, true); - - // Act, Assert - assert.throws(() => { bv.removeRange(0.1, 1); }); - assert.throws(() => { bv.removeRange(-1, 1); }); - assert.throws(() => { bv.removeRange(bv.length, 1); }); - - assert.isTrue(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public CountOutOfRange() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00, true); - - // Act, Assert - assert.throws(() => { bv.removeRange(0, 0.1); }); - assert.throws(() => { bv.removeRange(0, -1); }); - - assert.isTrue(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public RemoveFromFront() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0xAA, 0xBB, 0x00, 0x01, 0x02, 0x03])); - - // Act - bv.removeRange(0, 2); - - // Assert - assert.strictEqual(bv.length, 4); - assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02, 0x03])); - } - - @test - public RemoveFromBack() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xAA, 0xBB])); - - // Act - bv.removeRange(bv.length - 2, 2); - - // Assert - assert.strictEqual(bv.length, 4); - assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02, 0x03])); - } - - @test - public RemoveFromMiddle() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0x00, 0x01, 0xAA, 0xBB, 0x02, 0x03])); - - // Act - bv.removeRange(2, 2); - - // Assert - assert.strictEqual(bv.length, 4); - assert.deepEqual(bv.data, new Uint8Array([0x00, 0x01, 0x02, 0x03])); - } -} - -@suite(timeout(3000), slow(1000)) -class ByteVector_SetTests { - @test - public ReadOnly() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00, true); - - // Act, Assert - assert.throws(() => { bv.set(0, 1); }); - - assert.isTrue(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public IndexOutOfRange() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00, true); - - // Act, Assert - assert.throws(() => { bv.set(0.1, 1); }); - assert.throws(() => { bv.set(-1, 1); }); - assert.throws(() => { bv.set(bv.length, 1); }); - - assert.isTrue(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public ValueOutOfRange() { - // Arrange - const bv = ByteVector.fromSize(1, 0x00, true); - - // Act, Assert - assert.throws(() => { bv.set(0, 0.1); }); - assert.throws(() => { bv.set(0, -1); }); - assert.throws(() => { bv.set(0, 0x1FF); }); - - assert.isTrue(bv.isReadOnly); - assert.isFalse(bv.isEmpty); - assert.strictEqual(bv.length, 1); - assert.deepEqual(bv.data, new Uint8Array([0x00])); - } - - @test - public SetValue() { - // Arrange - const bv = ByteVector.fromByteArray(new Uint8Array([0x00, 0x01, 0x02])); - - // Act - bv.set(1, 0xAA); - - // Assert - assert.strictEqual(bv.length, 3); - assert.deepEqual(bv.data, new Uint8Array([0x00, 0xAA, 0x02])); - } -} From 353b443d91ccaf7e56c92b7ad95a5e54e9ef5d13 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Wed, 8 Apr 2020 23:08:20 -0400 Subject: [PATCH 61/71] Adding some tests for stream --- .gitignore | 5 +- package-lock.json | 32 +++- package.json | 4 +- src/fileAbstraction.ts | 2 + src/stream.ts | 80 +++++++-- test/streamTest.ts | 379 +++++++++++++++++++++++++++++++++++++++++ test/testConstants.ts | 12 +- 7 files changed, 495 insertions(+), 19 deletions(-) create mode 100644 test/streamTest.ts diff --git a/.gitignore b/.gitignore index 19b3443d..78e75114 100644 --- a/.gitignore +++ b/.gitignore @@ -83,4 +83,7 @@ crashlytics-build.properties fabric.properties # Ignore build output -dist/** \ No newline at end of file +dist/** + +# Files created during test +test/resources/testFile_* \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d39eecdc..ba145f04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -349,6 +349,12 @@ "@types/node": "*" } }, + "@types/uuid": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.2.tgz", + "integrity": "sha512-8Ly3zIPTnT0/8RCU6Kg/G3uTICf9sRwYOpUzSIM3503tLIKcnJPRuinHhXngJUy2MntrEf6dlpOHXJju90Qh5w==", + "dev": true + }, "@typescript-eslint/typescript-estree": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz", @@ -1900,6 +1906,12 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2803,6 +2815,12 @@ "ansi-regex": "^5.0.0" } }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -3348,6 +3366,14 @@ "tough-cookie": "~2.4.3", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, "require-directory": { @@ -3863,9 +3889,9 @@ "dev": true }, "uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", "dev": true }, "verror": { diff --git a/package.json b/package.json index 01d6c609..6ce2159d 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@types/dateformat": "^3.0.0", "@types/node": "^11.10.4", "@types/stream-buffers": "^3.0.2", + "@types/uuid": "^7.0.2", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", "coveralls": "^3.0.9", @@ -35,6 +36,7 @@ "ts-node": "^8.5.4", "tslint": "^5.13.1", "typemoq": "^2.1.0", - "typescript": "^3.4.3" + "typescript": "^3.4.3", + "uuid": "^7.0.2" } } diff --git a/src/fileAbstraction.ts b/src/fileAbstraction.ts index 9fc8dff5..97c1e0ec 100644 --- a/src/fileAbstraction.ts +++ b/src/fileAbstraction.ts @@ -1,4 +1,5 @@ import {IStream, Stream} from "./stream"; +import {Guards} from "./utils"; export interface IFileAbstraction { /** @@ -41,6 +42,7 @@ export class LocalFileAbstraction implements IFileAbstraction { * @throws Error Thrown if {@param path} is falsey */ public constructor(path: string) { + Guards.truthy(path, "path"); if (!path) { throw new Error("Argument null: path was not provided"); } diff --git a/src/stream.ts b/src/stream.ts index 840f333c..7df41c80 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -1,5 +1,7 @@ import * as fs from "fs"; +import {Guards} from "./utils"; + export enum SeekOrigin { Begin, Current, @@ -7,18 +9,61 @@ export enum SeekOrigin { } export interface IStream { + /** + * Whether or not the stream can be written to + */ readonly canWrite: boolean; + + /** + * Number of bytes currently stored in file this stream connects to + */ readonly length: number; + + /** + * Position within the stream + */ position: number; + /** + * Closes the stream + */ close(): void; - read(buffer: Uint8Array, bufferOffset: number, length: number): number; - + /** + * Reads a block of bytes from the current stream and writes the data to a buffer. + * @param buffer When this method returns, contains the specified byte array with the values + * between {@paramref offset} and ({@paramref offset} + {@paramref length} - 1) replaced by + * the characters read from the current stream + * @param offset Zero-based byte offset in {@paramref buffer} at which to begin storing data + * from the current stream + * @param length The maximum number of bytes to read + * @returns number Total number of bytes written to the buffer. This can be less than the + * number of bytes requested if that number of bytes are not currently available or zero if + * the end of the stream is reached before any bytes are read + */ + read(buffer: Uint8Array, offset: number, length: number): number; + + /** + * Sets the position within the current stream to the specified value. + * @param offset New positioon within the stream. this is relative to the {@paramref origin} + * paramter and can be positive or negative + * @param origin Seek reference point {@see SeekOrigin} + */ seek(offset: number, origin: SeekOrigin): void; + /** + * Sets the length of the current current stream to the specified value. + * @param length Number of bytes to set the length of the stream to + */ setLength(length: number): void; + /** + * Writes a block of bytes to the current stream using data read from a buffer. + * @param buffer Buffer to write data from + * @param bufferOffset Zero-based byte offset in {@paramref buffer} at which to begin copying + * bytes to the current stream + * @param length Maximum number of bytes to write + */ write(buffer: fs.BinaryData, bufferOffset: number, length: number): number; } @@ -54,16 +99,18 @@ export class Stream implements IStream { // #region Properties + /** @inheritDoc */ public get canWrite(): boolean { return this._canWrite; } + /** @inheritDoc */ public get length(): number { return this._length; } + /** @inheritDoc */ public get position(): number { return this._position; } + /** @inheritDoc */ public set position(position: number) { - // @TODO: Make sure seek position is valid for the file - if (!Number.isSafeInteger(position) || position < 0) { - throw new Error("Argument out of range: position must be a positive, safe integer"); - } + Guards.uint(position, "position"); + Guards.lessThanInclusive(position, this.length, "position"); this._position = position; } @@ -71,21 +118,20 @@ export class Stream implements IStream { // #region Public Methods + /** @inheritDoc */ public close(): void { fs.closeSync(this._fd); } + /** @inheritDoc */ public read(buffer: Uint8Array, bufferOffset: number, length: number): number { const bytes = fs.readSync(this._fd, buffer, bufferOffset, length, this._position); this._position += bytes; return bytes; } + /** @inheritDoc */ public seek(offset: number, origin: SeekOrigin) { - if (!Number.isSafeInteger(offset) || offset < 0) { - throw new Error("Argument out of range: offset must be a safe, positive integer"); - } - switch (origin) { case SeekOrigin.Begin: this.position = offset; @@ -94,22 +140,30 @@ export class Stream implements IStream { this.position = this.position + offset; break; case SeekOrigin.End: - // @TODO: Add support for end + this.position = this.length + offset; + break; } } + /** @inheritDoc */ public setLength(length: number): void { + Guards.uint(length, "length"); if (!this._canWrite) { throw new Error("Invalid operation: this stream is a read-only stream"); } - if (!Number.isSafeInteger(length) || length < 0) { - throw new Error("Argument out of range: length must be a safe, positive integer"); + + if (length === this._length) { + return; } fs.ftruncateSync(this._fd, length); this._length = length; + if (this._position > this._length) { + this._position = this._length; + } } + /** @inheritDoc */ public write(buffer: fs.BinaryData, bufferOffset: number, length: number): number { if (!this._canWrite) { throw new Error("Invalid operation: this stream is a read-only stream"); diff --git a/test/streamTest.ts b/test/streamTest.ts new file mode 100644 index 00000000..44f3069e --- /dev/null +++ b/test/streamTest.ts @@ -0,0 +1,379 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import * as fs from "fs"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import PropertyTests from "./utilities/propertyTests"; +import TestConstants from "./testConstants"; +import {SeekOrigin, Stream} from "../src/stream"; +import testFile from "./utilities/testFile"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(slow(1000), timeout(3000)) +class StreamTests { + @test + public createAsRead() { + // Act + const stream = Stream.createAsRead(TestConstants.testFilePath); + + try { + // Assert + assert.isOk(stream); + assert.isFalse(stream.canWrite); + assert.strictEqual(stream.length, 10); + assert.strictEqual(stream.position, 0); + } finally { + // Cleanup + stream.close(); + } + } + + @test + public createAsRead_fileDoesNotExist() { + // Arrange + const testPath = TestConstants.getTestFilePath(); + + // Act / Assert + assert.throws(() => { const stream = Stream.createAsRead(testPath); }); + } + + @test + public createAsReadWrite_fileExists() { + // Act + const stream = Stream.createAsReadWrite(TestConstants.testFilePath); + + try { + // Assert + assert.isOk(stream); + assert.isTrue(stream.canWrite); + assert.strictEqual(stream.length, 10); + assert.strictEqual(stream.position, 0); + } finally { + // Cleanup + stream.close(); + } + } + + @test + public createAsReadWrite_fileDoesNotExist() { + // Arrange + const testPath = TestConstants.getTestFilePath(); + + // Act / Assert + assert.throws(() => { const stream = Stream.createAsReadWrite(testPath); }); + } + + @test + public position_invalidPosition() { + // Arrange + const stream = Stream.createAsRead(TestConstants.testFilePath); + + try { + // Act / Assert + assert.throws(() => stream.position = -1); + assert.throws(() => stream.position = 1.23); + assert.throws(() => stream.position = TestConstants.testFileContents.length + 1); + assert.throws(() => stream.position = Number.MAX_SAFE_INTEGER + 1); + } finally { + // Cleanup + stream.close(); + } + } + + @test + public read() { + // Arrange + const stream = Stream.createAsRead(TestConstants.testFilePath); + let buffer: Uint8Array; + let output: number; + + try { + // Act - Read into beginning of buffer + buffer = new Uint8Array(5); + output = stream.read(buffer, 0, 3); + + // Assert + const expected1 = new Uint8Array([ + TestConstants.testFileContents[0], + TestConstants.testFileContents[1], + TestConstants.testFileContents[2], + 0, 0 + ]); + assert.deepStrictEqual(buffer, expected1); + assert.strictEqual(output, 3); + assert.strictEqual(stream.position, 3); + + // Act - Read into buffer w/offset + buffer = new Uint8Array(5); + output = stream.read(buffer, 1, 3); + + // Assert + const expected2 = new Uint8Array([ + 0, + TestConstants.testFileContents[3], + TestConstants.testFileContents[4], + TestConstants.testFileContents[5], + 0 + ]); + assert.deepStrictEqual(buffer, expected2); + assert.strictEqual(output, 3); + assert.strictEqual(stream.position, 6); + + // Act - Read to end of stream + buffer = new Uint8Array(5); + output = stream.read(buffer, 0, 5); + + // Assert + const expected3 = new Uint8Array([ + TestConstants.testFileContents[6], + TestConstants.testFileContents[7], + TestConstants.testFileContents[8], + TestConstants.testFileContents[9], + 0 + ]); + assert.deepStrictEqual(buffer, expected3); + assert.strictEqual(output, 4); + assert.strictEqual(stream.position, 10); + + // Act - Read at end of stream + buffer = new Uint8Array(3); + output = stream.read(buffer, 0, 3); + + // Assert + const expected4 = new Uint8Array([0, 0, 0]); + assert.deepStrictEqual(buffer, expected4); + assert.strictEqual(output, 0); + assert.strictEqual(stream.position, 10); + + // Act - Move position and read again + stream.position = 0; + buffer = new Uint8Array(5); + output = stream.read(buffer, 0, 5); + + // Assert + const expected5 = new Uint8Array([ + TestConstants.testFileContents[0], + TestConstants.testFileContents[1], + TestConstants.testFileContents[2], + TestConstants.testFileContents[3], + TestConstants.testFileContents[4] + ]); + assert.deepStrictEqual(buffer, expected5); + assert.strictEqual(output, 5); + assert.strictEqual(stream.position, 5); + } finally { + // Cleanup + stream.close(); + } + } + + @test + public seek_begin_invalidPosition() { + // Arrange + const stream = Stream.createAsRead(TestConstants.testFilePath); + + try { + // Act / Assert + assert.throws(() => stream.seek(-1, SeekOrigin.Begin)); + assert.throws(() => stream.seek(1.23, SeekOrigin.Begin)); + assert.throws(() => stream.seek(TestConstants.testFileContents.length + 1, SeekOrigin.Begin)); + assert.throws(() => stream.seek(Number.MAX_SAFE_INTEGER + 1, SeekOrigin.Begin)); + } finally { + // Cleanup + stream.close(); + } + } + + @test + public seek_begin_valid() { + // Arrange + const stream = Stream.createAsRead(TestConstants.testFilePath); + const get = () => stream.position; + const set = (v: number) => stream.seek(v, SeekOrigin.Begin); + + try { + // Act / Assert + PropertyTests.propertyRoundTrip(set, get, 3); + PropertyTests.propertyRoundTrip(set, get, 0); + PropertyTests.propertyRoundTrip(set, get, TestConstants.testFileContents.length); + } finally { + // Cleanup + stream.close(); + } + } + + @test + public seek_current_invalidPosition() { + // Arrange + const stream = Stream.createAsRead(TestConstants.testFilePath); + stream.position = 5; + + try { + // Act / Assert + assert.throws(() => stream.seek(1.23, SeekOrigin.Current)); // Not int + assert.throws(() => stream.seek(Number.MAX_SAFE_INTEGER + 1, SeekOrigin.Current)); // Not int + assert.throws(() => stream.seek(6, SeekOrigin.Current)); // Would go past end of stream + assert.throws(() => stream.seek(-6, SeekOrigin.Current)); // Would go past beginning of stream + } finally { + // Cleanup + stream.close(); + } + } + + @test + public seek_current_valid() { + // Arrange + const stream = Stream.createAsRead(TestConstants.testFilePath); + stream.position = 5; + const get = () => stream.position; + const set = (v: number) => stream.seek(v, SeekOrigin.Current); + + try { + // Act / Assert + PropertyTests.propertyNormalized(set, get, 3, 8); + PropertyTests.propertyNormalized(set, get, 0, 8); + PropertyTests.propertyNormalized(set, get, 2, 10); + PropertyTests.propertyNormalized(set, get, -10, 0); + } finally { + // Cleanup + stream.close(); + } + } + + @test + public seek_end_invalidPosition() { + // Arrange + const stream = Stream.createAsRead(TestConstants.testFilePath); + + try { + // Act / Assert + assert.throws(() => stream.seek(1.23, SeekOrigin.End)); // Not int + assert.throws(() => stream.seek(Number.MAX_SAFE_INTEGER + 1, SeekOrigin.End)); // Not int + assert.throws(() => stream.seek(1, SeekOrigin.End)); // Would go past end of stream + assert.throws(() => stream.seek(-11, SeekOrigin.End)); // Would go past beginning of stream + } finally { + // Cleanup + stream.close(); + } + } + + @test + public seek_end_valid() { + // Arrange + const stream = Stream.createAsRead(TestConstants.testFilePath); + const get = () => stream.position; + const set = (v: number) => stream.seek(v, SeekOrigin.End); + + try { + // Act / Assert + PropertyTests.propertyNormalized(set, get, -5, 5); + PropertyTests.propertyNormalized(set, get, 0, TestConstants.testFileContents.length); + PropertyTests.propertyNormalized(set, get, -TestConstants.testFileContents.length, 0); + } finally { + // Cleanup + stream.close(); + } + } + + @test + public setLength_invalidLength() { + const testAction = (testFilePath: string, stream: Stream) => { + // Act / Assert + assert.throws(() => stream.setLength(-1)); + assert.throws(() => stream.setLength(1.23)); + assert.throws(() => stream.setLength(Number.MAX_SAFE_INTEGER + 1)); + }; + this.testWithFile(testAction, true); + } + + @test + public setLength_readOnly() { + const testAction = (testFilePath: string, stream: Stream) => { + // Act / Assert + assert.throws(() => stream.setLength(10)); + }; + this.testWithFile(testAction, false); + } + + @test + public setLength_noChange() { + const testAction = (testFilePath: string, stream: Stream) => { + // Arrange + stream.position = 5; + + // Act + stream.setLength(stream.length); + + // Assert + assert.strictEqual(stream.length, testFilePath.length); + assert.strictEqual(stream.position, 5); + + const stats = fs.statSync(testFilePath); + assert.strictEqual(stats.size, testFilePath.length); + }; + this.testWithFile(testAction, true); + } + + @test + public setLength_shorten() { + const testAction = (testFilePath: string, stream: Stream) => { + // Arrange + stream.position = 5; + + // Act + stream.setLength(4); + + // Assert + assert.strictEqual(stream.length, 4); + assert.strictEqual(stream.position, 4); + + const stats = fs.statSync(testFilePath); + assert.strictEqual(stats.size, 4); + }; + this.testWithFile(testAction, true); + } + + @test + public setLength_lengthen() { + const testAction = (testFilePath: string, stream: Stream) => { + // Arrange + stream.position = 5; + const newLength = stream.length + 5; + + // Act + stream.setLength(newLength); + + // Assert + assert.strictEqual(stream.length, newLength); + assert.strictEqual(stream.position, 5); + + const stats = fs.statSync(testFilePath); + assert.strictEqual(stats.size, newLength); + }; + this.testWithFile(testAction, true); + } + + private testWithFile(testAction: (filePath: string, stream: Stream) => void, isWritable: boolean) { + // Arrange + const testFilePath = TestConstants.getTestFilePath(); + fs.writeFileSync(testFilePath, testFilePath); + + const stream = isWritable + ? Stream.createAsReadWrite(testFilePath) + : Stream.createAsRead(testFilePath); + + try { + testAction(testFilePath, stream); + } finally { + // Cleanup + try { + fs.unlinkSync(testFilePath); + } catch { + // It's just best effort + } + } + } +} diff --git a/test/testConstants.ts b/test/testConstants.ts index 9d0a6883..4e2e4c31 100644 --- a/test/testConstants.ts +++ b/test/testConstants.ts @@ -1,8 +1,18 @@ +import * as Path from "path"; +import {v4 as Uuidv4} from "uuid"; + import {ByteVector} from "../src/byteVector"; export default class TestConstants { - public static testFilePath: string = "./test/resources/testFile.txt"; + public static testFileFolderPath: string = "./test/resources/"; + public static testFilePath: string = Path.join(TestConstants.testFileFolderPath, "testFile.txt"); public static testFileContents: number[] = [0x31, 0x32, 0x33, 0x34, 0x35, 0x61, 0x62, 0x63, 0x64, 0x65]; + public static testFileContentsStr: string = "12345abcde"; + + public static getTestFilePath: () => string = () => { + const fileUid: string = Uuidv4(); + return Path.join(TestConstants.testFileFolderPath, `testFile_${fileUid}.txt`); + } public static testStrings: {[key: string]: {bytes: number[], str: string}} = { Latin1: { From 21f6df01c4265743b95f11bf5f04d1f4bab8a195 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Thu, 9 Apr 2020 23:54:55 -0400 Subject: [PATCH 62/71] The rest of the unit tests for streams --- test/streamTest.ts | 105 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/test/streamTest.ts b/test/streamTest.ts index 44f3069e..bb2a2875 100644 --- a/test/streamTest.ts +++ b/test/streamTest.ts @@ -5,8 +5,8 @@ import {slow, suite, test, timeout} from "mocha-typescript"; import PropertyTests from "./utilities/propertyTests"; import TestConstants from "./testConstants"; +import {ByteVector} from "../src/byteVector"; import {SeekOrigin, Stream} from "../src/stream"; -import testFile from "./utilities/testFile"; // Setup chai Chai.use(ChaiAsPromised); @@ -356,6 +356,109 @@ class StreamTests { this.testWithFile(testAction, true); } + @test + public write_readOnly() { + const testAction = (testFilePath: string, stream: Stream) => { + // Arrange + const contents = new Uint8Array([0x01, 0x02, 0x03]); + + // Act / Assert + assert.throws(() => { stream.write(contents, 0, 1); }); + }; + this.testWithFile(testAction, false); + } + + @test + public write_writeAtBeginning() { + const testAction = (testFilePath: string, stream: Stream) => { + // Arrange + const buffer = new Uint8Array([0x01, 0x02, 0x03]); + + // Act + stream.write(buffer, 0, 3); + + // Assert + assert.strictEqual(stream.length, testFilePath.length); + assert.strictEqual(stream.position, 3); + + const contents = fs.readFileSync(testFilePath); + const expected = ByteVector.fromString(testFilePath); + expected.set(0, 0x01); + expected.set(1, 0x02); + expected.set(2, 0x03); + assert.deepStrictEqual(contents, expected.data); + }; + this.testWithFile(testAction, true); + } + + @test + public write_writeInMiddle() { + const testAction = (testFilePath: string, stream: Stream) => { + // Arrange + const buffer = new Uint8Array([0x01, 0x02, 0x03]); + stream.position = 3; + + // Act + stream.write(buffer, 0, 3); + + // Assert + assert.strictEqual(stream.length, testFilePath.length); + assert.strictEqual(stream.position, 6); + + const contents = fs.readFileSync(testFilePath); + const expected = ByteVector.fromString(testFilePath); + expected.set(3, 0x01); + expected.set(4, 0x02); + expected.set(5, 0x03); + assert.deepStrictEqual(contents, expected.data); + }; + this.testWithFile(testAction, true); + } + + @test + public write_writeAtEnd() { + const testAction = (testFilePath: string, stream: Stream) => { + // Arrange + const buffer = new Uint8Array([0x01, 0x02, 0x03]); + stream.position = stream.length; + + // Act + stream.write(buffer, 0, 3); + + // Assert + assert.strictEqual(stream.length, testFilePath.length + 3); + assert.strictEqual(stream.position, testFilePath.length + 3); + + const contents = fs.readFileSync(testFilePath); + const expected = ByteVector.fromString(testFilePath); + expected.addByteArray(buffer); + assert.deepStrictEqual(contents, expected.data); + }; + this.testWithFile(testAction, true); + } + + @test + public write_partialBuffer() { + const testAction = (testFilePath: string, stream: Stream) => { + // Arrange + const buffer = new Uint8Array([0x01, 0x02, 0x03, 0x04]); + + // Act + stream.write(buffer, 1, 2); + + // Assert + assert.strictEqual(stream.length, testFilePath.length); + assert.strictEqual(stream.position, 2); + + const contents = fs.readFileSync(testFilePath); + const expected = ByteVector.fromString(testFilePath); + expected.set(0, 0x02); + expected.set(1, 0x03); + assert.deepStrictEqual(contents, expected.data); + }; + this.testWithFile(testAction, true); + } + private testWithFile(testAction: (filePath: string, stream: Stream) => void, isWritable: boolean) { // Arrange const testFilePath = TestConstants.getTestFilePath(); From 2beedebdedd30a078ecedc6d07b14b3d60f03003 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Mon, 4 May 2020 23:13:38 -0400 Subject: [PATCH 63/71] Fuck yeah, I figured out that dumb ass insert logic. --- src/file.ts | 240 +++++---------- src/fileAbstraction.ts | 24 +- src/picture.ts | 12 +- src/utils.ts | 14 + test/fileAbstractionTests.ts | 61 ++++ test/fileTests.ts | 581 +++++++++++++++++++++++++++++++++++ test/utilities/testFile.ts | 3 +- test/utilities/testStream.ts | 30 +- 8 files changed, 783 insertions(+), 182 deletions(-) create mode 100644 test/fileAbstractionTests.ts create mode 100644 test/fileTests.ts diff --git a/src/file.ts b/src/file.ts index f357f342..59196c91 100644 --- a/src/file.ts +++ b/src/file.ts @@ -1,10 +1,9 @@ -import * as path from "path"; - import Properties from "./properties"; import {ByteVector} from "./byteVector"; import {IFileAbstraction, LocalFileAbstraction} from "./fileAbstraction"; import {IStream, SeekOrigin} from "./stream"; import {Tag, TagTypes} from "./tag"; +import {FileUtils, Guards} from "./utils"; /** * Specifies the options to use when reading the media. Can be treated as flags. @@ -83,25 +82,23 @@ export type FileTypeConstructor = new (abstraction: IFileAbstraction, style: Rea export abstract class File { // #region Member Variables - private static readonly _bufferSize: number = 1024; - private static readonly _fileTypes: {[mimeType: string]: FileTypeConstructor} = {}; - private static readonly _fileTypeResolvers: FileTypeResolver[] = []; + private static readonly _bufferSize: number = 64; + private static _fileTypes: {[mimeType: string]: FileTypeConstructor} = {}; + private static _fileTypeResolvers: FileTypeResolver[] = []; protected _fileAbstraction: IFileAbstraction; + protected _fileStream: IStream; // Not intended to be used by implementing classes protected _invariantEndPosition: number = -1; protected _invariantStartPosition: number = -1; protected _tagTypesOnDisk: TagTypes = TagTypes.None; private _corruptionReasons: string[] = []; - private _fileStream: IStream; private _mimeType: string; // #endregion protected constructor(file: IFileAbstraction | string) { - if (!file) { - throw new Error("Argument null: file not provided"); - } + Guards.truthy(file, "file"); this._fileAbstraction = typeof(file) === "string" ? new LocalFileAbstraction(file) : file; @@ -144,9 +141,11 @@ export abstract class File { } private static createInternal(abstraction: IFileAbstraction, mimeType: string, propertiesStyle: ReadStyle): File { + Guards.truthy(abstraction, "abstraction"); + // Step 1) Calculate the MimeType based on the extension of the file if it was not provided if (!mimeType) { - const ext = path.extname(abstraction.name); + const ext = FileUtils.getExtension(abstraction.name); mimeType = `taglib/${ext.toLowerCase()}`; } @@ -161,7 +160,7 @@ export abstract class File { // Step 3) Use the lookup table of MimeTypes => types and attempt to instantiate it const fileType = File._fileTypes[mimeType]; if (!fileType) { - throw new Error(`Unsupported format: mimetype for ${abstraction} (${mimeType}) is not supported`); + throw new Error(`Unsupported format: mimetype for ${abstraction.name} (${mimeType}) is not supported`); } return new fileType(abstraction, propertiesStyle); } @@ -225,7 +224,7 @@ export abstract class File { if (!this._fileStream) { return FileAccessMode.Closed; } - if (!this._fileStream.canWrite) { + if (this._fileStream.canWrite) { return FileAccessMode.Write; } return FileAccessMode.Read; @@ -249,7 +248,7 @@ export abstract class File { this._fileAbstraction.closeStream(this._fileStream); } - this._fileStream = null; + this._fileStream = undefined; // Open a new stream that corresponds to the access mode requested if (val === FileAccessMode.Read) { @@ -310,13 +309,9 @@ export abstract class File { * {@param mimeType}, it will be forcefully overridden. If `false`, an {@see Error} will be * thrown if a subclass already registered to the MimeType.} */ - public static AddFileType(mimeType: string, constructor: FileTypeConstructor, override: boolean = false): void { - if (!mimeType) { - throw new Error("Argument null: mimeType was not provided"); - } - if (!constructor) { - throw new Error("Argument null: constructor was not provided"); - } + public static addFileType(mimeType: string, constructor: FileTypeConstructor, override: boolean = false): void { + Guards.truthy(mimeType, "mimeType"); + Guards.truthy(constructor, "constructor"); if (!override && File._fileTypes[mimeType]) { throw new Error(`Invalid operation: MimeType ${mimeType} already has a file type associated with it`); } @@ -328,13 +323,19 @@ export abstract class File { * @param resolver Function to handle resolving a subclass of {@see File} from an * {@see IFileAbstraction} */ - public static AddFileTypeResolver(resolver: FileTypeResolver): void { - if (!resolver) { - throw new Error("Argument null: resolver was not provided"); - } + public static addFileTypeResolver(resolver: FileTypeResolver): void { + Guards.truthy(resolver, "resolver"); File._fileTypeResolvers.unshift(resolver); } + /** + * Used for clearing all the types and resolvers during unit testing + */ + public static clearFileTypesAndResolvers() { + File._fileTypeResolvers = []; + File._fileTypes = {}; + } + /** * Dispose the current instance. Equivalent to setting the mode to closed. */ @@ -353,12 +354,8 @@ export abstract class File { * @returns Index at which the value was found. If not found, `-1` is returned. */ public find(pattern: ByteVector, startPosition: number = 0, before?: ByteVector): number { - if (!pattern) { - throw new Error("Argument null: pattern was not provided"); - } - if (!Number.isSafeInteger(startPosition) || startPosition < 0) { - throw new Error("Argument out of range: startPosition is not a positive, safe integer"); - } + Guards.truthy(pattern, "pattern"); + Guards.uint(startPosition, "startPosition"); this.mode = FileAccessMode.Read; @@ -376,7 +373,7 @@ export abstract class File { let buffer = this.readBlock(File._bufferSize); for (buffer; buffer.length > 0; buffer = this.readBlock(File._bufferSize)) { const location = buffer.find(pattern); - if (!before) { + if (before) { const beforeLocation = buffer.find(before); if (beforeLocation < location) { return -1; @@ -438,35 +435,58 @@ export abstract class File { * replace is not a safe, positive number */ public insert(data: ByteVector, start: number, replace: number = 0): void { - if (!data) { - throw new Error("Argument null: data was not provided"); - } - if (!Number.isSafeInteger(start) || start < 0) { - throw new Error("Argument out of range: start must be a safe, positive integer"); + Guards.truthy(data, "data"); + Guards.uint(start, "start"); + Guards.uint(replace, "replace"); + + this.mode = FileAccessMode.Write; + this._fileStream.position = start; + + if (data.length === replace) { + // Case 1: We're writing the same number of bytes as we're replacing + // Simply overwrite the block + this.writeBlock(data); + return; } - if (!Number.isSafeInteger(replace) || replace < 0) { - throw new Error("Argument out of range: replace must be a safe, positive integer"); + if (data.length < replace) { + // Case 2: We're writing less bytes than we are replacing + // Write the block and then remove the rest of it + this.writeBlock(data); + this.removeBlock(start + data.length, replace - data.length); + return; } - this.insertInternal(data, data.length, start, replace); - } + // Case 3: We're writing more bytes than we're replacing + // We need to write out as much as we're replacing, then shuffle the rest to the end - /** - * Inserts a specified block-size into the file represented by the current instance, at a - * specified location. Former data at this location is not overwritten and may then contain - * random content. This method is useful to reserve some space in the file. - * @param size Number of bytes of the block to be inserted. Must be safe, positive integer. - * @param start Index into the file at which to insert the data. Must be safe positive integer. - */ - public insertBlank(size: number, start: number): void { - if (!Number.isSafeInteger(start) || start < 0) { - throw new Error("Argument out of range: start must be a safe, positive integer"); - } - if (!Number.isSafeInteger(size) || size < 0) { - throw new Error("Argument out of range: size must be a safe, positive integer"); + // Step 1: Write the number of bytes to replace + this._fileStream.write(data.data, 0, replace); + + // Step 2: Resize the file to fit all the new bytes + const bytesToAdd = data.length - replace; + this._fileStream.setLength(this._fileStream.length + bytesToAdd); + + // Step 3: Shuffle bytes to the end + const buffer = new Uint8Array(File.bufferSize); + const stopShufflingIndex = start + replace + bytesToAdd; + let shuffleIndex = this._fileStream.length; + while (shuffleIndex > stopShufflingIndex + bytesToAdd) { + const bytesToReplace = Math.min(shuffleIndex - stopShufflingIndex, File.bufferSize); + + // Fill up the buffer + this._fileStream.seek(shuffleIndex - bytesToReplace - bytesToAdd, SeekOrigin.Begin); + this._fileStream.read(buffer, 0, bytesToReplace); + + // Write the buffer back + this._fileStream.seek(shuffleIndex - bytesToReplace, SeekOrigin.Begin); + this._fileStream.write(buffer, 0, bytesToReplace); + + shuffleIndex -= bytesToReplace; } - this.insertInternal(undefined, size, start, 0); + // Step 4: Write the remainder of the data + this._fileStream.seek(start + replace, SeekOrigin.Begin); + this._fileStream.write(data.data, replace, data.length - replace); } /** @@ -486,11 +506,9 @@ export abstract class File { * @throws Error Thrown when {@param length} is not a positive, safe integer. */ public readBlock(length: number): ByteVector { - if (!Number.isSafeInteger(length) || length < 0) { - throw new Error("Argument out of range: length must be a positive, safe integer"); - } + Guards.uint(length, "length"); if (length === 0) { - return ByteVector.fromSize(0); + return ByteVector.empty(); } this.mode = FileAccessMode.Read; @@ -505,7 +523,7 @@ export abstract class File { needed -= count; } while (needed > 0 && count !== 0); - return ByteVector.fromByteArray(buffer); + return ByteVector.fromByteArray(buffer, read); } /** @@ -642,9 +660,7 @@ export abstract class File { * @throws Error Thrown when {@param data} is not provided. */ public writeBlock(data: ByteVector): void { - if (!data) { - throw new Error("Argument null: data was not provided"); - } + Guards.truthy(data, "data"); this.mode = FileAccessMode.Write; @@ -681,105 +697,5 @@ export abstract class File { this.mode = oldMode; } - /** - * Inserts a specified block into the file represented by the current instance at a specified - * location. - * @param data Data to insert into the file. If falsy, no data is written to the file and the - * block is just inserted without overwriting the former data at the given location. - * @param size Size of the block to insert in bytes - * @param start Index into the file at which to insert the data. - * @param replace Number of bytes to replace. Typically this is the original size of the data - * block so that a new block will replace the old one. - */ - private insertInternal(data: ByteVector, size: number, start: number, replace: number): void { - this.mode = FileAccessMode.Write; - - if (size === replace) { - if (data) { - this._fileStream.position = start; - this.writeBlock(data); - } - return; - } - if (size < replace) { - if (data) { - this._fileStream.position = start; - this.writeBlock(data); - } - this.removeBlock(start + size, replace - size); - return; - } - - // NOTE: I'm not 100% sure that this behaves the same in node land, but I'll preserve the - // notes and implementation from the original .NET implementation (which looks to be based - // on the original *original* TagLib implementation). If we need to revisit, we can do that - // later. - - // Woohoo! Faster (about 20%) than id3lib at last. I had to get hardcore and avoid - // TagLib's high level API for rendering just copying parts of the file that don't contain - // tag data. - // - // Now I'll explain the steps in this ugliness: - // - // First, make sure that we're working with a buffer that is longer or equal than the - // *difference* in the tag sizes, and that is a multiple of buffer_size. We want to avoid - // overwriting parts that aren't yet in memory, so this is necessary. - - let bufferLength = size - replace; - const mod = bufferLength % File._bufferSize; - if (mod !== 0) { - bufferLength += File._bufferSize - mod; - } - - // Set where to start the reading and writing - let readPosition = start + replace; - let writePosition = start; - - // This is basically a special case of the loop below. Here we're just doing the same steps - // as below, but since we aren't using the same buffer size -- instead we're using the tag - // size -- this has to be handled as a special case. We're also using File.writeBlock() - // just for the tag. That's a bit slower than using char*'s so, we're only doing it here. - this._fileStream.position = readPosition; - const aboutToOverwrite: Uint8Array = this.readBlock(bufferLength).data; - readPosition += bufferLength; - - if (data) { - this._fileStream.position = writePosition; - this.writeBlock(data); - } else if (start + size > this.length) { - this._fileStream.setLength(start + size); - } - writePosition += size; - - const buffer: Uint8Array = new Uint8Array(aboutToOverwrite); - - // Ok here's the main loop. We want to loop until the read fails, which means we hit the - // end of the file. - while (bufferLength !== 0) { - // Seek to the current read position and read the data that we're about to overwrite. - // Appropriately increment the readPosition. - this._fileStream.position = readPosition; - const bytesToRead = Math.min(bufferLength, aboutToOverwrite.length); - const bytesRead = this._fileStream.read(aboutToOverwrite, 0, bytesToRead); - readPosition += bufferLength; - - // Seek to the write position and write our buffer. Increment the write position. - this._fileStream.position = writePosition; - const bytesToWrite = Math.min(bufferLength, buffer.length); - this._fileStream.write(buffer, 0, bytesToWrite); - writePosition += bufferLength; - - // Make the current buffer the data that we read in the beginning - // NOTE: This is basically Array.copy, but javascript doesn't have that?? - for (let i = 0; i < bytesRead; i++) { - buffer[i] = aboutToOverwrite[i]; - } - - // Again, we need this for the last write. We don't want to write garbage to the end of - // the file, so we need to set the buffer size to the amound that we actually read. - bufferLength = bytesRead; - } - } - // #endregion } diff --git a/src/fileAbstraction.ts b/src/fileAbstraction.ts index 97c1e0ec..70416bbe 100644 --- a/src/fileAbstraction.ts +++ b/src/fileAbstraction.ts @@ -1,6 +1,10 @@ import {IStream, Stream} from "./stream"; import {Guards} from "./utils"; +/** + * This interface provides abstracted access to a file. It permits access to non-standard file + * systems and data retrieval methods. + */ export interface IFileAbstraction { /** * Name or identifier used by the implementation @@ -27,9 +31,18 @@ export interface IFileAbstraction { */ writeStream: IStream; + /** + * Closes a stream created by the current instance. + * @param stream Stream created by the current instance. + */ closeStream(stream: IStream): void; } +/** + * This class implements {@see IFileAbstraction} to provide support for accessing the local/ + * standard file. + * This class is used as the standard file abstraction throughout the library. + */ export class LocalFileAbstraction implements IFileAbstraction { /** * Contains the name used to open the file @@ -43,28 +56,27 @@ export class LocalFileAbstraction implements IFileAbstraction { */ public constructor(path: string) { Guards.truthy(path, "path"); - if (!path) { - throw new Error("Argument null: path was not provided"); - } this._name = path; } + /** @inheritDoc */ public get name(): string { return this._name; } + /** @inheritDoc */ public get readStream(): IStream { return Stream.createAsRead(this._name); } + /** @inheritDoc */ public get writeStream(): IStream { return Stream.createAsReadWrite(this._name); } + /** @inheritDoc */ public closeStream(stream: IStream): void { - if (!stream) { - throw new Error("Argument null: stream was not provided"); - } + Guards.truthy(stream, "stream"); stream.close(); } } diff --git a/src/picture.ts b/src/picture.ts index 82107dba..b52747c9 100644 --- a/src/picture.ts +++ b/src/picture.ts @@ -2,7 +2,7 @@ import * as path from "path"; import {ByteVector} from "./byteVector"; import {IFileAbstraction} from "./fileAbstraction"; -import {Guards} from "./utils"; +import {FileUtils} from "./utils"; export enum PictureType { /** @@ -358,15 +358,7 @@ export class Picture implements IPicture { return mimeType; } - let ext = path.extname(name); - if (!ext) { - ext = name.startsWith(".") ? name.substring(1) : name; - } else { - ext = ext.substring(1); - } - - ext = ext.toLowerCase(); - + const ext = FileUtils.getExtension(name); for (let i = 0; i < this._lutExtensionMime.length; i += 2) { if (this._lutExtensionMime[i] === ext) { mimeType = this._lutExtensionMime[i + 1]; diff --git a/src/utils.ts b/src/utils.ts index 775fb549..fa46dba2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import * as BigInt from "big-integer"; +import * as Path from "path"; export class Guards { public static betweenExclusive(value: number, minValue: number, maxValue: number, name: string): void { @@ -88,6 +89,19 @@ export class StringComparison { } } +export class FileUtils { + public static getExtension(name: string) { + let ext = Path.extname(name); + if (!ext) { + ext = name.startsWith(".") ? name.substring(1) : name; + } else { + ext = ext.substring(1); + } + + return ext.toLowerCase(); + } +} + export class ArrayUtils { public static remove(array: T[], callbackFn: (e: T, i: number) => boolean): void { let i = this.length; diff --git a/test/fileAbstractionTests.ts b/test/fileAbstractionTests.ts new file mode 100644 index 00000000..695ec1a6 --- /dev/null +++ b/test/fileAbstractionTests.ts @@ -0,0 +1,61 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import * as TypeMoq from "typemoq"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import TestConstants from "./testConstants"; +import {LocalFileAbstraction} from "../src/fileAbstraction"; +import {IStream} from "../src/stream"; + +// Setup Chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(slow(1000), timeout(3000)) +class LocalFileAbstractionTests { + @test + public constructor_test() { + // Act + const fileAbstraction = new LocalFileAbstraction(TestConstants.testFilePath); + + // Assert + assert.strictEqual(fileAbstraction.name, TestConstants.testFilePath); + + let readStream; + let writeStream; + try { + readStream = fileAbstraction.readStream; + assert.isFalse(readStream.canWrite); + + writeStream = fileAbstraction.writeStream; + assert.isTrue(writeStream.canWrite); + } finally { + if (readStream) { readStream.close(); } + if (writeStream) { writeStream.close(); } + } + } + + @test + public closeStream_invalidStream() { + // Arrange + const abstraction = new LocalFileAbstraction(TestConstants.testFilePath); + + // Act / Assert + assert.throws(() => abstraction.closeStream(null)); + assert.throws(() => abstraction.closeStream(undefined)); + } + + @test + public closeStream_closesStream() { + // Arrange + const abstraction = new LocalFileAbstraction(TestConstants.testFilePath); + const testStream = TypeMoq.Mock.ofType(); + testStream.setup((s) => s.close()); + + // Act + abstraction.closeStream(testStream.object); + + // Assert + testStream.verify((s) => s.close(), TypeMoq.Times.once()); + } +} diff --git a/test/fileTests.ts b/test/fileTests.ts new file mode 100644 index 00000000..b2df03a0 --- /dev/null +++ b/test/fileTests.ts @@ -0,0 +1,581 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import * as TypeMoq from "typemoq"; +import {slow, suite, test, timeout} from "mocha-typescript"; + +import Properties from "../src/properties"; +import PropertyTests from "./utilities/propertyTests"; +import TestConstants from "./testConstants"; +import {File, FileAccessMode, FileTypeResolver, ReadStyle} from "../src/file"; +import {IFileAbstraction} from "../src/fileAbstraction"; +import {Tag, TagTypes} from "../src/tag"; +import {IStream} from "../src/stream"; +import TestStream from "./utilities/testStream"; +import {ByteVector} from "../src/byteVector"; + +// Setup Chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite(slow(1000), timeout(3000)) +class FileTests { + private static readonly chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private readonly pattern1 = ByteVector.fromString("efg"); + private readonly pattern3 = ByteVector.fromString("bbbbba"); + + private readonly length1 = Math.floor(0.75 * File.bufferSize); // Smaller than buffer size + private readonly length2 = Math.floor(1.5 * File.bufferSize); // Bigger than buffer size + private readonly length3 = Math.floor(3.1 * File.bufferSize); // Even more bigger to catch special cases + + @test + public createFromAbstraction_invalidAbstraction() { + // Act / Assert + assert.throws(() => File.createFromAbstraction(undefined)); + assert.throws(() => File.createFromAbstraction(null)); + } + + @test + public createFromAbstraction_noMimetypeProvided() { + // Arrange + const mockAbstraction = TypeMoq.Mock.ofType(); + mockAbstraction.setup((a) => a.name).returns(() => "foobarbaz.qux"); + const mockFile = new TestFile(mockAbstraction.object); + + const testResolver = TypeMoq.Mock.ofType(); + testResolver.setup((r) => r( + TypeMoq.It.isAny(), + TypeMoq.It.isAnyString(), + TypeMoq.It.isAny() + )).returns(() => mockFile); + File.addFileTypeResolver(testResolver.object); + + try { + // Act + const file = File.createFromAbstraction(mockAbstraction.object); + + // Assert + assert.isOk(file); + assert.strictEqual(file, mockFile); + assert.strictEqual(file.fileAbstraction, mockAbstraction.object); + + assert.isNotNull(file.corruptionReasons); + assert.isEmpty(file.corruptionReasons); + assert.strictEqual(file.invariantEndPosition, -1); + assert.strictEqual(file.invariantStartPosition, -1); + assert.isFalse(file.isPossiblyCorrupt); + assert.isTrue(file.isWritable); + assert.strictEqual(file.length, 0); + assert.isUndefined(file.mimeType); + assert.strictEqual(file.mode, FileAccessMode.Closed); + assert.strictEqual(file.name, "foobarbaz.qux"); + assert.strictEqual(file.position, 0); + assert.strictEqual(file.tagTypes, TagTypes.None); + assert.strictEqual(file.tagTypesOnDisk, TagTypes.None); + + testResolver.verify((r) => r( + TypeMoq.It.isAny(), + TypeMoq.It.isValue("taglib/qux"), + TypeMoq.It.isValue(ReadStyle.Average) + ), TypeMoq.Times.once()); + } finally { + // Cleanup + File.clearFileTypesAndResolvers(); + } + } + + @test + public createFromAbstraction_mimetypeProvided() { + // Arrange + const mockAbstraction = TypeMoq.Mock.ofType(); + mockAbstraction.setup((a) => a.name).returns(() => "foobarbaz.qux"); + + File.addFileType("taglib/qux", TestFile); + + try { + // Act + const file = File.createFromAbstraction(mockAbstraction.object); + + // Assert + assert.isOk(file); + assert.strictEqual(file.fileAbstraction, mockAbstraction.object); + + assert.isNotNull(file.corruptionReasons); + assert.isEmpty(file.corruptionReasons); + assert.strictEqual(file.invariantEndPosition, -1); + assert.strictEqual(file.invariantStartPosition, -1); + assert.isFalse(file.isPossiblyCorrupt); + assert.isTrue(file.isWritable); + assert.strictEqual(file.length, 0); + assert.isUndefined(file.mimeType); + assert.strictEqual(file.mode, FileAccessMode.Closed); + assert.strictEqual(file.name, "foobarbaz.qux"); + assert.strictEqual(file.position, 0); + assert.strictEqual(file.tagTypes, TagTypes.None); + assert.strictEqual(file.tagTypesOnDisk, TagTypes.None); + } finally { + // Cleanup + File.clearFileTypesAndResolvers(); + } + } + + @test + public createFromPath_invalidParams() { + // Act / Assert + assert.throws(() => File.createFromPath(null)); + assert.throws(() => File.createFromPath(undefined)); + assert.throws(() => File.createFromPath("")); + } + + @test + public createFromPath_mimetypeNotProvided() { + // Arrange + const testResolver = TypeMoq.Mock.ofType(); + testResolver.setup((r) => r( + TypeMoq.It.isAny(), + TypeMoq.It.isAnyString(), + TypeMoq.It.isAny() + )).returns((a) => new TestFile(a)); + File.addFileTypeResolver(testResolver.object); + + try { + // Act + const file = File.createFromPath(TestConstants.testFilePath); + + // Assert + assert.isOk(file); + assert.isOk(file.fileAbstraction); + + assert.isNotNull(file.corruptionReasons); + assert.isEmpty(file.corruptionReasons); + assert.strictEqual(file.invariantEndPosition, -1); + assert.strictEqual(file.invariantStartPosition, -1); + assert.isFalse(file.isPossiblyCorrupt); + assert.isTrue(file.isWritable); + assert.strictEqual(file.length, 0); + assert.isUndefined(file.mimeType); + assert.strictEqual(file.mode, FileAccessMode.Closed); + assert.strictEqual(file.name, TestConstants.testFilePath); + assert.strictEqual(file.position, 0); + assert.strictEqual(file.tagTypes, TagTypes.None); + assert.strictEqual(file.tagTypesOnDisk, TagTypes.None); + + testResolver.verify((r) => r( + TypeMoq.It.isAny(), + TypeMoq.It.isValue("taglib/txt"), + TypeMoq.It.isValue(ReadStyle.Average) + ), TypeMoq.Times.once()); + } finally { + // Cleanup + File.clearFileTypesAndResolvers(); + } + } + + @test + public createFromPath_mimetypeProvided() { + // Arrange + File.addFileType("foo/bar", TestFile); + + try { + // Act + const file = File.createFromPath(TestConstants.testFilePath, "foo/bar"); + + // Assert + assert.isOk(file); + assert.isOk(file.fileAbstraction); + + assert.isNotNull(file.corruptionReasons); + assert.isEmpty(file.corruptionReasons); + assert.strictEqual(file.invariantEndPosition, -1); + assert.strictEqual(file.invariantStartPosition, -1); + assert.isFalse(file.isPossiblyCorrupt); + assert.isTrue(file.isWritable); + assert.strictEqual(file.length, 0); + assert.isUndefined(file.mimeType); + assert.strictEqual(file.mode, FileAccessMode.Closed); + assert.strictEqual(file.name, TestConstants.testFilePath); + assert.strictEqual(file.position, 0); + assert.strictEqual(file.tagTypes, TagTypes.None); + assert.strictEqual(file.tagTypesOnDisk, TagTypes.None); + } finally { + // Cleanup + File.clearFileTypesAndResolvers(); + } + } + + @test + public createInternal_unsupported() { + // Arrange + const testResolver = TypeMoq.Mock.ofType(); + testResolver.setup((r) => r( + TypeMoq.It.isAny(), + TypeMoq.It.isAnyString(), + TypeMoq.It.isAny() + )).returns(() => undefined); + File.addFileTypeResolver(testResolver.object); + + File.addFileType("foobar/baz", TestFile); + + try { + // Act / Assert + assert.throws(() => File.createFromPath(TestConstants.testFilePath, "foo/bar")); + } finally { + // Cleanup + File.clearFileTypesAndResolvers(); + } + } + + @test + public setMode_closedToClosed() { + const testAction = (f: File) => { + // Prereq + assert.strictEqual(f.mode, FileAccessMode.Closed); + + // Act / Assert + PropertyTests.propertyRoundTrip((v) => f.mode = v, () => f.mode, FileAccessMode.Closed); + }; + this.testWithMockAbstraction(testAction); + } + + @test + public setMode_closedToRead() { + const testAction = (f: TestFile) => { + // Act / Assert + PropertyTests.propertyRoundTrip((v) => f.mode = v, () => f.mode, FileAccessMode.Read); + + assert.isOk(f.stream); + assert.isFalse(f.stream.canWrite); + }; + this.testWithMockAbstraction(testAction); + } + + @test + public setMode_closedToWrite() { + const testAction = (f: TestFile) => { + // Act / Assert + PropertyTests.propertyRoundTrip((v) => f.mode = v, () => f.mode, FileAccessMode.Write); + + assert.isOk(f.stream); + assert.isTrue(f.stream.canWrite); + }; + this.testWithMockAbstraction(testAction); + } + + @test + public setMode_readToRead() { + const testAction = (f: TestFile, a: TypeMoq.IMock) => { + // Arrange + f.mode = FileAccessMode.Read; + const readStream = f.stream; + + // Act / Assert + PropertyTests.propertyRoundTrip((v) => f.mode = v, () => f.mode, FileAccessMode.Read); + assert.strictEqual(f.stream, readStream); + + a.verify((ma) => ma.closeStream(TypeMoq.It.isAny()), TypeMoq.Times.never()); + }; + this.testWithMockAbstraction(testAction); + } + + @test + public setMode_readToWrite() { + const testAction = (f: TestFile, a: TypeMoq.IMock) => { + // Arrange + f.mode = FileAccessMode.Read; + const readStream = f.stream; + + // Act + f.mode = FileAccessMode.Write; + + // Assert + assert.strictEqual(f.mode, FileAccessMode.Write); + assert.isOk(f.stream); + assert.isTrue(f.stream.canWrite); + assert.notStrictEqual(f.stream, readStream); + + a.verify((ma) => ma.closeStream(TypeMoq.It.isAny()), TypeMoq.Times.once()); + }; + this.testWithMockAbstraction(testAction); + } + + @test + public setMode_readToClosed() { + const testAction = (f: TestFile, a: TypeMoq.IMock) => { + // Arrange + f.mode = FileAccessMode.Read; + + // Act + f.mode = FileAccessMode.Closed; + + // Assert + assert.strictEqual(f.mode, FileAccessMode.Closed); + assert.isUndefined(f.stream); + + a.verify((ma) => ma.closeStream(TypeMoq.It.isAny()), TypeMoq.Times.once()); + }; + this.testWithMockAbstraction(testAction); + } + + @test + public setMode_writeToWrite() { + const testAction = (f: TestFile, a: TypeMoq.IMock) => { + // Arrange + f.mode = FileAccessMode.Write; + const writeStream = f.stream; + + // Act + f.mode = FileAccessMode.Write; + + // Assert + assert.strictEqual(f.mode, FileAccessMode.Write); + assert.isOk(f.stream); + assert.strictEqual(f.stream, writeStream); + + a.verify((ma) => ma.closeStream(TypeMoq.It.isAny()), TypeMoq.Times.never()); + }; + this.testWithMockAbstraction(testAction); + } + + @test + public setMode_writeToRead() { + const testAction = (f: TestFile, a: TypeMoq.IMock) => { + // Arrange + f.mode = FileAccessMode.Write; + const writeStream = f.stream; + + // Act + f.mode = FileAccessMode.Read; + + // Assert + assert.strictEqual(f.mode, FileAccessMode.Write); + assert.isOk(f.stream); + assert.strictEqual(f.stream, writeStream); + + a.verify((ma) => ma.closeStream(TypeMoq.It.isAny()), TypeMoq.Times.never()); + }; + this.testWithMockAbstraction(testAction); + } + + @test + public setMode_writeToClosed() { + const testAction = (f: TestFile, a: TypeMoq.IMock) => { + // Arrange + f.mode = FileAccessMode.Write; + + // Act + f.mode = FileAccessMode.Closed; + + // Assert + assert.strictEqual(f.mode, FileAccessMode.Closed); + assert.isUndefined(f.stream); + + a.verify((ma) => ma.closeStream(TypeMoq.It.isAny()), TypeMoq.Times.once()); + }; + this.testWithMockAbstraction(testAction); + } + + @test + public addFileType_invalidParameters() { + // Act / Assert + assert.throws(() => File.addFileType(undefined, TestFile, false)); + assert.throws(() => File.addFileType(null, TestFile, false)); + assert.throws(() => File.addFileType("", TestFile, false)); + assert.throws(() => File.addFileType("foo/bar", undefined, false)); + assert.throws(() => File.addFileType("foo/bar", null, false)); + } + + @test + public addFileType_overrideTurnedOff() { + // Arrange + File.addFileType("foo/bar", TestFile); + + try { + // Act / Assert + assert.throws(() => File.addFileType("foo/bar", TestFile, false)); + } finally { + File.clearFileTypesAndResolvers(); + } + } + + @test + public addFileTypeResolver_invalidParameters() { + // Act / Assert + assert.throws(() => { File.addFileTypeResolver(null); }); + assert.throws(() => { File.addFileTypeResolver(undefined); }); + } + + @test + public dispose() { + const testAction = (f: TestFile) => { + // Arrange + f.mode = FileAccessMode.Read; + + // Act + f.dispose(); + + // Assert + assert.strictEqual(f.mode, FileAccessMode.Closed); + }; + this.testWithMockAbstraction(testAction); + } + + @test + public find_file1() { + const testAction = (f: TestFile) => { + // f.insert(ByteVector.fromString("123"), 4, 2); + + // Act / Assert + assert.strictEqual(f.find(ByteVector.fromString("U")), FileTests.chars.indexOf("U")); + + assert.strictEqual(f.find(this.pattern1), -1); + assert.strictEqual(f.find(this.pattern1, 9), -1); + + f.insert(this.pattern1, this.length1 - 10, this.pattern1.length); // Insert closer to end + assert.strictEqual(f.find(this.pattern1), this.length1 - 10); + + f.insert(this.pattern1, this.length1 / 2, this.pattern1.length); // Insert somewhere in the middle + assert.strictEqual(f.find(this.pattern1), this.length1 / 2); + + f.insert(this.pattern1, 10, this.pattern1.length); // Insert closer to beginning + assert.strictEqual(f.find(this.pattern1), 10); + }; + this.testWithMemoryStream(testAction, this.length1); + } + + @test + public find_file2() { + const testAction = (f: TestFile) => { + // Act / Assert + assert.strictEqual(f.find(ByteVector.fromString("M")), FileTests.chars.indexOf("M")); + + assert.strictEqual(f.find(this.pattern1), -1); + assert.strictEqual(f.find(this.pattern1, 3), -1); + + f.insert(this.pattern1, this.length2 - 30, this.pattern1.length); + assert.strictEqual(f.find(this.pattern1), this.length2 - 30); + + f.insert(this.pattern1, this.length2 / 2, this.pattern1.length); + assert.strictEqual(f.find(this.pattern1), this.length2 / 2); + + f.insert(this.pattern1, 30); + assert.strictEqual(f.find(this.pattern1), 30); + + assert.strictEqual(f.find(this.pattern1, 2), 30); + assert.strictEqual(f.find(this.pattern1, 31), this.length2 / 2); + assert.strictEqual(f.find(this.pattern1, this.length2 / 2 + 3), this.length2 - 30); + }; + this.testWithMemoryStream(testAction, this.length2); + } + + @test + public find_file3() { + const testAction = (f: TestFile) => { + // Act / Assert + assert.strictEqual(f.find(this.pattern1), -1); + assert.strictEqual(f.find(this.pattern1, 13), -1); + + const bufferCross2 = 2 * File.bufferSize - Math.floor(this.pattern1.length / 2); + f.insert(this.pattern1, bufferCross2, this.pattern1.length); + assert.strictEqual(f.find(this.pattern1), bufferCross2); + + const bufferCross1 = File.bufferSize - Math.floor(this.pattern1.length / 2); + f.insert(this.pattern1, bufferCross1, this.pattern1.length); + assert.strictEqual(f.find(this.pattern1), bufferCross1); + + assert.strictEqual(f.find(this.pattern1, bufferCross1 + 1), bufferCross2); + + const bufferCross3 = File.bufferSize - 1; + f.insert(this.pattern3, bufferCross3 - 1, this.pattern3.length); + f.insert(this.pattern3, bufferCross3, this.pattern3.length); + assert.strictEqual(f.find(this.pattern3), bufferCross3); + }; + this.testWithMemoryStream(testAction, this.length3); + } + + private testWithMockAbstraction(testAction: (f: File, a: TypeMoq.IMock) => void) { + // Arrange + const testResolver = TypeMoq.Mock.ofType(); + testResolver.setup((r) => r( + TypeMoq.It.isAny(), + TypeMoq.It.isAnyString(), + TypeMoq.It.isAny() + )).returns((a) => new TestFile(a)); + File.addFileTypeResolver(testResolver.object); + + const mockReadStream = TypeMoq.Mock.ofType(); + mockReadStream.setup((rs) => rs.canWrite).returns(() => false); + + const mockWriteStream = TypeMoq.Mock.ofType(); + mockWriteStream.setup((ws) => ws.canWrite).returns(() => true); + + const mockAbstraction = TypeMoq.Mock.ofType(); + mockAbstraction.setup((a) => a.readStream).returns(() => mockReadStream.object); + mockAbstraction.setup((a) => a.writeStream).returns(() => mockWriteStream.object); + mockAbstraction.setup((a) => a.closeStream(TypeMoq.It.isAny())); + mockAbstraction.setup((a) => a.name).returns(() => TestConstants.testFilePath); + + const file = File.createFromAbstraction(mockAbstraction.object); + + try { + testAction(file, mockAbstraction); + } finally { + // Cleanup + File.clearFileTypesAndResolvers(); + } + } + + private testWithMemoryStream(testAction: (f: File) => void, length: number) { + // Arrange + const testResolver = TypeMoq.Mock.ofType(); + testResolver.setup((r) => r( + TypeMoq.It.isAny(), + TypeMoq.It.isAnyString(), + TypeMoq.It.isAny() + )).returns((a) => new TestFile(a)); + File.addFileTypeResolver(testResolver.object); + + const data = ByteVector.fromSize(length); + for (let i = 0; i < length; i++) { + data.set(i, FileTests.chars.charCodeAt(i % FileTests.chars.length)); + } + const stream = new TestStream(data, true); + + const mockAbstraction = TypeMoq.Mock.ofType(); + mockAbstraction.setup((a) => a.readStream).returns(() => stream); + mockAbstraction.setup((a) => a.writeStream).returns(() => stream); + mockAbstraction.setup((a) => a.closeStream(TypeMoq.It.isAny())); + mockAbstraction.setup((a) => a.name).returns(() => TestConstants.testFilePath); + + const file = File.createFromAbstraction(mockAbstraction.object); + + try { + testAction(file); + } finally { + // Cleanup + File.clearFileTypesAndResolvers(); + } + } +} + +class TestFile extends File { + constructor(abstraction: IFileAbstraction, style?: ReadStyle) { + super(abstraction); + } + + public get properties(): Properties { throw new Error("Not implemented"); } + + public get tag(): Tag { return undefined; } + + public get stream(): IStream { return this._fileStream; } + + public getTag(types: TagTypes, create: boolean): Tag { + throw new Error("Not implemented"); + } + + public removeTags(types: TagTypes): Tag { + throw new Error("Not implemented"); + } + + public save() { + throw new Error("Not implemented"); + } +} diff --git a/test/utilities/testFile.ts b/test/utilities/testFile.ts index 9ec18ac2..6ad76de5 100644 --- a/test/utilities/testFile.ts +++ b/test/utilities/testFile.ts @@ -1,5 +1,6 @@ -import {ByteVector} from "../../src/byteVector"; import * as TypeMoq from "typemoq"; + +import {ByteVector} from "../../src/byteVector"; import {File} from "../../src/file"; export default { diff --git a/test/utilities/testStream.ts b/test/utilities/testStream.ts index cc6319d1..52cc437e 100644 --- a/test/utilities/testStream.ts +++ b/test/utilities/testStream.ts @@ -2,9 +2,11 @@ import * as fs from "fs"; import {ByteVector} from "../../src/byteVector"; import {IStream, SeekOrigin} from "../../src/stream"; +const AB2B = require("arraybuffer-to-buffer"); + export default class TestStream implements IStream { - private readonly _data: ByteVector; private readonly _isWritable: boolean; + private _data: ByteVector; private _position: number; public constructor(bytesToReturn: ByteVector, isWritable: boolean) { @@ -24,6 +26,9 @@ export default class TestStream implements IStream { public get position(): number { return this._position; } + public set position(value: number) { + this._position = value; + } public close(): void { /* no op */ } @@ -53,11 +58,30 @@ export default class TestStream implements IStream { } public setLength(length: number): void { - throw new Error("Not Implemented"); + if (this.length < length) { + // Extend + this._data.addByteVector(ByteVector.fromSize(length - this.length)); + } else if (this.length > length) { + // Shrink + this._data = this._data.mid(0, length); + } + this._position = Math.max(this.length, this._position); } public write(buffer: fs.BinaryData, bufferOffset: number, length: number): number { - throw new Error("Not Implemented"); + if (!this._isWritable) { + throw new Error("Invalid operation: this stream is a read-only stream"); + } + + let bytesToWrite = ByteVector.fromByteArray(AB2B(buffer)); + bytesToWrite = bytesToWrite.mid(bufferOffset, length); + + if (this._position < this._data.length) { + this._data.removeRange(this._position, buffer.byteLength); + } + this._data.insertByteVector(this._position, bytesToWrite); + + return length; } } From db4d55b20b342b4c9417c515211e045008bd4759 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Thu, 7 May 2020 22:18:48 -0400 Subject: [PATCH 64/71] More tests for file class --- src/file.ts | 27 ++--- test/fileTests.ts | 197 ++++++++++++++++++++++++++++++++++- test/utilities/testStream.ts | 10 +- 3 files changed, 208 insertions(+), 26 deletions(-) diff --git a/src/file.ts b/src/file.ts index 59196c91..002f2bbd 100644 --- a/src/file.ts +++ b/src/file.ts @@ -82,7 +82,7 @@ export type FileTypeConstructor = new (abstraction: IFileAbstraction, style: Rea export abstract class File { // #region Member Variables - private static readonly _bufferSize: number = 64; + private static readonly _bufferSize: number = 1024; private static _fileTypes: {[mimeType: string]: FileTypeConstructor} = {}; private static _fileTypeResolvers: FileTypeResolver[] = []; @@ -345,7 +345,7 @@ export abstract class File { /** * Searches forward through a file for a specified pattern, starting at a specified offset. - * @param pattern Pattern to search for in the current instance. + * @param pattern Pattern to search for in the current instance. Must be smaller than the * @param startPosition Seek position to start searching. Must be positive, safe integer. * @param before Optional pattern that the searched for pattern must appear before. If this * pattern is found first, `-1` is returned. @@ -387,7 +387,7 @@ export abstract class File { // Ensure that we always rewind the stream a little so we never have a partial // match where our data exists betweenInclusive the end of read A and the start of read B. bufferOffset += File._bufferSize - pattern.length; - if (before != null && before.length > pattern.length) { + if (before && before.length > pattern.length) { bufferOffset -= before.length - pattern.length; } this._fileStream.position = bufferOffset; @@ -534,12 +534,8 @@ export abstract class File { * integer. */ public removeBlock(start: number, length: number): void { - if (!Number.isSafeInteger(start) || start < 0) { - throw new Error("Argument out of range: start must be a safe, positive integer."); - } - if (!Number.isSafeInteger(length)) { - throw new Error("Argument out of range: length must be a safe integer."); - } + Guards.uint(start, "start"); + Guards.int(length, "length"); if (length <= 0) { return; @@ -573,8 +569,9 @@ export abstract class File { public abstract removeTags(types: TagTypes): void; /** - * Searched backwards through a file for a specified patterh, starting at a specified offset - * @param pattern Pattern to search for in the current instance. + * Searched backwards through a file for a specified patterh, starting at a specified offset. + * @param pattern Pattern to search for in the current instance. Must be shorter than the + * {@see bufferSize} * @param startPosition Seek position from which to start searching. * @param after Pattern that the searched for pattern must appear after. If this pattern is * found first, `-1` is returned. @@ -583,12 +580,8 @@ export abstract class File { * @returns Index at which the value wa found. If not found, `-1` is returned. */ public rFind(pattern: ByteVector, startPosition: number = 0, after?: ByteVector): number { - if (!pattern) { - throw new Error("Argument null: pattern was not provided"); - } - if (!Number.isSafeInteger(startPosition) || startPosition < 0) { - throw new Error("Argument out of range: startPosition must be a safe, positive integer"); - } + Guards.truthy(pattern, "pattern"); + Guards.uint(startPosition, "startPosition"); this.mode = FileAccessMode.Read; diff --git a/test/fileTests.ts b/test/fileTests.ts index b2df03a0..b246507b 100644 --- a/test/fileTests.ts +++ b/test/fileTests.ts @@ -456,7 +456,7 @@ class FileTests { f.insert(this.pattern1, this.length2 / 2, this.pattern1.length); assert.strictEqual(f.find(this.pattern1), this.length2 / 2); - f.insert(this.pattern1, 30); + f.insert(this.pattern1, 30, this.pattern1.length); assert.strictEqual(f.find(this.pattern1), 30); assert.strictEqual(f.find(this.pattern1, 2), 30); @@ -491,6 +491,195 @@ class FileTests { this.testWithMemoryStream(testAction, this.length3); } + @test + public find_tooBig() { + const testAction = (f: TestFile) => { + // Arrange + const bytes = ByteVector.fromSize(File.bufferSize + 1); + + // Act / Assert + assert.strictEqual(f.find(bytes), -1); + }; + this.testWithMemoryStream(testAction, this.length1); + } + + @test + public insert_insertLessThanReplace() { + const testAction = (f: TestFile) => { + // Act + f.insert(this.pattern1, 2, this.pattern1.length + 1); + + // Assert + assert.strictEqual(f.find(this.pattern1), 2); + assert.strictEqual(f.length, this.length1 - 1); + }; + this.testWithMemoryStream(testAction, this.length1); + } + + @test + public markAsCorrupt_fileBecomesCorrupt() { + const testAction = (f: TestFile) => { + // Act + f.markAsCorrupt("foobarbaz"); + + // Assert + assert.isTrue(f.isPossiblyCorrupt); + assert.isNotNull(f.corruptionReasons); + assert.strictEqual(f.corruptionReasons.length, 1); + assert.sameDeepMembers(f.corruptionReasons, ["foobarbaz"]); + }; + this.testWithMemoryStream(testAction, 10); + } + + @test + public readBlock_lengthIsZero() { + const testAction = (f: TestFile) => { + // Act + const result = f.readBlock(0); + + // Assert + assert.strictEqual(result.length, 0); + }; + this.testWithMemoryStream(testAction, this.length1); + } + + @test + public rFind_file1() { + const testAction = (f: TestFile, d: ByteVector) => { + // Act / Assert + assert.strictEqual(f.rFind(ByteVector.fromString("U")), d.data.lastIndexOf("U".charCodeAt(0))); + + assert.strictEqual(f.rFind(this.pattern1), -1); + assert.strictEqual(f.rFind(this.pattern1, 9), -1); + + f.insert(this.pattern1, 10, this.pattern1.length); // Insert closer to beginning + assert.strictEqual(f.rFind(this.pattern1), 10); + + f.insert(this.pattern1, this.length1 / 2, this.pattern1.length); // Insert somewhere in the middle + assert.strictEqual(f.rFind(this.pattern1), this.length1 / 2); + + f.insert(this.pattern1, this.length1 - 10, this.pattern1.length); // Insert closer to end + assert.strictEqual(f.rFind(this.pattern1), this.length1 - 10); + }; + this.testWithMemoryStream(testAction, this.length1); + } + + @test + public rFind_file2() { + const testAction = (f: TestFile, d: ByteVector) => { + // Act / Assert + assert.strictEqual(f.rFind(ByteVector.fromString("M")), d.data.lastIndexOf("M".charCodeAt(0))); + + assert.strictEqual(f.rFind(this.pattern1), -1); + assert.strictEqual(f.rFind(this.pattern1, 3), -1); + + f.insert(this.pattern1, 30); + assert.strictEqual(f.rFind(this.pattern1), 30); + + f.insert(this.pattern1, this.length2 / 2, this.pattern1.length); + assert.strictEqual(f.rFind(this.pattern1), this.length2 / 2); + + f.insert(this.pattern1, this.length2 - 30, this.pattern1.length); + assert.strictEqual(f.rFind(this.pattern1), this.length2 - 30); + + assert.strictEqual(f.rFind(this.pattern1, 2), this.length2 - 30); + assert.strictEqual(f.rFind(this.pattern1, 31), this.length2 / 2); + assert.strictEqual(f.rFind(this.pattern1, this.length2 / 2 + 3), 30); + }; + this.testWithMemoryStream(testAction, this.length2); + } + + @test + public rFind_file3() { + const testAction = (f: TestFile) => { + // Act / Assert + assert.strictEqual(f.rFind(this.pattern1), -1); + assert.strictEqual(f.rFind(this.pattern1, 13), -1); + + const bufferCross1 = File.bufferSize - Math.floor(this.pattern1.length / 2); + const bufferCross2 = 2 * File.bufferSize - Math.floor(this.pattern1.length / 2); + + f.insert(this.pattern1, bufferCross1, this.pattern1.length); + assert.strictEqual(f.rFind(this.pattern1), bufferCross1); + assert.strictEqual(f.rFind(this.pattern1, bufferCross1 + this.pattern1.length), bufferCross1); + + f.insert(this.pattern1, bufferCross2, this.pattern1.length); + assert.strictEqual(f.rFind(this.pattern1), bufferCross2); + + const bufferCross3 = File.bufferSize - 1; + f.insert(this.pattern3, bufferCross3 - 1, this.pattern3.length); + f.insert(this.pattern3, bufferCross3, this.pattern3.length); + assert.strictEqual(f.rFind(this.pattern3), bufferCross3); + }; + this.testWithMemoryStream(testAction, this.length3); + } + + @test + public removeBlock_invalidParams() { + const testAction = (f: TestFile) => { + // Act / Assert + assert.throws(() => f.removeBlock(-1, 0)); + assert.throws(() => f.removeBlock(1.23, 0)); + assert.throws(() => f.removeBlock(Number.MAX_SAFE_INTEGER + 1, 0)); + assert.throws(() => f.removeBlock(0, 1.23)); + assert.throws(() => f.removeBlock(0, Number.MAX_SAFE_INTEGER + 1)); + assert.throws(() => f.removeBlock(0, Number.MIN_SAFE_INTEGER - 1)); + }; + this.testWithMemoryStream(testAction, 10); + } + + @test + public removeBlock_nothingToDo() { + const testAction = (f: TestFile, d: ByteVector) => { + // Act + f.removeBlock(0, 0); + f.removeBlock(0, -1); + + // Assert + // - Mode shouldn't have changed + assert.strictEqual(f.mode, FileAccessMode.Closed); + + // - Open the stream to verify it's contents didn't change + f.mode = FileAccessMode.Read; + assert.isTrue(ByteVector.equal(( f.stream).data, d)); + }; + this.testWithMemoryStream(testAction, 10); + } + + @test + public seek_closedAccess() { + const testAction = (f: TestFile) => { + // Arrange - get the test stream + f.mode = FileAccessMode.Read; + const stream = f.stream; + f.mode = FileAccessMode.Closed; + + // Act + f.seek(123); + + // Assert + assert.strictEqual(f.mode, FileAccessMode.Closed); + assert.strictEqual(stream.position, 0); + }; + this.testWithMemoryStream(testAction, 10); + } + + @test + public seek_validSeek() { + const testAction = (f: TestFile) => { + // Arrange - get the test stream + f.mode = FileAccessMode.Read; + const stream = f.stream; + + // Act + f.seek(5); + + // Assert + assert.strictEqual(stream.position, 5); + }; + this.testWithMemoryStream(testAction, 10); + } + private testWithMockAbstraction(testAction: (f: File, a: TypeMoq.IMock) => void) { // Arrange const testResolver = TypeMoq.Mock.ofType(); @@ -523,7 +712,7 @@ class FileTests { } } - private testWithMemoryStream(testAction: (f: File) => void, length: number) { + private testWithMemoryStream(testAction: (f: File, inputData: ByteVector) => void, length: number) { // Arrange const testResolver = TypeMoq.Mock.ofType(); testResolver.setup((r) => r( @@ -548,7 +737,7 @@ class FileTests { const file = File.createFromAbstraction(mockAbstraction.object); try { - testAction(file); + testAction(file, data); } finally { // Cleanup File.clearFileTypesAndResolvers(); @@ -557,7 +746,7 @@ class FileTests { } class TestFile extends File { - constructor(abstraction: IFileAbstraction, style?: ReadStyle) { + constructor(abstraction: IFileAbstraction) { super(abstraction); } diff --git a/test/utilities/testStream.ts b/test/utilities/testStream.ts index 52cc437e..58806218 100644 --- a/test/utilities/testStream.ts +++ b/test/utilities/testStream.ts @@ -10,7 +10,7 @@ export default class TestStream implements IStream { private _position: number; public constructor(bytesToReturn: ByteVector, isWritable: boolean) { - this._data = bytesToReturn; + this._data = ByteVector.fromByteVector(bytesToReturn); this._position = 0; this._isWritable = isWritable; } @@ -19,6 +19,8 @@ export default class TestStream implements IStream { return this._isWritable; } + public get data(): ByteVector { return this._data; } + public get length(): number { return this._data.length; } @@ -73,11 +75,9 @@ export default class TestStream implements IStream { throw new Error("Invalid operation: this stream is a read-only stream"); } - let bytesToWrite = ByteVector.fromByteArray(AB2B(buffer)); - bytesToWrite = bytesToWrite.mid(bufferOffset, length); - + const bytesToWrite = ByteVector.fromByteArray(AB2B(buffer.buffer.slice(bufferOffset, length))); if (this._position < this._data.length) { - this._data.removeRange(this._position, buffer.byteLength); + this._data.removeRange(this._position, bytesToWrite.length); } this._data.insertByteVector(this._position, bytesToWrite); From fd533eee4149591464538975210b5f3ce4a94521 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Sun, 17 May 2020 21:06:47 +0000 Subject: [PATCH 65/71] Update Test Definitions (#8) * First attempt at fixing for latest webstorm * Fixing suite definitions for latest webstorm Moving all unit tests to unit test folder * Adding webstorm unit test run definition * Fix unit test w/coverage npm script --- .gitignore | 2 +- .idea/runConfigurations/Tests__Unit.xml | 15 +++++++++++++++ .mocharc.json | 11 +++++++++++ appveyor.yml | 2 +- package.json | 4 ++-- {test => test-unit}/byteVectorConstructorTests.ts | 3 +-- {test => test-unit}/byteVectorConversionTests.ts | 3 +-- .../byteVectorStaticMethodTests.ts | 3 +-- {test => test-unit}/byteVectorVoidMethodTests.ts | 3 +-- {test => test-unit}/fileAbstractionTests.ts | 3 +-- {test => test-unit}/fileTests.ts | 3 +-- .../id3v2/attachmentsFrameTests.ts | 9 +++------ {test => test-unit}/id3v2/commentsFrameTests.ts | 9 +++------ .../id3v2/eventTimeCodeFrameTests.ts | 12 ++++-------- .../id3v2/frameConstructorTests.ts | 0 {test => test-unit}/id3v2/frameFactoryTests.ts | 3 +-- .../id3v2/frameIdentifiersTests.ts | 3 +-- {test => test-unit}/id3v2/frameTests.ts | 3 +-- {test => test-unit}/id3v2/id3v2TagTests.ts | 9 +++------ .../id3v2/musicCdIdentifierFrameTests.ts | 3 +-- {test => test-unit}/id3v2/playCountFrameTests.ts | 9 +++------ .../id3v2/popularimeterFrameTests.ts | 9 +++------ {test => test-unit}/id3v2/privateFrameTests.ts | 9 +++------ .../id3v2/relativeVolumeFrameTests.ts | 9 +++------ {test => test-unit}/id3v2/syncDataTests.ts | 3 +-- .../id3v2/synchronizedLyricsFrameTests.ts | 12 ++++-------- .../id3v2/tagExtendedHeaderTests.ts | 3 +-- {test => test-unit}/id3v2/tagFooterTests.ts | 9 +++------ {test => test-unit}/id3v2/tagHeaderTests.ts | 9 +++------ {test => test-unit}/id3v2/termsOfUseFrameTests.ts | 9 +++------ .../id3v2/textInformationFrameTests.ts | 9 +++------ .../id3v2/uniqueFileIdentifierFrameTests.ts | 9 +++------ {test => test-unit}/id3v2/unknownFrameTests.ts | 6 ++---- .../id3v2/unsynchronizedLyricsFrameTests.ts | 9 +++------ {test => test-unit}/id3v2/urlLinkFrameTests.ts | 9 +++------ .../id3v2/userTextInformationFrameTests.ts | 9 +++------ .../id3v2/userUrlLinkFrameTests.ts | 9 +++------ {test => test-unit}/pictureLazyTests.ts | 3 +-- {test => test-unit}/pictureTests.ts | 3 +-- {test => test-unit}/resources/testFile.txt | 0 {test => test-unit}/streamTest.ts | 3 +-- {test => test-unit}/testConstants.ts | 2 +- {test => test-unit}/tslint.json | 0 {test => test-unit}/utilities/propertyTests.ts | 0 {test => test-unit}/utilities/testFile.ts | 0 {test => test-unit}/utilities/testStream.ts | 0 test/mocha.opts | 5 ----- tsconfig.json | 3 +-- 48 files changed, 105 insertions(+), 158 deletions(-) create mode 100644 .idea/runConfigurations/Tests__Unit.xml create mode 100644 .mocharc.json rename {test => test-unit}/byteVectorConstructorTests.ts (99%) rename {test => test-unit}/byteVectorConversionTests.ts (99%) rename {test => test-unit}/byteVectorStaticMethodTests.ts (99%) rename {test => test-unit}/byteVectorVoidMethodTests.ts (99%) rename {test => test-unit}/fileAbstractionTests.ts (96%) rename {test => test-unit}/fileTests.ts (99%) rename {test => test-unit}/id3v2/attachmentsFrameTests.ts (98%) rename {test => test-unit}/id3v2/commentsFrameTests.ts (98%) rename {test => test-unit}/id3v2/eventTimeCodeFrameTests.ts (96%) rename {test => test-unit}/id3v2/frameConstructorTests.ts (100%) rename {test => test-unit}/id3v2/frameFactoryTests.ts (99%) rename {test => test-unit}/id3v2/frameIdentifiersTests.ts (98%) rename {test => test-unit}/id3v2/frameTests.ts (99%) rename {test => test-unit}/id3v2/id3v2TagTests.ts (99%) rename {test => test-unit}/id3v2/musicCdIdentifierFrameTests.ts (97%) rename {test => test-unit}/id3v2/playCountFrameTests.ts (95%) rename {test => test-unit}/id3v2/popularimeterFrameTests.ts (97%) rename {test => test-unit}/id3v2/privateFrameTests.ts (96%) rename {test => test-unit}/id3v2/relativeVolumeFrameTests.ts (98%) rename {test => test-unit}/id3v2/syncDataTests.ts (97%) rename {test => test-unit}/id3v2/synchronizedLyricsFrameTests.ts (98%) rename {test => test-unit}/id3v2/tagExtendedHeaderTests.ts (96%) rename {test => test-unit}/id3v2/tagFooterTests.ts (96%) rename {test => test-unit}/id3v2/tagHeaderTests.ts (97%) rename {test => test-unit}/id3v2/termsOfUseFrameTests.ts (97%) rename {test => test-unit}/id3v2/textInformationFrameTests.ts (98%) rename {test => test-unit}/id3v2/uniqueFileIdentifierFrameTests.ts (96%) rename {test => test-unit}/id3v2/unknownFrameTests.ts (96%) rename {test => test-unit}/id3v2/unsynchronizedLyricsFrameTests.ts (98%) rename {test => test-unit}/id3v2/urlLinkFrameTests.ts (97%) rename {test => test-unit}/id3v2/userTextInformationFrameTests.ts (96%) rename {test => test-unit}/id3v2/userUrlLinkFrameTests.ts (97%) rename {test => test-unit}/pictureLazyTests.ts (99%) rename {test => test-unit}/pictureTests.ts (98%) rename {test => test-unit}/resources/testFile.txt (100%) rename {test => test-unit}/streamTest.ts (99%) rename {test => test-unit}/testConstants.ts (96%) rename {test => test-unit}/tslint.json (100%) rename {test => test-unit}/utilities/propertyTests.ts (100%) rename {test => test-unit}/utilities/testFile.ts (100%) rename {test => test-unit}/utilities/testStream.ts (100%) delete mode 100644 test/mocha.opts diff --git a/.gitignore b/.gitignore index 78e75114..ce1c19ac 100644 --- a/.gitignore +++ b/.gitignore @@ -86,4 +86,4 @@ fabric.properties dist/** # Files created during test -test/resources/testFile_* \ No newline at end of file +test*/resources/**/testFile_* \ No newline at end of file diff --git a/.idea/runConfigurations/Tests__Unit.xml b/.idea/runConfigurations/Tests__Unit.xml new file mode 100644 index 00000000..f40d5ea4 --- /dev/null +++ b/.idea/runConfigurations/Tests__Unit.xml @@ -0,0 +1,15 @@ + + + project + + $PROJECT_DIR$/node_modules/mocha + $PROJECT_DIR$ + true + mocha-typescript + + DIRECTORY + $PROJECT_DIR$/test-unit + true + + + \ No newline at end of file diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 00000000..bd9c9fe2 --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,11 @@ +{ + "extension": ["ts"], + "recursive": true, + "require": [ + "ts-node/register", + "source-map-support/register" + ], + "slow": 1000, + "timout": 3000, + "ui": "mocha-typescript" +} \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 47b8ca7f..f71e596b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,7 +9,7 @@ build_script: - npm run build test_script: - - npm run test-with-coverage + - npm run test-unit-with-coverage after_test: - npm run publish-coverage \ No newline at end of file diff --git a/package.json b/package.json index 6ce2159d..48c456ca 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "build": "tsc -p ./", "madge": "node node_modules/madge/bin/cli.js --warning --circular --extension ts ./", "publish-coverage": "nyc report --reporter=text-lcov | coveralls", - "test": "mocha", - "test-with-coverage": "nyc mocha" + "test-unit": "mocha test-unit", + "test-unit-with-coverage": "nyc mocha test-unit" }, "dependencies": { "arraybuffer-to-buffer": "^0.0.5", diff --git a/test/byteVectorConstructorTests.ts b/test-unit/byteVectorConstructorTests.ts similarity index 99% rename from test/byteVectorConstructorTests.ts rename to test-unit/byteVectorConstructorTests.ts index 3ab60bad..0481c7cc 100644 --- a/test/byteVectorConstructorTests.ts +++ b/test-unit/byteVectorConstructorTests.ts @@ -16,8 +16,7 @@ const AB2B = require("arraybuffer-to-buffer"); Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class ByteVector_ConstructorTests { +@suite class ByteVector_ConstructorTests { private testArray = new Uint8Array([0x80, 0x08, 0x50]); private testByteVector = ByteVector.fromByteArray(this.testArray); diff --git a/test/byteVectorConversionTests.ts b/test-unit/byteVectorConversionTests.ts similarity index 99% rename from test/byteVectorConversionTests.ts rename to test-unit/byteVectorConversionTests.ts index 029d4385..1c8641cb 100644 --- a/test/byteVectorConversionTests.ts +++ b/test-unit/byteVectorConversionTests.ts @@ -7,8 +7,7 @@ import {ByteVector, StringType} from "../src/byteVector"; const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class ByteVector_ConversionTests { +@suite class ByteVector_ConversionTests { private readonly doublePositiveBV = ByteVector.fromByteArray( // 56.12 new Uint8Array([0x8F, 0xC2, 0xF5, 0x28, 0x5C, 0x0F, 0x4C, 0x40, 0xAA]) ); diff --git a/test/byteVectorStaticMethodTests.ts b/test-unit/byteVectorStaticMethodTests.ts similarity index 99% rename from test/byteVectorStaticMethodTests.ts rename to test-unit/byteVectorStaticMethodTests.ts index 743a6a4d..5648dccb 100644 --- a/test/byteVectorStaticMethodTests.ts +++ b/test-unit/byteVectorStaticMethodTests.ts @@ -8,8 +8,7 @@ import {ByteVector} from "../src/byteVector"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class ByteVector_StaticMethodTests { +@suite class ByteVector_StaticMethodTests { @test public add_invalidParameters() { // Arrange diff --git a/test/byteVectorVoidMethodTests.ts b/test-unit/byteVectorVoidMethodTests.ts similarity index 99% rename from test/byteVectorVoidMethodTests.ts rename to test-unit/byteVectorVoidMethodTests.ts index 4e55b10c..028b3c7f 100644 --- a/test/byteVectorVoidMethodTests.ts +++ b/test-unit/byteVectorVoidMethodTests.ts @@ -8,8 +8,7 @@ import {ByteVector} from "../src/byteVector"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class ByteVector_VoidMethodTests { +@suite class ByteVector_VoidMethodTests { private readonly vectorToAdd: ByteVector = ByteVector.fromByteArray( new Uint8Array([0xAA, 0xBB]) ); diff --git a/test/fileAbstractionTests.ts b/test-unit/fileAbstractionTests.ts similarity index 96% rename from test/fileAbstractionTests.ts rename to test-unit/fileAbstractionTests.ts index 695ec1a6..185c7203 100644 --- a/test/fileAbstractionTests.ts +++ b/test-unit/fileAbstractionTests.ts @@ -11,8 +11,7 @@ import {IStream} from "../src/stream"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(slow(1000), timeout(3000)) -class LocalFileAbstractionTests { +@suite class LocalFileAbstractionTests { @test public constructor_test() { // Act diff --git a/test/fileTests.ts b/test-unit/fileTests.ts similarity index 99% rename from test/fileTests.ts rename to test-unit/fileTests.ts index b246507b..8fbdc950 100644 --- a/test/fileTests.ts +++ b/test-unit/fileTests.ts @@ -17,8 +17,7 @@ import {ByteVector} from "../src/byteVector"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(slow(1000), timeout(3000)) -class FileTests { +@suite class FileTests { private static readonly chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; private readonly pattern1 = ByteVector.fromString("efg"); private readonly pattern3 = ByteVector.fromString("bbbbba"); diff --git a/test/id3v2/attachmentsFrameTests.ts b/test-unit/id3v2/attachmentsFrameTests.ts similarity index 98% rename from test/id3v2/attachmentsFrameTests.ts rename to test-unit/id3v2/attachmentsFrameTests.ts index ef7b93e3..47cbb2c7 100644 --- a/test/id3v2/attachmentsFrameTests.ts +++ b/test-unit/id3v2/attachmentsFrameTests.ts @@ -38,8 +38,7 @@ function getCustomTestFrame(data: ByteVector, desc: string, filename: string, mi return AttachmentFrame.fromPicture(mockPicture.object); } -@suite(timeout(3000), slow(1000)) -class Id3v2_AttachmentFrame_ConstructorTests extends FrameConstructorTests { +@suite class Id3v2_AttachmentFrame_ConstructorTests extends FrameConstructorTests { get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return AttachmentFrame.fromOffsetRawData; } @@ -295,8 +294,7 @@ class Id3v2_AttachmentFrame_ConstructorTests extends FrameConstructorTests { } } -@suite(slow(1000), timeout(3000)) -class Id3v2_AttachmentFrame_PropertyTests { +@suite class Id3v2_AttachmentFrame_PropertyTests { @test public data() { // Arrange @@ -405,8 +403,7 @@ class Id3v2_AttachmentFrame_PropertyTests { } } -@suite(slow(1000), timeout(3000)) -class Id3v2_AttachmentFrame_MethodTests { +@suite class Id3v2_AttachmentFrame_MethodTests { @test public clone_fromPictureUnread() { // Arrange diff --git a/test/id3v2/commentsFrameTests.ts b/test-unit/id3v2/commentsFrameTests.ts similarity index 98% rename from test/id3v2/commentsFrameTests.ts rename to test-unit/id3v2/commentsFrameTests.ts index f7f62522..f2d136f6 100644 --- a/test/id3v2/commentsFrameTests.ts +++ b/test-unit/id3v2/commentsFrameTests.ts @@ -30,8 +30,7 @@ function getTestFrame(): CommentsFrame { return CommentsFrame.fromRawData(data, 4); } -@suite(timeout(3000), slow(1000)) -class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { +@suite class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return CommentsFrame.fromOffsetRawData; } @@ -294,8 +293,7 @@ class Id3v2_CommentsFrame_ConstructorTests extends FrameConstructorTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_CommentsFrame_PropertyTests { +@suite class Id3v2_CommentsFrame_PropertyTests { @test public description() { const frame = getTestFrame(); @@ -343,8 +341,7 @@ class Id3v2_CommentsFrame_PropertyTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_CommentsFrame_MethodTests { +@suite class Id3v2_CommentsFrame_MethodTests { @test public find_falsyFrames() { // Act/Assert diff --git a/test/id3v2/eventTimeCodeFrameTests.ts b/test-unit/id3v2/eventTimeCodeFrameTests.ts similarity index 96% rename from test/id3v2/eventTimeCodeFrameTests.ts rename to test-unit/id3v2/eventTimeCodeFrameTests.ts index 69e3287f..c7bd7821 100644 --- a/test/id3v2/eventTimeCodeFrameTests.ts +++ b/test-unit/id3v2/eventTimeCodeFrameTests.ts @@ -16,8 +16,7 @@ import framePropertyTests from "../utilities/propertyTests"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class Id3v2_EventTimeCodeTests { +@suite class Id3v2_EventTimeCodeTests { @test public constructor_invalidTime() { // Act/Assert @@ -94,8 +93,7 @@ class Id3v2_EventTimeCodeTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_EventTimeCodeFrame_ConstructorTests extends FrameConstructorTests { +@suite class Id3v2_EventTimeCodeFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return EventTimeCodeFrame.fromOffsetRawData; } @@ -211,8 +209,7 @@ class Id3v2_EventTimeCodeFrame_ConstructorTests extends FrameConstructorTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_EventTimeCodeFrame_PropertyTests { +@suite class Id3v2_EventTimeCodeFrame_PropertyTests { @test public events() { // Arrange @@ -241,8 +238,7 @@ class Id3v2_EventTimeCodeFrame_PropertyTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_EventTimeCodeFrame_MethodTests { +@suite class Id3v2_EventTimeCodeFrame_MethodTests { @test public clone() { // Arrange diff --git a/test/id3v2/frameConstructorTests.ts b/test-unit/id3v2/frameConstructorTests.ts similarity index 100% rename from test/id3v2/frameConstructorTests.ts rename to test-unit/id3v2/frameConstructorTests.ts diff --git a/test/id3v2/frameFactoryTests.ts b/test-unit/id3v2/frameFactoryTests.ts similarity index 99% rename from test/id3v2/frameFactoryTests.ts rename to test-unit/id3v2/frameFactoryTests.ts index d206c0de..d8d8310a 100644 --- a/test/id3v2/frameFactoryTests.ts +++ b/test-unit/id3v2/frameFactoryTests.ts @@ -28,8 +28,7 @@ import {EventTimeCodeFrame} from "../../src/id3v2/frames/eventTimeCodeFrame"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(slow(1000), timeout(3000)) -class FrameFactoryTests { +@suite class FrameFactoryTests { @test public createFrame_invalidVersion() { // Arrange diff --git a/test/id3v2/frameIdentifiersTests.ts b/test-unit/id3v2/frameIdentifiersTests.ts similarity index 98% rename from test/id3v2/frameIdentifiersTests.ts rename to test-unit/id3v2/frameIdentifiersTests.ts index de61a582..2312308e 100644 --- a/test/id3v2/frameIdentifiersTests.ts +++ b/test-unit/id3v2/frameIdentifiersTests.ts @@ -9,8 +9,7 @@ import {FrameIdentifier, FrameIdentifiers} from "../../src/id3v2/frameIdentifier Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class FrameIdentifierTests { +@suite class FrameIdentifierTests { @test public isTextFrame_v2StartsWithT() { // Arrange diff --git a/test/id3v2/frameTests.ts b/test-unit/id3v2/frameTests.ts similarity index 99% rename from test/id3v2/frameTests.ts rename to test-unit/id3v2/frameTests.ts index 142b3463..47594d56 100644 --- a/test/id3v2/frameTests.ts +++ b/test-unit/id3v2/frameTests.ts @@ -44,8 +44,7 @@ class TestFrame extends Frame { } } -@suite(timeout(3000), slow(1000)) -class FrameTests { +@suite class FrameTests { // NOTE: We're mostly ignoring test cases that were already covered by concrete frame classes @test diff --git a/test/id3v2/id3v2TagTests.ts b/test-unit/id3v2/id3v2TagTests.ts similarity index 99% rename from test/id3v2/id3v2TagTests.ts rename to test-unit/id3v2/id3v2TagTests.ts index 8b2a7627..bbd84951 100644 --- a/test/id3v2/id3v2TagTests.ts +++ b/test-unit/id3v2/id3v2TagTests.ts @@ -39,8 +39,7 @@ function getTestTagHeader(version: number, flags: Id3v2TagHeaderFlags, tagSize: ); } -@suite(slow(1000), timeout(3000)) -class Id3v2_Tag_ConstructorTests { +@suite class Id3v2_Tag_ConstructorTests { @test public fromData_falsyData() { // Act / Assert @@ -264,8 +263,7 @@ class Id3v2_Tag_ConstructorTests { } } -@suite(slow(1000), timeout(3000)) -class Id3v2_Tag_PropertyTests { +@suite class Id3v2_Tag_PropertyTests { @test public flags() { // Arrange @@ -1478,8 +1476,7 @@ class Id3v2_Tag_PropertyTests { } } -@suite(slow(1000), timeout(3000)) -class Id3v2_Tag_MethodTests { +@suite class Id3v2_Tag_MethodTests { @test public clear() { // Arrange diff --git a/test/id3v2/musicCdIdentifierFrameTests.ts b/test-unit/id3v2/musicCdIdentifierFrameTests.ts similarity index 97% rename from test/id3v2/musicCdIdentifierFrameTests.ts rename to test-unit/id3v2/musicCdIdentifierFrameTests.ts index a0483711..2be02e10 100644 --- a/test/id3v2/musicCdIdentifierFrameTests.ts +++ b/test-unit/id3v2/musicCdIdentifierFrameTests.ts @@ -14,8 +14,7 @@ import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class Id3v2_MusicCdIdentifierFrameTests extends FrameConstructorTests { +@suite class Id3v2_MusicCdIdentifierFrameTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return MusicCdIdentifierFrame.fromOffsetRawData; } diff --git a/test/id3v2/playCountFrameTests.ts b/test-unit/id3v2/playCountFrameTests.ts similarity index 95% rename from test/id3v2/playCountFrameTests.ts rename to test-unit/id3v2/playCountFrameTests.ts index cd3b8531..76b05e7b 100644 --- a/test/id3v2/playCountFrameTests.ts +++ b/test-unit/id3v2/playCountFrameTests.ts @@ -15,8 +15,7 @@ import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class Id3v2_PlayCountFrame_ConstructorTests extends FrameConstructorTests { +@suite class Id3v2_PlayCountFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return PlayCountFrame.fromOffsetRawData; } @@ -112,8 +111,7 @@ class Id3v2_PlayCountFrame_ConstructorTests extends FrameConstructorTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_PlayCountFrame_PropertyTests { +@suite class Id3v2_PlayCountFrame_PropertyTests { @test public playCount() { // Arrange @@ -129,8 +127,7 @@ class Id3v2_PlayCountFrame_PropertyTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_PlayCountFrame_MethodTests { +@suite class Id3v2_PlayCountFrame_MethodTests { @test public clone() { // Arrange diff --git a/test/id3v2/popularimeterFrameTests.ts b/test-unit/id3v2/popularimeterFrameTests.ts similarity index 97% rename from test/id3v2/popularimeterFrameTests.ts rename to test-unit/id3v2/popularimeterFrameTests.ts index d97941fe..03cafd6f 100644 --- a/test/id3v2/popularimeterFrameTests.ts +++ b/test-unit/id3v2/popularimeterFrameTests.ts @@ -15,8 +15,7 @@ import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class Id3v2_PopularimeterFrame_ConstructorTests extends FrameConstructorTests { +@suite class Id3v2_PopularimeterFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return PopularimeterFrame.fromOffsetRawData; } @@ -177,8 +176,7 @@ class Id3v2_PopularimeterFrame_ConstructorTests extends FrameConstructorTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_PopularimeterFrame_PropertyTests { +@suite class Id3v2_PopularimeterFrame_PropertyTests { @test public playCount() { // Arrange @@ -221,8 +219,7 @@ class Id3v2_PopularimeterFrame_PropertyTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_PopularimeterFrame_MethodTests { +@suite class Id3v2_PopularimeterFrame_MethodTests { @test public find_falsyFrames() { // Act / Assert diff --git a/test/id3v2/privateFrameTests.ts b/test-unit/id3v2/privateFrameTests.ts similarity index 96% rename from test/id3v2/privateFrameTests.ts rename to test-unit/id3v2/privateFrameTests.ts index ce2b0496..f5536aa9 100644 --- a/test/id3v2/privateFrameTests.ts +++ b/test-unit/id3v2/privateFrameTests.ts @@ -14,8 +14,7 @@ import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class Id3v2_PrivateFrame_ConstructorTests extends FrameConstructorTests { +@suite class Id3v2_PrivateFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return PrivateFrame.fromOffsetRawData; } @@ -112,8 +111,7 @@ class Id3v2_PrivateFrame_ConstructorTests extends FrameConstructorTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_PrivateFrame_PropertyTests { +@suite class Id3v2_PrivateFrame_PropertyTests { @test public privateData() { // Arrange @@ -128,8 +126,7 @@ class Id3v2_PrivateFrame_PropertyTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_PrivateFrame_MethodTests { +@suite class Id3v2_PrivateFrame_MethodTests { @test public find_noFrames() { // Arrange diff --git a/test/id3v2/relativeVolumeFrameTests.ts b/test-unit/id3v2/relativeVolumeFrameTests.ts similarity index 98% rename from test/id3v2/relativeVolumeFrameTests.ts rename to test-unit/id3v2/relativeVolumeFrameTests.ts index e4ec15e0..7b311f05 100644 --- a/test/id3v2/relativeVolumeFrameTests.ts +++ b/test-unit/id3v2/relativeVolumeFrameTests.ts @@ -15,8 +15,7 @@ import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class Id3v2_RelativeVolumeChannelData { +@suite class Id3v2_RelativeVolumeChannelData { @test public peakBits_setInvalidValues() { // Arrange @@ -281,8 +280,7 @@ class Id3v2_RelativeVolumeChannelData { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_RelativeVolumeFrame_ConstructorTests extends ConstructorTests { +@suite class Id3v2_RelativeVolumeFrame_ConstructorTests extends ConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return RelativeVolumeFrame.fromOffsetRawData; } @@ -390,8 +388,7 @@ class Id3v2_RelativeVolumeFrame_ConstructorTests extends ConstructorTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_RelativeVolumeFrameMethodTests { +@suite class Id3v2_RelativeVolumeFrameMethodTests { @test public find_falsyFrames() { // Act / Assert diff --git a/test/id3v2/syncDataTests.ts b/test-unit/id3v2/syncDataTests.ts similarity index 97% rename from test/id3v2/syncDataTests.ts rename to test-unit/id3v2/syncDataTests.ts index 5b579ae4..b4ec6a95 100644 --- a/test/id3v2/syncDataTests.ts +++ b/test-unit/id3v2/syncDataTests.ts @@ -10,8 +10,7 @@ import {ByteVector} from "../../src/byteVector"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class Id3v2_SyncDataTests { +@suite class Id3v2_SyncDataTests { @test public fromUint_InvalidValues() { // Act/Assert diff --git a/test/id3v2/synchronizedLyricsFrameTests.ts b/test-unit/id3v2/synchronizedLyricsFrameTests.ts similarity index 98% rename from test/id3v2/synchronizedLyricsFrameTests.ts rename to test-unit/id3v2/synchronizedLyricsFrameTests.ts index 4b08eff8..8e518b36 100644 --- a/test/id3v2/synchronizedLyricsFrameTests.ts +++ b/test-unit/id3v2/synchronizedLyricsFrameTests.ts @@ -16,8 +16,7 @@ import {SynchronizedTextType, TimestampFormat} from "../../src/id3v2/utilTypes"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class Id3v2_SynchronizedTextTests { +@suite class Id3v2_SynchronizedTextTests { @test public synchronizedText_construct() { // Act @@ -46,8 +45,7 @@ class Id3v2_SynchronizedTextTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTests { +@suite class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return SynchronizedLyricsFrame.fromOffsetRawData; } @@ -338,8 +336,7 @@ class Id3v2_SynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTes } } -@suite(timeout(3000), slow(1000)) -class Id3v2_SynchronizedLyricsFrame_PropertyTests { +@suite class Id3v2_SynchronizedLyricsFrame_PropertyTests { @test public description() { // Arrange @@ -419,8 +416,7 @@ class Id3v2_SynchronizedLyricsFrame_PropertyTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_SynchronizedLyricsFrame_MethodTests { +@suite class Id3v2_SynchronizedLyricsFrame_MethodTests { @test public find_falsyFrames() { // Act / Assert diff --git a/test/id3v2/tagExtendedHeaderTests.ts b/test-unit/id3v2/tagExtendedHeaderTests.ts similarity index 96% rename from test/id3v2/tagExtendedHeaderTests.ts rename to test-unit/id3v2/tagExtendedHeaderTests.ts index 247bda05..e3d8d558 100644 --- a/test/id3v2/tagExtendedHeaderTests.ts +++ b/test-unit/id3v2/tagExtendedHeaderTests.ts @@ -9,8 +9,7 @@ import {ByteVector} from "../../src/byteVector"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class Id3v2_TagExtendedHeaderTests { +@suite class Id3v2_TagExtendedHeaderTests { @test public fromData_falsyData() { // Act/Assert diff --git a/test/id3v2/tagFooterTests.ts b/test-unit/id3v2/tagFooterTests.ts similarity index 96% rename from test/id3v2/tagFooterTests.ts rename to test-unit/id3v2/tagFooterTests.ts index 0a74b0ee..b404372e 100644 --- a/test/id3v2/tagFooterTests.ts +++ b/test-unit/id3v2/tagFooterTests.ts @@ -23,8 +23,7 @@ const getTestFooter = (majorVersion: number, minorVersion: number, flags: Id3v2T return Id3v2TagFooter.fromData(data); }; -@suite(timeout(3000), slow(1000)) -class Id3v2_TagFooter_ConstructorTests { +@suite class Id3v2_TagFooter_ConstructorTests { @test public fromData_falsyData() { // Act/Assert @@ -106,8 +105,7 @@ class Id3v2_TagFooter_ConstructorTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_TagFooter_PropertyTests { +@suite class Id3v2_TagFooter_PropertyTests { @test public getCompleteTagSize() { // Arrange @@ -216,8 +214,7 @@ class Id3v2_TagFooter_PropertyTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_TagFooter_RenderTests { +@suite class Id3v2_TagFooter_RenderTests { @test public render() { // Arrange diff --git a/test/id3v2/tagHeaderTests.ts b/test-unit/id3v2/tagHeaderTests.ts similarity index 97% rename from test/id3v2/tagHeaderTests.ts rename to test-unit/id3v2/tagHeaderTests.ts index 5558117f..ce828d6d 100644 --- a/test/id3v2/tagHeaderTests.ts +++ b/test-unit/id3v2/tagHeaderTests.ts @@ -22,8 +22,7 @@ const getTestHeader = (majorVersion: number, minorVersion: number, flags: Id3v2T return Id3v2TagHeader.fromData(data); }; -@suite(timeout(3000), slow(1000)) -class Id3v2_TagHeader_ConstructorTests { +@suite class Id3v2_TagHeader_ConstructorTests { @test public falsyData() { // Act/Assert @@ -135,8 +134,7 @@ class Id3v2_TagHeader_ConstructorTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_TagHeader_PropertyTests { +@suite class Id3v2_TagHeader_PropertyTests { @test public getFileIdentifier() { // Act @@ -346,8 +344,7 @@ class Id3v2_TagHeader_PropertyTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_TagHeader_RenderTests { +@suite class Id3v2_TagHeader_RenderTests { @test public render() { // Arrange diff --git a/test/id3v2/termsOfUseFrameTests.ts b/test-unit/id3v2/termsOfUseFrameTests.ts similarity index 97% rename from test/id3v2/termsOfUseFrameTests.ts rename to test-unit/id3v2/termsOfUseFrameTests.ts index 28be9d00..8fe36bdd 100644 --- a/test/id3v2/termsOfUseFrameTests.ts +++ b/test-unit/id3v2/termsOfUseFrameTests.ts @@ -15,8 +15,7 @@ import {FrameIdentifiers} from "../../src/id3v2/frameIdentifiers"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class Id3v2_TermsOfUseFrame_ConstructorTests extends FrameConstructorTests { +@suite class Id3v2_TermsOfUseFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return TermsOfUseFrame.fromOffsetRawData; } @@ -122,8 +121,7 @@ class Id3v2_TermsOfUseFrame_ConstructorTests extends FrameConstructorTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_TermsOfUseFrame_PropertyTests { +@suite class Id3v2_TermsOfUseFrame_PropertyTests { @test public language() { // Arrange @@ -162,8 +160,7 @@ class Id3v2_TermsOfUseFrame_PropertyTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_TermsOfUseFrame_MethodTests { +@suite class Id3v2_TermsOfUseFrame_MethodTests { @test public find_falsyFrames() { // Act/Assert diff --git a/test/id3v2/textInformationFrameTests.ts b/test-unit/id3v2/textInformationFrameTests.ts similarity index 98% rename from test/id3v2/textInformationFrameTests.ts rename to test-unit/id3v2/textInformationFrameTests.ts index 4dfefce0..a807e4c1 100644 --- a/test/id3v2/textInformationFrameTests.ts +++ b/test-unit/id3v2/textInformationFrameTests.ts @@ -29,8 +29,7 @@ function getTestFrame(): TextInformationFrame { return TextInformationFrame.fromRawData(data, 4); } -@suite(timeout(3000), slow(1000)) -class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests { +@suite class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return TextInformationFrame.fromOffsetRawData; } @@ -218,8 +217,7 @@ class Id3v2_TextInformationFrame_ConstructorTests extends FrameConstructorTests } } -@suite(timeout(3000), slow(1000)) -class Id3v2_TextInformationFrame_PropertyTests { +@suite class Id3v2_TextInformationFrame_PropertyTests { @test public getText() { // Arrange @@ -271,8 +269,7 @@ class Id3v2_TextInformationFrame_PropertyTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_TextInformationFrame_MethodTests { +@suite class Id3v2_TextInformationFrame_MethodTests { @test public clone_returnsCopy() { // Arrange diff --git a/test/id3v2/uniqueFileIdentifierFrameTests.ts b/test-unit/id3v2/uniqueFileIdentifierFrameTests.ts similarity index 96% rename from test/id3v2/uniqueFileIdentifierFrameTests.ts rename to test-unit/id3v2/uniqueFileIdentifierFrameTests.ts index 8f64b619..fc14f1ad 100644 --- a/test/id3v2/uniqueFileIdentifierFrameTests.ts +++ b/test-unit/id3v2/uniqueFileIdentifierFrameTests.ts @@ -18,8 +18,7 @@ const assert = Chai.assert; const testIdentifier = ByteVector.fromString("foobarbaz"); const testOwner = "http://github.com/benrr101/node-taglib-sharp"; -@suite(timeout(3000), slow(1000)) -class Id3v2_UniqueFileIdentifierFrame_ConstructorTests extends FrameConstructorTests { +@suite class Id3v2_UniqueFileIdentifierFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return UniqueFileIdentifierFrame.fromOffsetRawData; } @@ -194,8 +193,7 @@ class Id3v2_UniqueFileIdentifierFrame_ConstructorTests extends FrameConstructorT } } -@suite(timeout(3000), slow(1000)) -class Id3v2_UniqueFileIdentifierFrame_PropertyTests { +@suite class Id3v2_UniqueFileIdentifierFrame_PropertyTests { @test public setIdentifier_tooLong_throws() { // Arrange @@ -216,8 +214,7 @@ class Id3v2_UniqueFileIdentifierFrame_PropertyTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_UniqueFileIdentifierFrame_MethodTests { +@suite class Id3v2_UniqueFileIdentifierFrame_MethodTests { @test public find_falsyFrames_throws() { // Act/Assert diff --git a/test/id3v2/unknownFrameTests.ts b/test-unit/id3v2/unknownFrameTests.ts similarity index 96% rename from test/id3v2/unknownFrameTests.ts rename to test-unit/id3v2/unknownFrameTests.ts index ffc4f314..99d03ad9 100644 --- a/test/id3v2/unknownFrameTests.ts +++ b/test-unit/id3v2/unknownFrameTests.ts @@ -13,8 +13,7 @@ import {FrameIdentifier, FrameIdentifiers} from "../../src/id3v2/frameIdentifier Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class Id3v2_UnknownFrame_ConstructorTests extends FrameConstructorTests { +@suite class Id3v2_UnknownFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return UnknownFrame.fromOffsetRawData; } @@ -115,8 +114,7 @@ class Id3v2_UnknownFrame_ConstructorTests extends FrameConstructorTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_UnknownFrame_MethodTests { +@suite class Id3v2_UnknownFrame_MethodTests { @test public clone_returnsCopy() { // Arrange diff --git a/test/id3v2/unsynchronizedLyricsFrameTests.ts b/test-unit/id3v2/unsynchronizedLyricsFrameTests.ts similarity index 98% rename from test/id3v2/unsynchronizedLyricsFrameTests.ts rename to test-unit/id3v2/unsynchronizedLyricsFrameTests.ts index e173ed28..5ccc3c1f 100644 --- a/test/id3v2/unsynchronizedLyricsFrameTests.ts +++ b/test-unit/id3v2/unsynchronizedLyricsFrameTests.ts @@ -33,8 +33,7 @@ const getTestUnsynchronizedLyricsFrame = (): UnsynchronizedLyricsFrame => { return UnsynchronizedLyricsFrame.fromRawData(frameData, 4); }; -@suite(timeout(3000), slow(1000)) -class Id3v2_UnsynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTests { +@suite class Id3v2_UnsynchronizedLyricsFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return UnsynchronizedLyricsFrame.fromOffsetRawData; } @@ -151,8 +150,7 @@ class Id3v2_UnsynchronizedLyricsFrame_ConstructorTests extends FrameConstructorT } } -@suite(timeout(3000), slow(1000)) -class Id3v2_UnsynchronizedLyricsFrame_PropertyTests { +@suite class Id3v2_UnsynchronizedLyricsFrame_PropertyTests { @test public description() { // Arrange @@ -208,8 +206,7 @@ class Id3v2_UnsynchronizedLyricsFrame_PropertyTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_UnsynchronizedLyricsFrame_MethodTests { +@suite class Id3v2_UnsynchronizedLyricsFrame_MethodTests { @test public find_falsyFrames_throws() { // Act/Assert diff --git a/test/id3v2/urlLinkFrameTests.ts b/test-unit/id3v2/urlLinkFrameTests.ts similarity index 97% rename from test/id3v2/urlLinkFrameTests.ts rename to test-unit/id3v2/urlLinkFrameTests.ts index 0fa75711..dc7307a7 100644 --- a/test/id3v2/urlLinkFrameTests.ts +++ b/test-unit/id3v2/urlLinkFrameTests.ts @@ -33,8 +33,7 @@ const getTestUrlLinkFrame = (): UrlLinkFrame => { return UrlLinkFrame.fromRawData(frameData, 4); }; -@suite(timeout(3000), slow(1000)) -class Id3v2_UrlLinkFrame_ConstructorTests extends FrameConstructorTests { +@suite class Id3v2_UrlLinkFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return UrlLinkFrame.fromOffsetRawData; } @@ -148,8 +147,7 @@ class Id3v2_UrlLinkFrame_ConstructorTests extends FrameConstructorTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_UrlLinkFrame_PropertyTests { +@suite class Id3v2_UrlLinkFrame_PropertyTests { @test public getText_outputIsClone() { // Arrange @@ -208,8 +206,7 @@ class Id3v2_UrlLinkFrame_PropertyTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_UrlLinkFrame_MethodTests { +@suite class Id3v2_UrlLinkFrame_MethodTests { @test public findUrlLinkFrame_falsyFrames_throws(): void { // Act/Assert diff --git a/test/id3v2/userTextInformationFrameTests.ts b/test-unit/id3v2/userTextInformationFrameTests.ts similarity index 96% rename from test/id3v2/userTextInformationFrameTests.ts rename to test-unit/id3v2/userTextInformationFrameTests.ts index e979f152..fff03422 100644 --- a/test/id3v2/userTextInformationFrameTests.ts +++ b/test-unit/id3v2/userTextInformationFrameTests.ts @@ -28,8 +28,7 @@ function getTestFrame(): UserTextInformationFrame { return UserTextInformationFrame.fromRawData(data, 4); } -@suite(timeout(3000), slow(1000)) -class Id3v2_UserInformationFrame_ConstructorTests extends FrameConstructorTests { +@suite class Id3v2_UserInformationFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return UserTextInformationFrame.fromOffsetRawData; } @@ -114,8 +113,7 @@ class Id3v2_UserInformationFrame_ConstructorTests extends FrameConstructorTests } } -@suite(timeout(3000), slow(1000)) -class Id3v2_UserInformationFrame_PropertyTests { +@suite class Id3v2_UserInformationFrame_PropertyTests { @test public setDescription() { // Arrange @@ -157,8 +155,7 @@ class Id3v2_UserInformationFrame_PropertyTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_UserTextInformationFrame_MethodTests { +@suite class Id3v2_UserTextInformationFrame_MethodTests { @test public findUserTextInformationFrame_falsyFrames() { // Act/Assert diff --git a/test/id3v2/userUrlLinkFrameTests.ts b/test-unit/id3v2/userUrlLinkFrameTests.ts similarity index 97% rename from test/id3v2/userUrlLinkFrameTests.ts rename to test-unit/id3v2/userUrlLinkFrameTests.ts index 17ae0f01..797f7fcc 100644 --- a/test/id3v2/userUrlLinkFrameTests.ts +++ b/test-unit/id3v2/userUrlLinkFrameTests.ts @@ -32,8 +32,7 @@ const getTestUserUrlLinkFrame = (): UserUrlLinkFrame => { return UserUrlLinkFrame.fromRawData(frameData, 4); }; -@suite(timeout(3000), slow(1000)) -class Id3v2_UserUrlLinkFrame_ConstructorTests extends FrameConstructorTests { +@suite class Id3v2_UserUrlLinkFrame_ConstructorTests extends FrameConstructorTests { public get fromOffsetRawData(): (d: ByteVector, o: number, h: Id3v2FrameHeader, v: number) => Frame { return UserUrlLinkFrame.fromOffsetRawData; } @@ -96,8 +95,7 @@ class Id3v2_UserUrlLinkFrame_ConstructorTests extends FrameConstructorTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_UserUrlLinkFrame_PropertyTests { +@suite class Id3v2_UserUrlLinkFrame_PropertyTests { @test public getDescription_emptyText_returnsUndefined() { // Arrange @@ -244,8 +242,7 @@ class Id3v2_UserUrlLinkFrame_PropertyTests { } } -@suite(timeout(3000), slow(1000)) -class Id3v2_UserUrlLink_MethodTests { +@suite class Id3v2_UserUrlLink_MethodTests { @test public findUserUrlLinkFrame_falsyFrames_throws(): void { // Act/Assert diff --git a/test/pictureLazyTests.ts b/test-unit/pictureLazyTests.ts similarity index 99% rename from test/pictureLazyTests.ts rename to test-unit/pictureLazyTests.ts index feb5baf0..81343039 100644 --- a/test/pictureLazyTests.ts +++ b/test-unit/pictureLazyTests.ts @@ -13,8 +13,7 @@ import {PictureType} from "../src/picture"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class PictureLazy_Tests { +@suite class PictureLazy_Tests { @test public fromData_falsyData() { // Act / Assert diff --git a/test/pictureTests.ts b/test-unit/pictureTests.ts similarity index 98% rename from test/pictureTests.ts rename to test-unit/pictureTests.ts index 4dc2ef91..eb2153cd 100644 --- a/test/pictureTests.ts +++ b/test-unit/pictureTests.ts @@ -9,8 +9,7 @@ import {Picture} from "../src/picture"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(timeout(3000), slow(1000)) -class Picture_StaticMethodTests { +@suite class Picture_StaticMethodTests { @test public getExtensionFromData_noMatch() { // Arrange diff --git a/test/resources/testFile.txt b/test-unit/resources/testFile.txt similarity index 100% rename from test/resources/testFile.txt rename to test-unit/resources/testFile.txt diff --git a/test/streamTest.ts b/test-unit/streamTest.ts similarity index 99% rename from test/streamTest.ts rename to test-unit/streamTest.ts index bb2a2875..d65b6a6c 100644 --- a/test/streamTest.ts +++ b/test-unit/streamTest.ts @@ -12,8 +12,7 @@ import {SeekOrigin, Stream} from "../src/stream"; Chai.use(ChaiAsPromised); const assert = Chai.assert; -@suite(slow(1000), timeout(3000)) -class StreamTests { +@suite class StreamTests { @test public createAsRead() { // Act diff --git a/test/testConstants.ts b/test-unit/testConstants.ts similarity index 96% rename from test/testConstants.ts rename to test-unit/testConstants.ts index 4e2e4c31..949f194b 100644 --- a/test/testConstants.ts +++ b/test-unit/testConstants.ts @@ -4,7 +4,7 @@ import {v4 as Uuidv4} from "uuid"; import {ByteVector} from "../src/byteVector"; export default class TestConstants { - public static testFileFolderPath: string = "./test/resources/"; + public static testFileFolderPath: string = "./test-unit/resources/"; public static testFilePath: string = Path.join(TestConstants.testFileFolderPath, "testFile.txt"); public static testFileContents: number[] = [0x31, 0x32, 0x33, 0x34, 0x35, 0x61, 0x62, 0x63, 0x64, 0x65]; public static testFileContentsStr: string = "12345abcde"; diff --git a/test/tslint.json b/test-unit/tslint.json similarity index 100% rename from test/tslint.json rename to test-unit/tslint.json diff --git a/test/utilities/propertyTests.ts b/test-unit/utilities/propertyTests.ts similarity index 100% rename from test/utilities/propertyTests.ts rename to test-unit/utilities/propertyTests.ts diff --git a/test/utilities/testFile.ts b/test-unit/utilities/testFile.ts similarity index 100% rename from test/utilities/testFile.ts rename to test-unit/utilities/testFile.ts diff --git a/test/utilities/testStream.ts b/test-unit/utilities/testStream.ts similarity index 100% rename from test/utilities/testStream.ts rename to test-unit/utilities/testStream.ts diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index 9665c28b..00000000 --- a/test/mocha.opts +++ /dev/null @@ -1,5 +0,0 @@ ---require ts-node/register ---require source-map-support/register ---ui mocha-typescript ---recursive -dist/test/ \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index c67c9b75..ed747338 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,6 @@ "target": "es6" }, "include": [ - "./src/**/*", - "./test/**/*" + "./src/**/*" ] } \ No newline at end of file From b5ec24623ce54a5e654e5f453d965c5063e8477f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2020 16:30:59 +0000 Subject: [PATCH 66/71] Bump lodash from 4.17.15 to 4.17.19 (#9) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ba145f04..390d71b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2108,9 +2108,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, "lodash.flattendeep": { From fbc58ba533f7b00d6b7515a54f54e9d86a9d7036 Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Mon, 14 Sep 2020 01:55:48 +0000 Subject: [PATCH 67/71] Finishing MP3 (#10) * Id3v2 * Id3v1 * MPEG Audio headers etc ----------------------- * Removing code that isn't being used yet * Adding non container classes * Adding MPEG audio classes * WIP that might not work * Fixing a bug where multiple genres couldn't be defined * Writing the rest of the tests for id3v2 files. Not completely working * Fixing a few bugs in Id3v2 tags, adding option to bypass exceptions when rendering frames that aren't supported in a version of Id3v2. Almost all the file tests are passing now * Fixing issues with picture writing/reading. Lotta bugs found here * Fixing a couple more bugs introduced by changes for integration tests * WIP stuff * Fixing up TCON and adding a couple more tests around it * Not sure I'll keep this, but it exercises text information frames with non-latin encoding. * Wiring up the ID3v1 and writing tests for it Note: There's a nasty bug in the TDRC/TYER implementation, it's going to be refactored * Fixing stuff for TDRC/TYER * Adding samples that've been missing for a while * Removing lazy picture test from id3v1 tests Pictures are loaded eagerly if the entire tag is unsynchronized, so there's no way to do a lazily loaded test here. * Adding unit tests for ID3v1Tag * Fixing madge * Fixing circular dependency between audioHeader.ts <-> xingHeader.ts * Started adding MPEG AudioHeader tests * Adding unit tests for the boolean properties on MPEG Audio Header * Adding some more tests from fore vacation * Adding unit test for MPEG Xing VBR header * WIP, idk what's wrong here... * Adding unit tests for VBRI headers Finishing tests for MPEG audio headers --- .idea/inspectionProfiles/Project_Default.xml | 5 + README.md | 5 +- package.json | 2 +- src/aac/aacAudioHeader.ts | 203 ----- src/aac/aacFile.ts | 44 - src/aac/bitStream.ts | 44 - src/ape/apeFile.ts | 11 - src/ape/apeFooter.ts | 179 ---- src/ape/apeItem.ts | 275 ------ src/ape/apeStreamHeader.ts | 124 --- src/ape/apeTag.ts | 598 ------------- src/combinedTag.ts | 42 +- src/errors.ts | 16 +- src/file.ts | 19 +- src/genres.ts | 26 +- src/id3v1/id3v1Tag.ts | 15 +- src/id3v2/frameIdentifiers.ts | 13 +- src/id3v2/frames/attachmentFrame.ts | 52 +- src/id3v2/frames/frameFactory.ts | 26 +- src/id3v2/frames/textInformationFrame.ts | 230 ++--- src/id3v2/id3v2Settings.ts | 18 + src/id3v2/id3v2Tag.ts | 76 +- src/index.ts | 73 ++ src/mpeg/audioFile.ts | 93 ++ src/mpeg/audioHeader.ts | 408 +++++++++ src/mpeg/mpegEnums.ts | 33 + src/mpeg/vbriHeader.ts | 90 ++ src/mpeg/xingHeader.ts | 113 +++ src/nonContainer/endTag.ts | 462 +++++----- src/nonContainer/nonContainerFile.ts | 156 ++++ src/nonContainer/nonContainerTag.ts | 142 ++++ src/nonContainer/startTag.ts | 195 +++++ src/pictureLazy.ts | 4 +- src/tag.ts | 7 +- src/utils.ts | 9 + test-integration/id3v1_fileTests.ts | 55 ++ test-integration/id3v2_fileTests.ts | 186 ++++ .../corruptSamples/null_title_v2.mp3 | Bin 0 -> 12294 bytes .../resources/samples/apple_tags.m4a | Bin 0 -> 102400 bytes test-integration/resources/samples/sample.mp3 | Bin 0 -> 11815 bytes .../resources/samples/sample_gimp.gif | Bin 0 -> 73 bytes .../resources/samples/sample_teenytiny.gif | Bin 0 -> 37 bytes .../resources/samples/sample_v1_only.mp3 | Bin 0 -> 10784 bytes .../samples/sample_v2_3_ext_header.mp3 | Bin 0 -> 65674 bytes .../resources/samples/sample_v2_4_unsynch.mp3 | Bin 0 -> 12288 bytes .../resources/samples/sample_v2_only.mp3 | Bin 0 -> 12288 bytes test-integration/tslint.json | 6 + .../utilities/extendedFileTests.ts | 40 + .../utilities/standardFileTests.ts | 298 +++++++ test-integration/utilities/testConstants.ts | 19 + test-integration/utilities/utilities.ts | 11 + test-unit/fileTests.ts | 17 +- test-unit/id3v1/id3v1TagTests.ts | 303 +++++++ test-unit/id3v2/attachmentsFrameTests.ts | 2 +- test-unit/id3v2/id3v2TagTests.ts | 67 +- test-unit/id3v2/textInformationFrameTests.ts | 357 ++++++-- test-unit/mpeg/audioHeaderTests.ts | 799 ++++++++++++++++++ test-unit/mpeg/vbriHeaderTests.ts | 71 ++ test-unit/mpeg/xingHeaderTests.ts | 124 +++ test-unit/utilities/testFile.ts | 6 + 60 files changed, 4173 insertions(+), 1996 deletions(-) delete mode 100644 src/aac/aacAudioHeader.ts delete mode 100644 src/aac/aacFile.ts delete mode 100644 src/aac/bitStream.ts delete mode 100644 src/ape/apeFile.ts delete mode 100644 src/ape/apeFooter.ts delete mode 100644 src/ape/apeItem.ts delete mode 100644 src/ape/apeStreamHeader.ts delete mode 100644 src/ape/apeTag.ts create mode 100644 src/index.ts create mode 100644 src/mpeg/audioFile.ts create mode 100644 src/mpeg/audioHeader.ts create mode 100644 src/mpeg/mpegEnums.ts create mode 100644 src/mpeg/vbriHeader.ts create mode 100644 src/mpeg/xingHeader.ts create mode 100644 src/nonContainer/nonContainerFile.ts create mode 100644 src/nonContainer/nonContainerTag.ts create mode 100644 src/nonContainer/startTag.ts create mode 100644 test-integration/id3v1_fileTests.ts create mode 100644 test-integration/id3v2_fileTests.ts create mode 100644 test-integration/resources/corruptSamples/null_title_v2.mp3 create mode 100644 test-integration/resources/samples/apple_tags.m4a create mode 100644 test-integration/resources/samples/sample.mp3 create mode 100644 test-integration/resources/samples/sample_gimp.gif create mode 100644 test-integration/resources/samples/sample_teenytiny.gif create mode 100644 test-integration/resources/samples/sample_v1_only.mp3 create mode 100644 test-integration/resources/samples/sample_v2_3_ext_header.mp3 create mode 100644 test-integration/resources/samples/sample_v2_4_unsynch.mp3 create mode 100644 test-integration/resources/samples/sample_v2_only.mp3 create mode 100644 test-integration/tslint.json create mode 100644 test-integration/utilities/extendedFileTests.ts create mode 100644 test-integration/utilities/standardFileTests.ts create mode 100644 test-integration/utilities/testConstants.ts create mode 100644 test-integration/utilities/utilities.ts create mode 100644 test-unit/id3v1/id3v1TagTests.ts create mode 100644 test-unit/mpeg/audioHeaderTests.ts create mode 100644 test-unit/mpeg/vbriHeaderTests.ts create mode 100644 test-unit/mpeg/xingHeaderTests.ts diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 1beadde0..4738a099 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,11 @@ \ No newline at end of file diff --git a/README.md b/README.md index c003176c..7eec50f4 100644 --- a/README.md +++ b/README.md @@ -14,5 +14,6 @@ Note: A port of TagLib already exists for Node.js. Despite TagLib being the orig is substantially lacking in the variety of media formats that can be handled. TagLib# greatly improved on the original TagLib, hence why this project exists -## Supported Formats -(TODO) \ No newline at end of file +## Supported Tagging Formats +* ID3v1 +* ID3v2 \ No newline at end of file diff --git a/package.json b/package.json index 48c456ca..13d7e5e2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "LGPL-2.1-or-later", "scripts": { "build": "tsc -p ./", - "madge": "node node_modules/madge/bin/cli.js --warning --circular --extension ts ./", + "madge": "node node_modules/madge/bin/cli.js --warning --circular --extensions ts ./", "publish-coverage": "nyc report --reporter=text-lcov | coveralls", "test-unit": "mocha test-unit", "test-unit-with-coverage": "nyc mocha test-unit" diff --git a/src/aac/aacAudioHeader.ts b/src/aac/aacAudioHeader.ts deleted file mode 100644 index f199502f..00000000 --- a/src/aac/aacAudioHeader.ts +++ /dev/null @@ -1,203 +0,0 @@ -import {File} from "../file"; -import {IAudioCodec, MediaTypes} from "../iCodec"; -import BitStream from "./bitStream"; - -/** - * Provides information about an ADTS AAC audio stream. - */ -export default class AacAudioHeader implements IAudioCodec { - /** - * List of sample rates for ADTS AAC audio. - */ - private static readonly _sampleRates = [ - 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350 - ]; - - /** - * List of channels for ADTS AAC audio. - */ - private static readonly _channels = [ - 0, 1, 2, 3, 4, 5, 6, 8 - ]; - - // #region Member Variables - - /** - * Bitrate of the audio stream. - */ - private readonly _audioBitrate: number; - - /** - * Number of channels in the audio. - */ - private readonly _audioChannels: number; - - /** - * Sample rate of the audio stream. - */ - private readonly _audioSampleRate: number; - - /** - * Duration of the audio stream in milliseconds. - */ - private _durationMilliseconds: number; - - /** - * Length of the audio stream. - */ - private _streamLength: number; - - /** - * An empty, unset header. - */ - public static readonly Unknown: AacAudioHeader = new AacAudioHeader(); - - // #endregion - - private constructor(channels: number = 0, bitrate: number = 0, sampleRate: number = 0) { - this._audioBitrate = bitrate; - this._audioChannels = channels; - this._audioSampleRate = sampleRate; - this._durationMilliseconds = 0; - this._streamLength = 0; - } - - // #region IAudioCodec - - public get audioBitrate(): number { return this._audioBitrate; } - - public get audioChannels(): number { return this._audioChannels; } - - public get audioSampleRate(): number { return this._audioSampleRate; } - - public get description(): string { return "ADTS AAC"; } - - public get durationMilliseconds(): number { return this._durationMilliseconds; } - - public get mediaTypes(): MediaTypes { return MediaTypes.Audio; } - - // #endregion - - // #region Public Methods - - /** - * Searches for an audio header in {@see File} starting at a specified position and searching - * through a specified number of bytes or to the end of the file. - * @param file File object to search - * @param position Index into the file from which to start searching - * @param length Maximum number of bytes to search before aborting. If this value is `< 0`, - * the file will be searched until the end of the file. - * @returns An object containing: - * * `found` boolean of whether or not the audio header was successfully found in the file - * * `header` {@see AacAudioHeader} the audio header that was found. If `found` is `false`, - * {@see AacAudioHeader.Unknown} is returned. - */ - public static find(file: File, position: number, length: number = -1): {found: boolean, header: AacAudioHeader} { - if (!file) { - throw new Error("Argument null: file was not provided"); - } - if (!Number.isSafeInteger(position) || position < 0) { - throw new Error("Argument out of range: position must be a safe, positive integer"); - } - if (!Number.isSafeInteger(length)) { - throw new Error("Argument out of range: length must be a safe integer"); - } - - const end = position + length; - let header = this.Unknown; - - file.seek(position); - let buffer = file.readBlock(3); - if (buffer.length < 3) { - return {found: false, header: header}; - } - - do { - file.seek(position + 3); - buffer = buffer.mid(buffer.length - 3); - buffer.addByteVector(file.readBlock(File.bufferSize)); - - for (let i = 0; i < buffer.length - 3 && (length < 0 || position + i < end); i++) { - if (buffer.get(i) !== 0xFF || buffer.get(i + 1) < 0xF0) { - continue; - } - - const bits = new BitStream(buffer.mid(i, 7).data); - - // 12 bits sync header - bits.readInt32(12); - - // 1 bit MPEG 2/4 - bits.readInt32(1); - - // 2 bits layer - bits.readInt32(2); - - // 1 bit protection absent - bits.readInt32(1); - - // 2 bits profile object type - bits.readInt32(2); - - // 4 bits sampling frequency index - const sampleRateIndex = bits.readInt32(4); - if (sampleRateIndex >= this._sampleRates.length) { - return {found: false, header: header}; - } - const sampleRate = this._sampleRates[sampleRateIndex]; - - // 1 bit private bit - bits.readInt32(1); - - // 3 bits channel configuration - const channelConfigIndex = bits.readInt32(3); - if (channelConfigIndex >= this._channels.length) { - return {found: false, header: header}; - } - const channels = this._channels[channelConfigIndex]; - - // 4 copyright bits - bits.readInt32(4); - - // 13 bits frame length - const frameLength = bits.readInt32(13); - if (frameLength < 7) { - return {found: false, header: header}; - } - - // 11 bits buffer fullness - bits.readInt32(11); - - // 2 bits number of raw data blocks in frame - const numberOfFrames = bits.readInt32(2) + 1; - const numberOfSamples = numberOfFrames * 1024; - const bitRate = frameLength * 8 * sampleRate / numberOfSamples; - - header = new AacAudioHeader(channels, bitRate, sampleRate); - return {found: true, header: header}; - } - - position += File.bufferSize; - } while (buffer.length > 3 && (length < 0 || position < end)); - - // If we make it to here, we didn't find the audio header - return {found: false, header: header}; - } - - /** - * Sets the length of the audio stream represented by the current instance. Until this value - * has been set, {@see AacAudioHeader.durationMilliseconds} will return an incorrect value. - * @param streamLength Length of the audio stream in bytes represented by the current instance. - * Must be an integer - */ - public setStreamLength(streamLength: number): void { - if (!Number.isInteger(streamLength)) { - throw new Error("Argument out of range: streamLength must be an integer"); - } - - this._streamLength = streamLength; - this._durationMilliseconds = this._streamLength * 8.0 / this._audioBitrate * 1000; - } - - // #endregion -} diff --git a/src/aac/aacFile.ts b/src/aac/aacFile.ts deleted file mode 100644 index ff2b833f..00000000 --- a/src/aac/aacFile.ts +++ /dev/null @@ -1,44 +0,0 @@ -// import AacAudioHeader from "./aacAudioHeader"; -// import {File} from "../file"; -// import {Tag, TagTypes} from "../tag"; -// -// /** -// * Provides tagging and property support for ADTS AAC audio files. -// * An ID3v1 tag and ID3v2 tag will be added automatically to any file that doesn' -// */ -// export default class AacFile extends File { -// private _firstHeader: AacAudioHeader; -// -// protected startTag: -// -// /** -// * Gets a tag of a specified type from the current instance, optionally creating a new tag if -// * possible. If an ID3v2 tag is added to the current instance, it will be placed at the start -// * of the file. Alternatively, ID3v1 and Ape tags will be added to the end of the list. All -// * other tag types will be ignored. -// * @param type Type of tag to read. -// * @param create Boolean specifying whether or not to try and create the tag if one was found. -// * @returns Tag that was found or added to the current instance. If no matching tag was -// * found and none was created `undefined` is returned. -// */ -// public getTag(type: TagTypes, create: boolean): Tag { -// const tag = ( this.tag).getTag(type); -// -// if (tag || !create) { -// return tag; -// } -// -// switch (type) { -// case TagTypes.Id3v1: -// return this.endTag.addTag(type, this.tag); -// case TagTypes.Id3v2: -// return this.startTag.addTag(type, this.tag); -// case TagTypes.Ape: -// return this.endTag.addTag(type, this.tag); -// default: -// return undefined; -// } -// } -// -// -// } diff --git a/src/aac/bitStream.ts b/src/aac/bitStream.ts deleted file mode 100644 index 8fbdb2a1..00000000 --- a/src/aac/bitStream.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * This class is used to help read arbitrary number of bits from a fixed array of bytes. - */ -export default class BitStream { - private _bits: boolean[]; - private _bitIndex: number; - - public constructor(buffer: Uint8Array) { - if (buffer.length !== 7) { - throw new Error("Argument out of range: buffer must be exactly 7 bytes"); - } - - // Reverse bits - this._bits = new Array(56); // 7 bytes, 8 bits/byte = 56 bits - for (let i = 0; i < buffer.length; i++) { - for (let j = 0; j < 8; j++) { - this._bits[i * 8 + j] = (buffer[i] & (1 << (7 - j))) != 0; - } - } - - this._bitIndex = 0; - } - - /** - * Reads a 32-bit integer from the current position of the bitstream - * @param numberOfBits Number of bits to read from the bitstream - * @returns 32-bit integer based on the bits in the bitstream - */ - public readInt32(numberOfBits: number): number { - if (!Number.isSafeInteger(numberOfBits) || numberOfBits <= 0 || numberOfBits > 32) { - throw new Error("Argument out of range exception: numberOfBits must be >0 and <32"); - } - - let value = 0; - let start = this._bitIndex + numberOfBits - 1; - for (let i = 0; i < numberOfBits; i++) { - value += this._bits[start] ? (i << i) : 0; - this._bitIndex++; - start--; - } - - return value; - } -} diff --git a/src/ape/apeFile.ts b/src/ape/apeFile.ts deleted file mode 100644 index eb34635e..00000000 --- a/src/ape/apeFile.ts +++ /dev/null @@ -1,11 +0,0 @@ -// import ApeTag from "./apeTag"; -// import {ByteVector} from "../byteVector"; -// import {Tag, TagTypes} from "../tag"; -// -// export default class ApeFile extends Tag{ -// private _headerBlock: ByteVector; -// -// public getTag(type: TagTypes, create: boolean) { -// -// } -// } diff --git a/src/ape/apeFooter.ts b/src/ape/apeFooter.ts deleted file mode 100644 index 0835a87c..00000000 --- a/src/ape/apeFooter.ts +++ /dev/null @@ -1,179 +0,0 @@ -import {ByteVector, StringType} from "../byteVector"; -import {CorruptFileError} from "../errors"; -import {Guards} from "../utils"; - -/** - * Indicates the flags applied to a {@see ApeFooter} object. - */ -export enum ApeFooterFlags { - /** - * Tag lacks a footer object. - */ - FooterAbsent = 0x40000000, - - /** - * Footer is actually a header. - */ - IsHeader = 0x20000000, - - /** - * Tag contains a header. - */ - HeaderPresent = 0x80000000 -} - -export class ApeFooter { - // #region Member Variables - - /** - * Specifies the identifier used to find an APEv2 footer in a file. - */ - public static readonly fileIdentifier = ByteVector.fromString("APETAGEX", StringType.UTF8); - - /** - * Specifies the size of an APEv2 footer. - */ - public static readonly size = 32; - - private readonly _version: number; - private _flags: ApeFooterFlags; - private _itemCount: number; - private _tagSize: number; - - // #endregion - - /** - * Constructs and initializes a new instance of {@see ApeFooter} by reading it from raw footer - * data. - * @param data ByteVector object containing the raw data to build the new instance from. - * @throws CorruptFileError If {@param data} is smaller than {@see ApeFooter.size} or does not - * begin with {@see ApeFooter.fileIdentifier}. - */ - public constructor(data: ByteVector) { - Guards.truthy(data, "data"); - - if (data.length < ApeFooter.size) { - throw new CorruptFileError("Provided data is smaller than object size"); - } - if (!data.startsWith(ApeFooter.fileIdentifier)) { - throw new CorruptFileError("Provided data does not start with APEv2 file identifier."); - } - - this._version = data.mid(8, 4).toUInt(false); - this._tagSize = data.mid(12, 4).toUInt(false); - this._itemCount = data.mid(16, 4).toUInt(false); - this._flags = data.mid(20, 4).toUInt(false); - } - - // #region Properties - - /** - * Gets the complete size of the tag represented by the current instance, including the header - * and footer. - */ - public get completeTagSize(): number { - return this._tagSize + ((this._flags & ApeFooterFlags.HeaderPresent) != 0 ? ApeFooter.size : 0); - } - - /** - * Gets the flags that apply to the current instance. - */ - public get flags(): ApeFooterFlags { return this._flags; } - /** - * Sets the flags that apply to the current instance. - */ - public set flags(val: ApeFooterFlags) { this._flags = val; } - - /** - * Gets the number of items in the tag represented by the current instance. - */ - public get itemCount(): number { return this._itemCount; } - /** - * Sets the number of items in the tag represented by the current instance. Must be a positive, - * safe integer. - */ - public set itemCount(val: number) { - Guards.uint(val, "val"); - this._itemCount = val; - } - - /** - * Gets the size of the tag represented by the current instance, including the footer but - * excluding the header if applicable. - */ - public get tagSize(): number { return this._tagSize; } - /** - * Sets the size of the tag represented by the current instance, including the footer but - * excluding the header if applicable. Must be a positive, safe integer. - * @param val - */ - public set tagSize(val: number) { - Guards.uint(val, "val"); - this._tagSize = val; - } - - /** - * Gets the version of APE tag described by the current instance. - */ - public get version(): number { return this._version === 0 ? 2000 : this._version; } - - // #endregion - - // #region Public Methods - - /**. - * Renders the current instance as an APE tag footer - */ - public renderFooter(): ByteVector { - return this.render(false); - } - - /** - * Renders the current instance as an APE tag header. - */ - public renderHeader(): ByteVector { - return (this._flags & ApeFooterFlags.HeaderPresent) != 0 - ? this.render(true) - : ByteVector.fromSize(0); - } - - // #endregion - - private render(isHeader: boolean): ByteVector { - // Start with the file identifier - const v = ByteVector.fromByteVector(ApeFooter.fileIdentifier); - - // Add the version number - // NOTE: We always render a 2.000 tag regardless of what the tag originall was. - v.addByteVector(ByteVector.fromUInt(2000, false)); - - // Add the tag size - v.addByteVector(ByteVector.fromUInt(this._tagSize, false)); - - // Add the item count - v.addByteVector(ByteVector.fromUInt(this._itemCount, false)); - - // Render and add the flags - let flags = 0; - if ((this._flags & ApeFooterFlags.HeaderPresent) != 0) { - flags |= ApeFooterFlags.HeaderPresent; - } - - // Footer is always present - if (isHeader) { - flags |= ApeFooterFlags.IsHeader; - } else { - flags &= ~ApeFooterFlags.IsHeader; - } - - v.addByteVector(ByteVector.fromUInt(flags, false)); - - // Add the reserved 64bit - v.addByteVector(ByteVector.fromSize(8, 0x00)); - - return v; - } - - // NOTE: original .NET implementation included IEquatable implementation. It doesn't appear to - // be used anywhere, so I'm omitting it for now. -} diff --git a/src/ape/apeItem.ts b/src/ape/apeItem.ts deleted file mode 100644 index 460038bb..00000000 --- a/src/ape/apeItem.ts +++ /dev/null @@ -1,275 +0,0 @@ -import {ByteVector, StringType} from "../byteVector"; -import {CorruptFileError} from "../errors"; -import {Guards} from "../utils"; - -/** - * Type of data stored in an {@see ApeItem}. - */ -export enum ApeItemType { - /** - * Item contains unicode text. - */ - Text = 0, - - /** - * Item contains binary data. - */ - Binary = 1, - - /** - * Item contains a locator (file path/URL) for external information - */ - Locator = 2 -} - -/** - * Representation of an APEv2 tag item which can be read from and written to disk. - */ -export class ApeItem { - private _data: ByteVector; - private _key: string; - private _isReadOnly: boolean; - private _size: number; - private _text: string[]; - private _type: ApeItemType = ApeItemType.Text; - - // #region Constructors - - private constructor() { } - - /** - * Clones an {@see ApeItem}. - * @param item Item to clone - */ - public static fromApeItem(item: ApeItem): ApeItem { - Guards.truthy(item, "item"); - - const newItem = new ApeItem(); - newItem._type = item._type; - newItem._key = item._key; - newItem._isReadOnly = item._isReadOnly; - newItem._size = item._size; - - if (item._data) { - newItem._data = ByteVector.fromByteVector(item._data, true); - } - if (item._text) { - newItem._text = item._text.slice(); - } - - return item; - } - - /** - * Constructs and initializes a new instance of {@see ApeItem} with a specified key and value - * consisting of binary data. - * @param key Key of the item - * @param value Value of the item - */ - public static fromBinaryData(key: string, value: ByteVector): ApeItem { - Guards.notNullOrUndefined(key, "key"); - Guards.truthy(value, "value"); - - const item = new ApeItem(); - item._key = key; - item._data = ByteVector.fromByteVector(value, true); - - return item; - } - - /** - * Constructs and initializes a new instance of {@see ApeItem} with a specified key and value - * consisting of a list of text elements. - * @param key Key of the item - * @param value Value of the item - */ - public static fromMultipleStrings(key: string, value: string[]): ApeItem { - Guards.notNullOrUndefined(key, "key"); - Guards.truthy(value, "value"); - - const item = new ApeItem(); - item._key = key; - item._text = value.slice(); - - return item; - } - - /** - * Constructs and initializes a new instance of {@see ApeItem} by reading in a raw APEv2 item. - * @param data ByteVector with the item to read. - * @param offset Offset into {@param data} at which the item data begins. Must be a positive, - * safe integer. - */ - public static fromRawData(data: ByteVector, offset: number): ApeItem { - Guards.truthy(data, "data"); - Guards.uint(offset, "offset"); - - const item = new ApeItem(); - item.parse(data, offset); - - return item; - } - - /** - * Constructs and initializes a new instance of {@see ApeItem} with a specified key and value - * consisting of a single text element. - * @param key Key of the item - * @param value Value of the item - */ - public static fromSingleString(key: string, value: string): ApeItem { - Guards.notNullOrUndefined(key, "key"); - Guards.notNullOrUndefined(value, "value"); - - const item = new ApeItem(); - item._key = key; - item._text = [value]; - - return item; - } - - // #endregion - - // #region Properties - - /** - * Gets whether or not the current instance is empty. - */ - public get isEmpty(): boolean { - if (this._type === ApeItemType.Binary) { - return !this._data || this._data.isEmpty; - } - return !this._text || this._text.length === 0; - } - - /** - * Gets whether or not the current instance is flagged as read-only on disk. - */ - public get isReadOnly(): boolean { return this._isReadOnly; } - /** - * Sets whether or not the current instance is flagges as read-only on disk. - */ - public set isReadOnly(val: boolean) { this._isReadOnly = val; } - - /** - * Gets the key used to identify the current instance. - */ - public get key(): string { return this._key; } - - /** - * Gets the size of the current instance as it last appeared on disk. - */ - public get size(): number { return this._size; } - - /** - * Gets the type of value contained in the current instance. - */ - public get type(): ApeItemType { return this._type; } - - /** - * Gets the binary value stored in the current instance. Returns `undefined` if the item - * contains text. - */ - public get value(): ByteVector { return this._type === ApeItemType.Binary ? this._data : undefined; } - - // #endregion - - // #region Public Methods - - /** - * Renders the current instance as an APEv2 item - */ - public render(): ByteVector { - const flags = (this._isReadOnly ? 1 : 0) | (this._type << 1); - - if (this.isEmpty) { - return ByteVector.fromSize(0); - } - - let dataByteVector: ByteVector; - - if (this._type === ApeItemType.Binary && !this._text && this._data) { - dataByteVector = this._data; - } - - if (!dataByteVector && this._text) { - dataByteVector = ByteVector.fromSize(0); - for (let i = 0; i < this._text.length; i++) { - if (i !== 0) { - dataByteVector.addByte(0); - } - dataByteVector.addByteVector(ByteVector.fromString(this._text[i], StringType.UTF8)); - } - } - - // If no data is stored, don't write the item - if (!dataByteVector || dataByteVector.length === 0) { - return ByteVector.fromSize(0); - } - - const output = ByteVector.fromUInt(dataByteVector.length, false); - output.addByteVector(ByteVector.fromUInt(flags, false)); - output.addByteVector(ByteVector.fromString(this._key, StringType.UTF8)); - output.addByte(0); - output.addByteVector(dataByteVector); - - this._size = output.length; - return output; - } - - /** - * Gets the contents of the current instance as a {@see string}. - */ - public toString(): string { - if (this._type === ApeItemType.Binary && this._data.length >= 0) { - return `[Binary(${this._data.length})]`; - } - if (this._type === ApeItemType.Text && this._text) { - return this._text.join(", "); - } - return "null"; - } - - /** - * Gets the contents of the current instance as an array of strings. If this item is storing - * binary data or an empty list of strings, `undefined` is returned. - */ - public toStringArray(): string[] { - if (this._type === ApeItemType.Binary || !this._text) { - return []; - } - return this._text; - } - - // #endregion - - private parse(data: ByteVector, offset: number): void { - Guards.truthy(data, "data"); - Guards.uint(offset, "offset"); - - // 11 bytes is the minimum size for an APE item - if (data.length < offset + 11) { - throw new CorruptFileError("Not enough data for an ApeItem"); - } - - const valueLength = data.mid(offset, 4).toUInt(false); - const flags = data.mid(offset + 4, 4).toUInt(false); - - this._isReadOnly = (flags & 1) === 1; - this._type = (flags >> 1) & 3; - - const pos = data.find(ByteVector.getTextDelimiter(StringType.UTF8), offset + 8); - - this._key = data.toString(StringType.UTF8, offset + 8, pos - offset - 8); - if (valueLength > data.length - pos - 1) { - throw new CorruptFileError("Invalid data length."); - } - - this._size = pos + 1 + valueLength - offset; - - if (this._type === ApeItemType.Binary) { - this._data = ByteVector.fromByteVector(data.mid(pos + 1, valueLength)); - } else { - this._text = data.mid(pos + 1, valueLength).toStrings(StringType.UTF8, 0); - } - } -} diff --git a/src/ape/apeStreamHeader.ts b/src/ape/apeStreamHeader.ts deleted file mode 100644 index 15f9b44d..00000000 --- a/src/ape/apeStreamHeader.ts +++ /dev/null @@ -1,124 +0,0 @@ -import {ByteVector, StringType} from "../byteVector"; -import {CorruptFileError} from "../errors"; -import {ILosslessAudioCodec, MediaTypes} from "../iCodec"; -import {Guards} from "../utils"; - -/** - * Indicates the compression level used when encoding a Monkey's Audio APE file. - */ -export enum CompressionLevel { - /** - * The audio is not compressed. - */ - None = 0, - - /** - * The audio is mildly compressed. - */ - Fast = 1000, - - /** - * The audio is compressed at a normal level. - */ - Normal = 2000, - - /** - * The audio is highly compressed. - */ - High = 3000, - - /** - * The audio is extremely highly compressed. - */ - ExtraHigh = 4000, - - /** - * The audio is compressed to an insane level. - */ - Insane -} - -export class ApeStreamHeader implements ILosslessAudioCodec { - // #region Member Variables - - public static readonly headerSize = 76; - public static readonly fileIdentifier: ByteVector = ByteVector.fromString("MAC", StringType.UTF8); - - private readonly _bitsPerSample: number; - private readonly _blocksPerFrame: number; - private readonly _channels: number; - private readonly _compression: CompressionLevel; - private readonly _finalFrameBlocks: number; - private readonly _sampleRate: number; - private readonly _streamLength: number; - private readonly _totalFrames: number; - private readonly _version: number; - - // #endregion - - public constructor(data: ByteVector, streamLength: number) { - Guards.truthy(data, "data"); - Guards.uint(streamLength, "streamLength"); - - if (!data.startsWith(ApeStreamHeader.fileIdentifier)) { - throw new CorruptFileError("Data does not begin with identifier."); - } - if (data.length < ApeStreamHeader.headerSize) { - throw new CorruptFileError("Insufficient data in stream header"); - } - - this._streamLength = streamLength; - this._version = data.mid(4, 2).toUShort(false); - this._compression = data.mid(52, 2).toUShort(false); - this._blocksPerFrame = data.mid(56, 4).toUInt(false); - this._finalFrameBlocks = data.mid(60, 4).toUInt(false); - this._totalFrames = data.mid(64, 4).toUInt(false); - this._bitsPerSample = data.mid(70, 2).toUShort(false); - this._channels = data.mid(70, 2).toUShort(false); - this._sampleRate = data.mid(72, 4).toUInt(false); - } - - // #region Public Properties - - /** @inheritDoc */ - public get audioBitrate(): number { - const duration = this.durationMilliseconds; - if (duration <= 0) { return 0; } - return this._streamLength * 8 / (duration / 1000) / 1000; - } - - /** @inheritDoc */ - public get audioChannels(): number { return this._channels; } - - /** @inheritDoc */ - public get audioSampleRate(): number { return this._sampleRate; } - - /** @inheritDoc */ - public get bitsPerSample(): number { return this._bitsPerSample; } - - /** - * Gets the level of compression used when encoding the audio represented by this instance. - */ - public get compression(): CompressionLevel { return this._compression; } - - /** @inheritDoc */ - public get description(): string { return `Monkey's Audio APE Version ${this.version}`; } - - /** @inheritDoc */ - public get durationMilliseconds(): number { - if (this._sampleRate <= 0 || this._totalFrames <= 0) { return 0; } - return ((this._totalFrames - 1) * this._blocksPerFrame + this._finalFrameBlocks) / this._sampleRate * 1000; - } - - /** @inheritDoc */ - public get mediaTypes(): MediaTypes { return MediaTypes.LosslessAudio; } - - /** - * Gets the APE version of the audio represented by the current instance. - * @description This valus is stored in bytes (4,5) of the file and is 1000 times the actual - * version number, so 3810 indicates version 3.81. - */ - public get version(): number { return this._version / 1000.0; } - - // #endregion -} diff --git a/src/ape/apeTag.ts b/src/ape/apeTag.ts deleted file mode 100644 index 1ecf8373..00000000 --- a/src/ape/apeTag.ts +++ /dev/null @@ -1,598 +0,0 @@ -import * as dateFormat from "dateformat"; - -import {ApeFooter, ApeFooterFlags} from "./apeFooter"; -import {ApeItem, ApeItemType} from "./apeItem"; -import {ByteVector, StringType} from "../byteVector"; -import {IPicture, Picture, PictureType} from "../picture"; -import {Tag, TagTypes} from "../tag"; -import {ArrayUtils, Guards} from "../utils"; - -/** - * Class extends {@see Tag} to provide a representation of an APEv2 tag which can be read and - * written to disk. - */ -export default class ApeTag extends Tag { - private static readonly _pictureItemNames = [ - "Cover Art (other)", - "Cover Art (icon)", - "Cover Art (other icon)", - "Cover Art (front)", - "Cover Art (back)", - "Cover Art (leaflet)", - "Cover Art (media)", - "Cover Art (lead)", - "Cover Art (artist)", - "Cover Art (conductor)", - "Cover Art (band)", - "Cover Art (composer)", - "Cover Art (lyricist)", - "Cover Art (studio)", - "Cover Art (recording)", - "Cover Art (performance)", - "Cover Art (movie scene)", - "Cover Art (colored fish)", - "Cover Art (illustration)", - "Cover Art (band logo)", - "Cover Art (publisher logo)", - "Embedded Object" - ]; - - private readonly _items: ApeItem[] = []; - private _footer: ApeFooter; - - public get headerPresent(): boolean { return (this._footer.flags & ApeFooterFlags.HeaderPresent) !== 0; } - public set headerPresent(val: boolean) { - if (val) { - this._footer.flags |= ApeFooterFlags.HeaderPresent; - } else { - this._footer.flags &= ~ApeFooterFlags.HeaderPresent; - } - } - - // #region Public Methods - - /** - * Adds the contents of an array of strings to the value stored in a specified item. - * @param key Key of the item to store the value in. - * @param value Text values to add. - */ - public addMultipleStringValue(key: string, value: string[]): void { - Guards.notNullOrUndefined(key, "key"); - - if (!value || value.length === 0) { - return; - } - - // Add existing values of the item first - const index = this.getItemIndex(key); - const values = []; - if (index >= 0) { - values.push(... this._items[index].toStringArray()); - } - - // Add the new values to the item - values.push(... value); - - // Add the item if it doesn't already exist, replace it with a new one if it does - const item = ApeItem.fromMultipleStrings(key, values); - if (index >= 0) { - this._items[0] = item; - } else { - this._items.push(item); - } - } - - /** - * Adds a new item to this tag as a single numeric value. If both {@param n} and {@param count} - * are provided, the value will be stored as "n/count". If only {@param n} is provided, the - * value will be stored as just "n". If both {@param n} and {@param count} are 0, the item item - * will be removed. - * @param key Key of the item to store the value in. - * @param n Numeric value to store. If {@param count} is also provided, this will be the - * numerator of the fraction to store. - * @param count Optionally, the denominator of the fraction to store. - */ - public addNumericValue(key: string, n: number, count: number = 0): void { - Guards.notNullOrUndefined(key, "key"); - Guards.uint(n, "n"); - Guards.uint(count, "count"); - - if (n === 0 && count === 0) { - return; - } - - if (count !== 0) { - this.addSingleStringValue(key, `${n}/${count}`); - } else { - this.addSingleStringValue(key, n.toString()); - } - } - - /** - * Adds a single string to the specified item in this tag. - * @param key Key of the item to add. - * @param value Value for the item to add. - */ - public addSingleStringValue(key: string, value: string): void { - Guards.notNullOrUndefined(key, "key"); - - if (!value) { - return; - } - - this.addMultipleStringValue(key, [value]); - } - - /** - * Gets a specified item from the current instance. - * @param key Key for locating the requested {@see ApeItem}. - * @returns The requested {@see ApeItem} if the specified {@param key} exists, `undefined` - * otherwise. - */ - public getItem(key: string): ApeItem { - Guards.notNullOrUndefined(key, "key"); - const upperKey = key.toUpperCase(); - return this._items.find((i) => i.key.toUpperCase() === upperKey); - } - - /** - * Checks if an item exists in the current instance. - * @param key Key of the item to check. - */ - public hasItem(key: string): boolean { - Guards.notNullOrUndefined(key, "key"); - return this.getItemIndex(key) >= 0; - } - - /** - * Renders the current instance as a raw APEv2 tag. - */ - public render(): ByteVector { - const data = this._items.reduce( - (bv, e) => { bv.addByteVector(e.render()); return bv; }, - ByteVector.fromSize(0) - ); - - this._footer.itemCount = this._items.length; - this._footer.tagSize = (data.length + ApeFooter.size); - this.headerPresent = true; - - data.insertByteVector(0, this._footer.renderHeader()); - data.addByteVector(this._footer.renderFooter()); - return data; - } - - /** - * Removes the items with a specified key from the current instance. - * @param key Key of the item to remove from the current instance. - */ - public removeItem(key: string): void { - Guards.notNullOrUndefined(key, "key"); - const keyUpper = key.toUpperCase(); - ArrayUtils.remove(this._items, (e) => e.key.toUpperCase() === keyUpper); - } - - /** - * Stores the contents of an array of strings in a specified item. If {@param value} is falsy - * or empty, the item will be removed. - * @param key Key of the item to store the value in. - * @param value Text to store in the item. - */ - public setMultipleStringValue(key: string, value: string[]): void { - Guards.notNullOrUndefined(key, "key"); - - if (!value || value.length === 0) { - this.removeItem(key); - return; - } - - const item = ApeItem.fromMultipleStrings(key, value); - const index = this.getItemIndex(key); - if (index >= 0) { - this._items[index] = item; - } else { - this._items.push(item); - } - } - - /** - * Sets the value of an existing item to this tag as a single numeric value. If both {@param n} - * and {@param count} are provided, the value will be stored as "n/count". If only {@param n} - * is provided, the value will be stored as just "n". - * @param key Key of the item to store the value in. - * @param n Numeric value to store. If {@param count} is also provided, this will be the - * numerator of the fraction to store. - * @param count Optionally, the denominator of the fraction to store. - */ - public setNumericValue(key: string, n: number, count: number = 0): void { - Guards.notNullOrUndefined(key, "key"); - Guards.uint(n, "n"); - Guards.uint(count, "count"); - - if (n === 0 || count === 0) { - this.removeItem(key); - } else if (count !== 0) { - this.setSingleStringValue(key, `${n}/${count}`); - } else { - this.setSingleStringValue(key, n.toString()); - } - } - - /** - * Sets the value of an existing item in this tag vith a single string as the value. If - * {@param value} is falsy, the item will be removed. - * @param key Key of the item to add. - * @param value Value for the item to add. - */ - public setSingleStringValue(key: string, value: string): void { - Guards.notNullOrUndefined(key, "key"); - - if (!value) { - this.removeItem(key); - } - - this.setMultipleStringValue(key, [value]); - } - - /** - * Adds an item to the current instance, replacing an existing item with the same key. - * @param item {@see ApeItem} to add to the current instance. - */ - public setItem(item: ApeItem): void { - Guards.truthy(item, "item"); - - const index = this.getItemIndex(item.key); - if (index >= 0) { - this._items[index] = item; - } else { - this._items.push(item); - } - } - - // #endregion - - // #region Tag - - public get tagTypes(): TagTypes { return TagTypes.Ape; } - - public get title(): string { return this.getItemAsString("Title"); } - public set title(value: string) { this.setSingleStringValue("Title", value); } - - public get titleSort(): string { return this.getItemAsString("TitleSort"); } - public set titleSort(value: string) { this.setSingleStringValue("TitleSort", value); } - - public get subtitle(): string { return this.getItemAsString("Subtitle"); } - public set subtitle(value: string) { this.setSingleStringValue("Subtitle", value); } - - public get description(): string { return this.getItemAsString("Description"); } - public set description(value: string) { this.setSingleStringValue("Description", value); } - - public get performers(): string[] { return this.getItemAsStrings("Artist"); } - public set performers(value: string[]) { this.setMultipleStringValue("Artist", value); } - - public get performersSort(): string[] { return this.getItemAsStrings("ArtistSort"); } - public set performersSort(value: string[]) { this.setMultipleStringValue("ArtistSort", value); } - - public get performersRole(): string[] { return this.getItemAsStrings("PerformersRole"); } - public set performersRole(value: string[]) { this.setMultipleStringValue("PerformersRoles", value); } - - /** - * Gets the artist who is credited in the creation of the entire album or collection containing - * the media described by the current instance. - * This property is implemented using the "Album Artist" item. "AlbumArtist" is used as a - * backup property if the first does not exist. - */ - public get albumArtists(): string[] { - let list = this.getItemAsStrings("Album Artist"); - if (list.length === 0) { - list = this.getItemAsStrings("AlbumArtist"); - } - return list; - } - /** - * Sets the artist who is credited in the creation of the entire album or collection containing - * the media described by the current instance. - * The "Album Artist" item will always be used to store the value, if the older "AlbumArtist" - * exists, it will also be used to store the value. - */ - public set albumArtists(value: string[]) { - this.setMultipleStringValue("Album Artist", value); - if (this.hasItem("AlbumArtist")) { - this.setMultipleStringValue("AlbumArtist", value); - } - } - - public get albumArtistsSort(): string[] { return this.getItemAsStrings("AlbumArtistSort"); } - public set albumArtistsSort(value: string[]) { this.setMultipleStringValue("AlbumArtistSort", value); } - - public get composers(): string[] { return this.getItemAsStrings("Composer"); } - public set composers(value: string[]) { this.setMultipleStringValue("Composer", value); } - - public get composersSort(): string[] { return this.getItemAsStrings("ComposerSort"); } - public set composersSort(value: string[]) { this.setMultipleStringValue("ComposerSort", value); } - - public get album(): string { return this.getItemAsString("Album"); } - public set album(value: string) { this.setSingleStringValue("Album", value); } - - public get albumSort(): string { return this.getItemAsString("AlbumSort"); } - public set albumSort(value: string) { this.setSingleStringValue("AlbumSort", value); } - - public get comment(): string { return this.getItemAsString("Comment"); } - public set comment(value: string) { this.setSingleStringValue("Comment", value); } - - public get genres(): string[] { return this.getItemAsStrings("Genre"); } - public set genres(value: string[]) { this.setMultipleStringValue("Genre", value); } - - public get year(): number { - const text = this.getItemAsString("Year"); - if (!text) { return 0; } - - const num = parseInt(text.substring(0, 4), 10); - return Number.isNaN(num) ? 0 : num; - } - public set year(value: number) { this.setNumericValue("Year", value, 0); } - - public get track(): number { return this.getItemAsUInt("Track", 0); } - public set track(value: number) { this.setNumericValue("Track", value, this.trackCount); } - - public get trackCount(): number { return this.getItemAsUInt("Track", 1); } - public set trackCount(value: number) { this.setNumericValue("Track", this.track, value); } - - public get disc(): number { return this.getItemAsUInt("Disc", 0); } - public set disc(value: number) { this.setNumericValue("Disc", value, this.discCount); } - - public get discCount(): number { return this.getItemAsUInt("Disc", 1); } - public set discCount(value: number) { this.setNumericValue("Disc", value, this.discCount); } - - public get lyrics(): string { return this.getItemAsString("Lyrics"); } - public set lyrics(value: string) { this.setSingleStringValue("Lyrics", value); } - - public get grouping(): string { return this.getItemAsString("Grouping"); } - public set grouping(value: string) { this.setSingleStringValue("Grouping", value); } - - public get beatsPerMinute(): number { - const text = this.getItemAsString("BPM"); - if (!text) { return 0; } - const num = parseFloat(text); - return Number.isNaN(num) ? 0 : Math.round(num); - } - public set beatsPerMinute(value: number) { this.setNumericValue("BPM", value, 0); } - - public get conductor(): string { return this.getItemAsString("Conductor"); } - public set conductor(value: string) { this.setSingleStringValue("Conductor", value); } - - public get copyright(): string { return this.getItemAsString("Copyright"); } - public set copyright(value: string) { this.setSingleStringValue("Copyright", value); } - - public get dateTagged(): Date { - const val = this.getItemAsString("DateTagged"); - if (val ) { - const date = new Date(val); - if (!Number.isNaN(date.valueOf())) { - return date; - } - } - - return undefined; - } - public set dateTagged(value: Date) { - let str; - if (value) { - str = dateFormat(value, "yyyy-mm-dd HH:MM:ss"); - } - this.setSingleStringValue("DateTagged", str); - } - - public get musicBrainzArtistId(): string { return this.getItemAsString("MUSICBRAINZ_ARTISTID"); } - public set musicBrainzArtistId(value: string) { this.setSingleStringValue("MUSICBRAINZ_ARTISTID", value); } - - public get musicBrainzReleaseGroupId(): string { return this.getItemAsString("MUSICBRAINZ_RELEASEGROUPID"); } - public set musicBrainzReleaseGroupId(value: string) { - this.setSingleStringValue("MUSICBRAINZ_RELEASEGROUPID", value); - } - - public get musicBrainzReleaseId(): string { return this.getItemAsString("MUSICBRAINZ_ALBUMID"); } - public set musicBrainzReleaseId(value: string) { this.setSingleStringValue("MUSICBRAINZ_ALBUMID", value); } - - public get musicBrainzReleaseArtistId(): string { return this.getItemAsString("MUSICBRAINZ_ALBUMARTISTID"); } - public set musicBrainzReleaseArtistId(value: string) { - this.setSingleStringValue("MUSICBRAINZ_ALBUMARTISTID", value); - } - - public get musicBrainzTrackId(): string { return this.getItemAsString("MUSICBRAINZ_TRACKID"); } - public set musicBrainzTrackId(value: string) { this.setSingleStringValue("MUSICBRAINZ_TRACKID", value); } - - public get musicBrainzDiscId(): string { return this.getItemAsString("MUSICBRAINZ_DISCID"); } - public set musicBrainzDiscId(value: string) { this.setSingleStringValue("MUSICBRAINZ_DISCID", value); } - - public get musicIpId(): string { return this.getItemAsString("MUSICIP_PUID"); } - public set musicIpId(value: string) { this.setSingleStringValue("MUSICIP_PUID", value); } - - public get amazonId(): string { return this.getItemAsString("ASIN"); } - public set amazonId(value: string) { this.setSingleStringValue("ASIN", value); } - - public get musicBrainzReleaseStatus(): string { return this.getItemAsString("MUSICBRAINZ_ALBUMSTATUS"); } - public set musicBrainzReleaseStatus(value: string) { this.setSingleStringValue("MUSICBRAINZ_ALBUMSTATUS", value); } - - public get musicBrainzReleaseType(): string { return this.getItemAsString("MUSICBRAINS_ALBUMTYPE"); } - public set musicBrainzReleaseType(value: string) { this.setSingleStringValue("MUSICBRAINZ_ALBUMTYPE", value); } - - public get musicBrainzReleaseCountry(): string { return this.getItemAsString("RELEASECOUNTRY"); } - public set musicBrainzReleaseCountry(value: string) { this.setSingleStringValue("RELEASECOUNTRY", value); } - - public get replayGainTrackGain(): number { - let text = this.getItemAsString("REPLAYGAIN_TRACK_GAIN"); - if (!text) { return NaN; } - if (text.toLowerCase().endsWith("db")) { - text = text.substring(0, text.length - 2).trim(); - } - return parseFloat(text); - } - public set replayGainTrackGain(value: number) { - if (Number.isNaN(value)) { - this.removeItem("REPLAYGAIN_TRACK_GAIN"); - } else { - const text = `${value.toFixed(2)} dB`; - this.setSingleStringValue("REPLAYGAIN_TRACK_GAIN", text); - } - } - - public get replayGainTrackPeak(): number { - let text = this.getItemAsString("REPLAYGAIN_TRACK_PEAK"); - if (!text) { return NaN; } - if (text.toLowerCase().endsWith("db")) { - text = text.substring(0, text.length - 2).trim(); - } - return parseFloat(text); - } - public set replayGainTrackPeak(value: number) { - if (Number.isNaN(value)) { - this.removeItem("REPLAYGAIN_TRACK_PEAK"); - } else { - const text = `${value.toFixed(6)} dB`; - this.setSingleStringValue("REPLAYGAIN_TRACK_PEAK", text); - } - } - - public get replayGainAlbumGain(): number { - let text = this.getItemAsString("REPLAYGAIN_ALBUM_GAIN"); - if (!text) { return NaN; } - if (text.toLowerCase().endsWith("db")) { - text = text.substring(0, text.length - 2).trim(); - } - return parseFloat(text); - } - public set replayGainAlbumGain(value: number) { - if (Number.isNaN(value)) { - this.removeItem("REPLAYGAIN_ALBUM_GAIN"); - } else { - const text = `${value.toFixed(2)} dB`; - this.setSingleStringValue("REPLAYGAIN_ALBUM_GAIN", text); - } - } - - public get replayGainAlbumPeak(): number { - let text = this.getItemAsString("REPLAYGAIN_ALBUM_PEAK"); - if (!text) { return NaN; } - if (text.toLowerCase().endsWith("db")) { - text = text.substring(0, text.length - 2).trim(); - } - return parseFloat(text); - } - public set replayGainAlbumPeak(value: number) { - if (Number.isNaN(value)) { - this.removeItem("REPLAYGAIN_ALBUM_PEAK"); - } else { - const text = `${value.toFixed(6)} dB`; - this.setSingleStringValue("REPLAYGAIN_ALBUM_PEAK", text); - } - } - - public get pictures(): IPicture[] { - const pictures = []; - for (const item of this._items) { - if (!item || item.type !== ApeItemType.Binary) { continue; } - - const keyLower = item.key.toLowerCase(); - const typeIndex = ApeTag._pictureItemNames.findIndex((e) => e.toLowerCase() === keyLower); - if (typeIndex < 0) { continue; } - - const index = item.value.find(ByteVector.getTextDelimiter(StringType.UTF8)); - if (index < 0) { continue; } - - const pic = Picture.fromData(item.value.mid(index + 1)); - pic.description = item.value.toString(StringType.UTF8, 0, index); - pic.type = typeIndex < ApeTag._pictureItemNames.length - 1 ? typeIndex : PictureType.NotAPicture; - - pictures.push(pic); - } - - return pictures; - } - public set pictures(value: IPicture[]) { - for (const itemName of ApeTag._pictureItemNames) { - this.removeItem(itemName); - } - - if (!value || value.length === 0) { return; } - - for (const pic of value) { - let type = pic.type; - if (type >= ApeTag._pictureItemNames.length) { - type = ApeTag._pictureItemNames.length - 1; - } - - const name = ApeTag._pictureItemNames[type]; - if (this.getItem(name)) { continue; } - - const data = ByteVector.fromString(pic.description, StringType.UTF8); - data.addByteVector(ByteVector.getTextDelimiter(StringType.UTF8)); - data.addByteVector(pic.data); - - this.setItem(ApeItem.fromBinaryData(name, data)); - } - } - - public get isEmpty(): boolean { return this._items.length === 0; } - - public clear(): void { - this._items.splice(0, this._items.length); - } - - public copyTo(target: Tag, overwrite: boolean): void { - Guards.truthy(target, "target"); - - // @TODO: Does this need to be an & check - const apeTarget = target; - if (target.tagTypes !== TagTypes.Ape || apeTarget._items === undefined) { - super.copyTo(target, overwrite); - return; - } - - for (const item of this._items) { - if (!overwrite && apeTarget.getItem(item.key)) { continue; } - apeTarget.setItem(ApeItem.fromApeItem(item)); - } - } - - // #endregion - - // #region Private Methods - - private getItemIndex(key: string): number { - const keyUpper = key.toUpperCase(); - return this._items.findIndex((e) => e.key.toUpperCase() === keyUpper); - } - - private getItemAsString(key: string): string { - const item = this.getItem(key); - return item ? item.toString() : undefined; - } - - private getItemAsStrings(key: string): string[] { - const item = this.getItem(key); - return item ? item.toStringArray() : []; - } - - private getItemAsUInt(key: string, index: number) { - Guards.uint(index, "index"); - - const text = this.getItemAsString(key); - if (!text) { - return 0; - } - - const values = text.split("/", index + 2); - if (values.length < index + 1) { - return 0; - } - const num = parseInt(values[index], 10); - if (Number.isNaN(num)) { - return 0; - } - - return num; - } - - // #endregion -} diff --git a/src/combinedTag.ts b/src/combinedTag.ts index 4e4a9a4c..5149d98d 100644 --- a/src/combinedTag.ts +++ b/src/combinedTag.ts @@ -10,7 +10,7 @@ export default class CombinedTag extends Tag { */ public constructor(tags?: Tag[]) { super(); - this.tags = tags; + this._tags = tags ? tags.slice(0) : []; } // #region Properties @@ -276,13 +276,13 @@ export default class CombinedTag extends Tag { * current instance. * Returns the first non-null/non-undefined value from the child tags. */ - public get discCount(): number { return this.getFirstValue((t) => t.disc); } + public get discCount(): number { return this.getFirstValue((t) => t.discCount); } /** * Sets the number of the discs in the boxed set containing the media represented by the * current instance. Must be a positive integer positive integer. * Sets the value on all child tags */ - public set discCount(val: number) { this.setAllUint((t, v) => { t.disc = v; }, val); } + public set discCount(val: number) { this.setAllUint((t, v) => { t.discCount = v; }, val); } /** * Gets the lyrics or script of the media represented by the current instance. @@ -606,7 +606,37 @@ export default class CombinedTag extends Tag { } } - // #region Private Helpers + /** + * Sets the child tags to combine in the current instance + * @param tags Tags to combine, falsy tags will be ignored + */ + public setTags(... tags: Tag[]): void { + this._tags.splice(0, tags.length); + + const truthyTags = tags.filter((t) => !!t); + this._tags.push(... truthyTags); + } + + // #region Protected/Private Methods + + protected addTagInternal(tag: Tag) { + this._tags.push(tag); + } + + protected clearTags(): void { + this._tags.splice(0, this._tags.length); + } + + protected insertTag(index: number, tag: Tag): void { + this._tags.splice(index, 0, tag); + } + + protected removeTag(tag: Tag) { + const index = this._tags.indexOf(tag); + if (index >= 0) { + this._tags.splice(index, 1); + } + } private getFirstArray(propertyFn: (t: Tag) => T[]): T[] { const tagWithProperty = this._tags.find((t) => { @@ -631,14 +661,14 @@ export default class CombinedTag extends Tag { throw new Error("Argument out of range: value must be a positive integer"); } this._tags.forEach((t) => { - if (!!t) { return; } + if (!t) { return; } propertyFn(t, val); }); } private setAllValues(propertyFn: (t: Tag, val: T) => void, val: T): void { this._tags.forEach((t) => { - if (!!t) { return; } + if (!t) { return; } propertyFn(t, val); }); } diff --git a/src/errors.ts b/src/errors.ts index a3b3834b..87a0b889 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,5 +1,5 @@ export class CorruptFileError extends Error { - public static isCorruptFileError: boolean = true; + public readonly isCorruptFileError: boolean = true; public constructor(msg?: string) { super(msg); @@ -11,7 +11,7 @@ export class CorruptFileError extends Error { } export class NotImplementedError extends Error { - public static isNotImplementedError: boolean = true; + public readonly isNotImplementedError: boolean = true; public constructor(message?: string) { super(`Not implemented${message ? `: ${message}` : ""}`); @@ -21,3 +21,15 @@ export class NotImplementedError extends Error { return e.hasOwnProperty("isNotImplementedError"); } } + +export class NotSupportedError extends Error { + public readonly isNotSupportedError: boolean = true; + + public constructor(message?: string) { + super(`Not supported${message ? `: ${message}` : ""}`); + } + + public static errorIs(e: Error): boolean { + return e.hasOwnProperty("isNotSupportedError"); + } +} diff --git a/src/file.ts b/src/file.ts index 002f2bbd..5ec0dd8f 100644 --- a/src/file.ts +++ b/src/file.ts @@ -329,11 +329,20 @@ export abstract class File { } /** - * Used for clearing all the types and resolvers during unit testing + * Used for removing a file type constructor during unit testing */ - public static clearFileTypesAndResolvers() { - File._fileTypeResolvers = []; - File._fileTypes = {}; + public static removeFileType(mimeType: string) { + delete File._fileTypes[mimeType]; + } + + /** + * Used for removing a file type resolver during unit testing + */ + public static removeFileTypeResolver(resolver: FileTypeResolver) { + const index = File._fileTypeResolvers.indexOf(resolver); + if (index >= 0) { + File._fileTypeResolvers.splice(index, 1); + } } /** @@ -470,7 +479,7 @@ export abstract class File { const buffer = new Uint8Array(File.bufferSize); const stopShufflingIndex = start + replace + bytesToAdd; let shuffleIndex = this._fileStream.length; - while (shuffleIndex > stopShufflingIndex + bytesToAdd) { + while (shuffleIndex > stopShufflingIndex) { const bytesToReplace = Math.min(shuffleIndex - stopShufflingIndex, File.bufferSize); // Fill up the buffer diff --git a/src/genres.ts b/src/genres.ts index bc8762b5..9c4a3a15 100644 --- a/src/genres.ts +++ b/src/genres.ts @@ -148,7 +148,7 @@ const audioGenres = [ "Anime", "Jpop", "Synthpop" -] +]; // DivX video genres (INDEXES MATTER) const videoGenres = [ @@ -195,9 +195,11 @@ const videoGenres = [ ]; -function stringToByte(text: string): number { - const trimRegex = /^\(+|\)+$/g; - text = text.replace(trimRegex, ""); +function stringToByte(text: string, allowParenthesis: boolean): number { + if (allowParenthesis) { + const trimRegex = /^\(+|\)+$/g; + text = text.replace(trimRegex, ""); + } const index = parseInt(text, 10); return Number.isNaN(index) ? 255 : index; } @@ -216,13 +218,15 @@ export default { /** * Gets the audio genre name for a specified index. * @param index Index of the genre in the audio genre array. Can be a {@see Number}, - * {@see string} or {@see string} wrapped in `( )` + * {@see string} or {@see string} wrapped in `( )`, if {@paramref allowParenthesis} is set + * to `true` + * @param allowParenthesis Whether or not a number wrapped in parenthesis is allowed * @returns string Genre name if found, or `undefined` if {@paramref index} is outside the * bounds of the audio genre array or if {@paramref index} is not valid. */ - indexToAudio: (index: number|string): string => { + indexToAudio: (index: number|string, allowParenthesis: boolean): string => { const safeIndex = typeof(index) === "string" - ? stringToByte(index) + ? stringToByte(index, allowParenthesis) : index; return Number.isSafeInteger(safeIndex) && safeIndex < audioGenres.length && safeIndex >= 0 ? audioGenres[safeIndex] @@ -232,13 +236,15 @@ export default { /** * Gets the video genre name for a specified index. * @param index Index of the genre in the video genre array. Can be a {@see Number}, - * {@see string} or {@see string} wrapped in `( )` + * {@see string} or {@see string} wrapped in `( )` if {@paramref allowParenthesis} is set + * to `true` + * @param allowParenthesis Whether or not a number wrapped in parenthesis is allowed * @returns string Genre name if found, or `undefined` if {@paramref index} is outside the * bounds of the video genre array or if {@paramref index} is not valid. */ - indexToVideo: (index: number|string): string => { + indexToVideo: (index: number|string, allowParenthesis: boolean): string => { const safeIndex = typeof(index) === "string" - ? stringToByte(index) + ? stringToByte(index, allowParenthesis) : index; return Number.isSafeInteger(safeIndex) && safeIndex < videoGenres.length && safeIndex >= 0 ? videoGenres[safeIndex] diff --git a/src/id3v1/id3v1Tag.ts b/src/id3v1/id3v1Tag.ts index f3fd6172..d68cbb81 100644 --- a/src/id3v1/id3v1Tag.ts +++ b/src/id3v1/id3v1Tag.ts @@ -88,11 +88,11 @@ export default class Id3v1Tag extends Tag { public render(): ByteVector { const data = ByteVector.empty(); data.addByteVector(Id3v1Tag.fileIdentifier); - data.addByteVector(ByteVector.fromString(this._title, StringType.Latin1).resize(30)); - data.addByteVector(ByteVector.fromString(this._artist, StringType.Latin1).resize(30)); - data.addByteVector(ByteVector.fromString(this._album, StringType.Latin1).resize(30)); - data.addByteVector(ByteVector.fromString(this._year, StringType.Latin1).resize(4)); - data.addByteVector(ByteVector.fromString(this._comment, StringType.Latin1).resize(28)); + data.addByteVector(ByteVector.fromString(this._title || "", StringType.Latin1).resize(30)); + data.addByteVector(ByteVector.fromString(this._artist || "", StringType.Latin1).resize(30)); + data.addByteVector(ByteVector.fromString(this._album || "", StringType.Latin1).resize(30)); + data.addByteVector(ByteVector.fromString(this._year || "", StringType.Latin1).resize(4)); + data.addByteVector(ByteVector.fromString(this._comment || "", StringType.Latin1).resize(28)); data.addByte(0x00); data.addByte(this._track); data.addByte(this._genre); @@ -142,7 +142,7 @@ export default class Id3v1Tag extends Tag { /** @inheritDoc */ public get genres(): string[] { - const genreName = Genres.indexToAudio(this._genre); + const genreName = Genres.indexToAudio(this._genre, false); return genreName ? [genreName] : []; } /** @@ -212,6 +212,7 @@ export default class Id3v1Tag extends Tag { this._track = data.get(126); } else { this._comment = Id3v1Tag.parseString(data.mid(97, 30)); + this._track = 0; } this._genre = data.get(127); @@ -220,7 +221,7 @@ export default class Id3v1Tag extends Tag { private static parseString(data: ByteVector): string { Guards.truthy(data, "data"); - const output = data.toString(StringType.Latin1).trim(); + const output = data.toString(undefined, StringType.Latin1).trim(); const i = output.indexOf("\0"); return i >= 0 ? output.substring(0, i) diff --git a/src/id3v2/frameIdentifiers.ts b/src/id3v2/frameIdentifiers.ts index 8b0423f6..e428fb60 100644 --- a/src/id3v2/frameIdentifiers.ts +++ b/src/id3v2/frameIdentifiers.ts @@ -1,4 +1,5 @@ import {ByteVector, StringType} from "../byteVector"; +import {NotSupportedError} from "../errors"; import {Guards} from "../utils"; /** @@ -54,11 +55,16 @@ export class FrameIdentifier { Guards.betweenInclusive(version, 2, 4, "version"); if (!this.versionTable[version]) { const newest = this.versionTable[4] || this.versionTable[3] || this.versionTable[2]; - throw new Error(`Frame ${newest} is not supported in ID3v2 version ${version}`); + throw new NotSupportedError(`Frame ${newest} is not supported in ID3v2 version ${version}`); } return ByteVector.fromByteVector(this.versionTable[version]); } + + public toString(): string { + const newest = this.versionTable[4] || this.versionTable[3] || this.versionTable[2]; + return newest.toString(); + } } // Pre-create the unique identifiers @@ -123,8 +129,8 @@ const uniqueFrameIdentifiers: {[key: string]: FrameIdentifier} = { TOWN: new FrameIdentifier("TOWN", "TOWN", undefined), // File owner/licensee TPE1: new FrameIdentifier("TPE1", "TPE1", "TP1"), // Lead performer(s)/soloist(s) TPE2: new FrameIdentifier("TPE2", "TPE2", "TP2"), // Band/orchestra/accompaniment - TPE3: new FrameIdentifier("TPE3", "TEP3", "TP3"), // Counductor/performer refinement - TPE4: new FrameIdentifier("TPE4", "TEP4", "TP4"), // Interpreted, remixed, or otherwise modified by + TPE3: new FrameIdentifier("TPE3", "TPE3", "TP3"), // Counductor/performer refinement + TPE4: new FrameIdentifier("TPE4", "TPE4", "TP4"), // Interpreted, remixed, or otherwise modified by TPOS: new FrameIdentifier("TPOS", "TPOS", "TPA"), // Part of a set TPRO: new FrameIdentifier("TPRO", undefined, undefined), // Produced notice TPUB: new FrameIdentifier("TPUB", "TPUB", "TPB"), // Publisher @@ -308,6 +314,7 @@ export const FrameIdentifiers: {[key: string]: FrameIdentifier} = { WCP: uniqueFrameIdentifiers.WCOP, WOAF: uniqueFrameIdentifiers.WOAF, WOAR: uniqueFrameIdentifiers.WOAR, + WOAS: uniqueFrameIdentifiers.WOAS, WORS: uniqueFrameIdentifiers.WORS, WPAY: uniqueFrameIdentifiers.WPAY, WPB: uniqueFrameIdentifiers.WPUB, diff --git a/src/id3v2/frames/attachmentFrame.ts b/src/id3v2/frames/attachmentFrame.ts index 6b68c43d..9eb04f77 100644 --- a/src/id3v2/frames/attachmentFrame.ts +++ b/src/id3v2/frames/attachmentFrame.ts @@ -6,6 +6,8 @@ import {Id3v2FrameHeader} from "./frameHeader"; import {FrameIdentifiers} from "../frameIdentifiers"; import {IPicture, Picture, PictureType} from "../../picture"; import {Guards} from "../../utils"; +import {IFileAbstraction} from "../../fileAbstraction"; +import PictureLazy from "../../pictureLazy"; export default class AttachmentFrame extends Frame implements IPicture { // NOTE: It probably doesn't look necessary to implement IPicture, but it makes converting a @@ -16,8 +18,8 @@ export default class AttachmentFrame extends Frame implements IPicture { private _encoding: StringType = Id3v2Settings.defaultEncoding; private _filename: string; private _mimeType: string; - private _rawPicture: IPicture; private _rawData: ByteVector; + private _rawPicture: IPicture; private _rawVersion: number; private _type: PictureType; @@ -63,12 +65,42 @@ export default class AttachmentFrame extends Frame implements IPicture { public static fromPicture(picture: IPicture): AttachmentFrame { Guards.truthy(picture, "picture"); - // In this case we will assume the frame is an APIC until the picture is parsed - const frame = new AttachmentFrame(new Id3v2FrameHeader(FrameIdentifiers.APIC)); + // NOTE: We assume the frame is an APIC frame of size 1 until we parse it and find out + // otherwise. + const frame = new AttachmentFrame(new Id3v2FrameHeader(FrameIdentifiers.APIC, undefined, 1)); frame._rawPicture = picture; return frame; } + /** + * Constructs and initializes a new attachment frame by populating it with the contents of a + * section of a file. This constructor is only meant to be used by the {@see FrameFactory} + * class. All loading is done lazily. + * @param file File to load frame data from + * @param header ID3v2 frame header that defines the frame + * @param frameStart Index into the file where the frame starts + * @param size Length of the frame data + * @param version ID3v2 version the frame was originally encoded with + */ + // @TODO: Make lazy loading optional + public static fromFile( + file: IFileAbstraction, + header: Id3v2FrameHeader, + frameStart: number, + size: number, + version: number + ): AttachmentFrame { + Guards.truthy(file, "file"); + Guards.truthy(header, "header"); + Guards.uint(frameStart, "frameStart"); + Guards.uint(size, "size"); + + const frame = new AttachmentFrame(header); + frame._rawPicture = PictureLazy.fromFile(file, frameStart, size); + frame._rawVersion = version; + return frame; + } + /** * Constructs and initializes a new attachment frame by reading its raw data in a specified * Id3v2 version. @@ -323,7 +355,13 @@ export default class AttachmentFrame extends Frame implements IPicture { if (this._rawData) { this.parseFromRawData(); } else if (this._rawPicture) { - this.parseFromRawPicture(); + if (this._rawVersion !== undefined) { + this._rawData = this._rawPicture.data; + this._rawPicture = undefined; + this.parseFromRawData(); + } else { + this.parseFromRawPicture(); + } } } @@ -392,15 +430,16 @@ export default class AttachmentFrame extends Frame implements IPicture { const mimeTypeLength = mimeTypeEndIndex - 1; this._mimeType = data.toString(mimeTypeLength, StringType.Latin1, 1); - const filenameEndIndex = data.find(delim, mimeTypeEndIndex + 1); + const filenameEndIndex = data.find(delim, mimeTypeEndIndex + 1, delim.length); const filenameLength = filenameEndIndex - mimeTypeEndIndex - 1; this._filename = data.toString(filenameLength, this._encoding, mimeTypeEndIndex + 1); - descriptionEndIndex = data.find(delim, filenameEndIndex + delim.length); + descriptionEndIndex = data.find(delim, filenameEndIndex + delim.length, delim.length); const descriptionLength = descriptionEndIndex - filenameEndIndex - delim.length; this._description = data.toString(descriptionLength, this._encoding, filenameEndIndex + delim.length); this._data = data.mid(descriptionEndIndex + delim.length); + this._type = PictureType.NotAPicture; } else { // Unsupported throw new Error("Unsupported: AttachmentFrame cannot be used for frame IDs other than GEOB or APIC"); @@ -418,6 +457,7 @@ export default class AttachmentFrame extends Frame implements IPicture { this._filename = picture.filename; this._mimeType = picture.mimeType; this._type = picture.type; + this._header.frameSize = this._data.length; this._encoding = Id3v2Settings.defaultEncoding; diff --git a/src/id3v2/frames/frameFactory.ts b/src/id3v2/frames/frameFactory.ts index bc577df3..72acc96e 100644 --- a/src/id3v2/frames/frameFactory.ts +++ b/src/id3v2/frames/frameFactory.ts @@ -1,7 +1,6 @@ import AttachmentFrame from "./attachmentFrame"; import CommentsFrame from "./commentsFrame"; import MusicCdIdentifierFrame from "./musicCdIdentifierFrame"; -import PictureLazy from "../../pictureLazy"; import PlayCountFrame from "./playCountFrame"; import PopularimeterFrame from "./popularimeterFrame"; import PrivateFrame from "./privateFrame"; @@ -97,8 +96,9 @@ export default { } const header = Id3v2FrameHeader.fromData(data.mid(position, frameHeaderSize), version); - const filePosition = offset + frameHeaderSize; - offset += header.frameSize + frameHeaderSize; + const frameStartIndex = offset + frameHeaderSize; + const frameEndIndex = offset + header.frameSize + frameHeaderSize; + const frameSize = frameEndIndex - frameStartIndex; // Illegal frames are filtered out when creating the frame header @@ -126,7 +126,7 @@ export default { if (frame) { return { frame: frame, - offset: offset + offset: frameEndIndex }; } } @@ -139,17 +139,17 @@ export default { if (file) { // Attached picture (frames 4.14) // General encapsulated object (frames 4.15) + // TODO: Make lazy loading optional if (header.frameId === FrameIdentifiers.APIC || header.frameId === FrameIdentifiers.GEOB) { - const picture = PictureLazy.fromFile(file.fileAbstraction, filePosition, offset - filePosition); return { - frame: AttachmentFrame.fromPicture(picture), - offset: offset + frame: AttachmentFrame.fromFile(file.fileAbstraction, header, frameStartIndex, frameSize, version), + offset: frameEndIndex }; } // Read remaining part of the frame for the non lazy Frame - file.seek(filePosition); - data.addByteVector(file.readBlock(offset - filePosition)); + file.seek(frameStartIndex); + data.addByteVector(file.readBlock(frameSize)); } let func; @@ -157,7 +157,7 @@ export default { // User text identification frame func = UserTextInformationFrame.fromOffsetRawData; } else if (header.frameId.isTextFrame) { - // Text identifiacation frame (frames 4.2) Starts with T + // Text identifiacation frame (frames 4.2) func = TextInformationFrame.fromOffsetRawData; } else if (header.frameId === FrameIdentifiers.UFID) { // Unique file identifier (frames 4.1) @@ -196,7 +196,7 @@ export default { // User URL link func = UserUrlLinkFrame.fromOffsetRawData; } else if (header.frameId.isUrlFrame) { - // URL link (frame 4.3.1) starts with 'W' + // URL link (frame 4.3.1) func = UrlLinkFrame.fromOffsetRawData; } else if (header.frameId === FrameIdentifiers.ETCO) { // Event timing codes (frames 4.6) @@ -208,7 +208,7 @@ export default { return { frame: func(data, position, header, version), - offset: offset + offset: frameEndIndex }; } catch (e) { if (CorruptFileError.errorIs(e) || NotImplementedError.errorIs(e)) { @@ -218,7 +218,7 @@ export default { // Other exceptions will just mean we ignore the frame return { frame: undefined, - offset: offset + offset: frameEndIndex }; } } diff --git a/src/id3v2/frames/textInformationFrame.ts b/src/id3v2/frames/textInformationFrame.ts index 73459675..466ec6c2 100644 --- a/src/id3v2/frames/textInformationFrame.ts +++ b/src/id3v2/frames/textInformationFrame.ts @@ -4,7 +4,7 @@ import {ByteVector, StringType} from "../../byteVector"; import {Frame, FrameClassType} from "./frame"; import {Id3v2FrameHeader} from "./frameHeader"; import {FrameIdentifier, FrameIdentifiers} from "../frameIdentifiers"; -import {Guards, StringComparison} from "../../utils"; +import {Guards, StringComparison, StringUtils} from "../../utils"; /** * This class provides support for ID3v2 text information frames (section 4.2) covering `T000` to @@ -128,8 +128,24 @@ import {Guards, StringComparison} from "../../utils"; * (TIT2) for sorting purposes. */ export class TextInformationFrame extends Frame { + private static COVER_ABBREV = "CR"; private static COVER_STRING = "Cover"; + private static REMIX_ABBREV = "RX"; private static REMIX_STRING = "Remix"; + private static SPLIT_FRAME_TYPES = [ + FrameIdentifiers.TCOM, + FrameIdentifiers.TEXT, + FrameIdentifiers.TMCL, + FrameIdentifiers.TOLY, + FrameIdentifiers.TOPE, + FrameIdentifiers.TSOC, + FrameIdentifiers.TSOP, + FrameIdentifiers.TSO2, + FrameIdentifiers.TPE1, + FrameIdentifiers.TPE2, + FrameIdentifiers.TPE3, + FrameIdentifiers.TPE4 + ]; protected _encoding: StringType = Id3v2Settings.defaultEncoding; protected _rawData: ByteVector; @@ -346,8 +362,25 @@ export class TextInformationFrame extends Frame { const fieldList = []; const delim = ByteVector.getTextDelimiter(this._encoding); - if (this._rawVersion > 3 || this.frameId === FrameIdentifiers.TXXX) { - fieldList.push(... data.toStrings(this._encoding, 1)); + if (this._rawVersion > 3 && this.frameId === FrameIdentifiers.TCON) { + // TCON on ID3v2.4 is encoded as a separate field for each genre. Fields can either be + // the old numeric ID3v1 genres (no parenthesis) or free text. RX/CR can also be used. + // This way is **much** better... + const genres = data.toStrings(this._encoding, 1); + const textGenres = genres.map((g) => { + switch (g) { + case TextInformationFrame.COVER_ABBREV: + return TextInformationFrame.COVER_STRING; + case TextInformationFrame.REMIX_ABBREV: + return TextInformationFrame.REMIX_STRING; + default: + const textGenre = Genres.indexToAudio(g, false); + return textGenre || g; + } + }); + fieldList.push(...textGenres); + } else if (this._rawVersion > 3 || this.frameId === FrameIdentifiers.TXXX) { + fieldList.push(...data.toStrings(this._encoding, 1)); } else if (data.length > 1 && !ByteVector.equal(data.mid(1, delim.length), delim)) { let value = data.toString(data.length - 1, this._encoding, 1); @@ -357,74 +390,61 @@ export class TextInformationFrame extends Frame { value = value.substring(0, nullIndex); } - const splitFrameTypes = [ - FrameIdentifiers.TCOM, - FrameIdentifiers.TEXT, - FrameIdentifiers.TMCL, - FrameIdentifiers.TOLY, - FrameIdentifiers.TOPE, - FrameIdentifiers.TSOC, - FrameIdentifiers.TSOP, - FrameIdentifiers.TSO2, - FrameIdentifiers.TPE1, - FrameIdentifiers.TPE2, - FrameIdentifiers.TPE3, - FrameIdentifiers.TPE4 - ]; - if (splitFrameTypes.some((ft) => ft === this.frameId)) { + if (TextInformationFrame.SPLIT_FRAME_TYPES.some((ft) => ft === this.frameId)) { // Some frames are designed to be split into multiple parts by a / fieldList.push(... value.split("/")); } else if (this.frameId === FrameIdentifiers.TCON) { - // TCON can take various formats. The ID3v2.3 docs specify it can be: + // TCON in ID3v2.2 and ID3v2.3 is specified as // * (xx) - where xx is a number from the ID3v1 genre list - // * (xx)yyy - where xx is a number from the ID3v1 genre list and yyy is a - // "refinement" of the genre - // * genrename - just a genre name - // * (RX) - a remix - // * (CR) - a cover - // * (( - used to escape a '(' in a refinement/genre name - // * Any combination of the above - - // Although this could probably be expressed with a ridiculous regex, we're just - // going to do it with a single iteration over the value - const buffer = []; - let index = 0; - let insideParen = false; - while (index < value.length) { - if (value[index] === "(") { - if (index < value.length - 1 && value[index + 1] === "(") { - // This is an escaped paren - buffer.push("("); - index++; - } else { - // We opened a parenthesis block - insideParen = true; - - // If there are bytes in the buffer, add them to the field list - TextInformationFrame.addBufferToFieldList(buffer, fieldList); - } - } else if (value[index] === ")" && insideParen) { - // We just closed a parenthesis block, store the string - if (buffer.length === 2 && buffer[0] === "R" && buffer[1] === "X") { - fieldList.push(TextInformationFrame.REMIX_STRING); - } else if (buffer.length === 2 && buffer[0] === "C" && buffer[1] === "R") { - fieldList.push(TextInformationFrame.COVER_STRING); - } else { - fieldList.push(buffer.join("")); - } + // * (xx)yy - where xx is a number from the ID3v1 genre list and yyy is a + // "refinement" of the genre + // * (RX) - "Remix" + // * (CR) - "Cover" + // * (( - used to escape a "(" in a refinement/genre name + + // NOTE: This encoding has and inherent flaw around how multiple genres should be + // encoded. Since multiple genres are already an edge case, I'm just going to + // say yolo to this whole block of code copied over from the .NET implementation + + while (value.length > 1 && value[0] === "(") { + const closing = value.indexOf(")"); + if (closing < 0) { + break; + } - buffer.length = 0; - insideParen = false; + const number = value.substr(1, closing - 1); + + let text: string; + if (number === TextInformationFrame.COVER_ABBREV) { + text = TextInformationFrame.COVER_STRING; + } else if (number === TextInformationFrame.REMIX_ABBREV) { + text = TextInformationFrame.REMIX_STRING; } else { - // Some other character - buffer.push(value[index]); + text = Genres.indexToAudio(number, true); + } + + if (!text) { + // Number in parenthesis was not a numeric genre but part of a larger bit + // of text? + break; } - index++; + // Number in parenthesis was a numeric genre + fieldList.push(text); + value = StringUtils.trimStart(value.substr(closing + 1), "/ "); + + // Ignore genre if the same genre appears after the numeric genre + if (value.startsWith(text)) { + value = StringUtils.trimStart(value.substr(text.length), "/ "); + } } - // Clear the buffer - TextInformationFrame.addBufferToFieldList(buffer, fieldList); + // Split the remaining genre value by dividers + // NOTE: Nowhere in the spec is this specified! + if (value.length > 0) { + const splitValues = value.split(/[\/;]/).map((v) => v.replace(/\(\(/, "(")); + fieldList.push(...splitValues); + } } else { fieldList.push(value); } @@ -452,6 +472,28 @@ export class TextInformationFrame extends Frame { v.addByte(encoding); + // Pre-process ID3v2.4 TCON frames + if (version > 3 && this.frameId === FrameIdentifiers.TCON) { + // For ID3v2.4, we should encode any genres that can be numeric as numeric by + // themselves. This then gets encoded the same as any other ID3v2.4 text frame (ie, + // with delimiters in between values) + text = text.map((g) => { + switch (g) { + case TextInformationFrame.COVER_STRING: + return TextInformationFrame.COVER_ABBREV; + case TextInformationFrame.REMIX_STRING: + return TextInformationFrame.REMIX_ABBREV; + default: + if (Id3v2Settings.useNumericGenres) { + const numericGenre = Genres.audioToIndex(g); + return numericGenre === 255 ? g : numericGenre.toString(); + } + return g; + } + }); + } + + // Main processing const isTxxx = this.frameId === FrameIdentifiers.TXXX; if (version > 3 || isTxxx) { if (isTxxx) { @@ -474,24 +516,39 @@ export class TextInformationFrame extends Frame { } } } else if (this.frameId === FrameIdentifiers.TCON) { - let data = ""; + // ID3v2.2 and ID3v2.3 TCON frames are going to be written with numeric genres first + // (if enabled) and multiple text-based genres separated by ;. + // NOTE: This doesn't follow the actual conventions for ID3v2.2/3 but nobody does this + // correctly. This implementation will at least work with MinimServer + // https://forum.minimserver.com/showthread.php?tid=2575 + const numericGenres = []; + const textGenres = []; for (const s of text) { - if (s === TextInformationFrame.COVER_STRING) { - data += "(CR)"; - } else if (s === TextInformationFrame.REMIX_STRING) { - data += "(RX)"; - } else { - const id = parseInt(s, 10); - if (!Number.isNaN(id)) { - data += `(${id})`; - } else { - data += s.replace("(", "(("); - } + switch (s) { + case TextInformationFrame.COVER_STRING: + numericGenres.push(`(${TextInformationFrame.COVER_ABBREV})`); + break; + case TextInformationFrame.REMIX_STRING: + numericGenres.push(`(${TextInformationFrame.REMIX_ABBREV})`); + break; + default: + if (Id3v2Settings.useNumericGenres) { + const numericGenre = Genres.audioToIndex(s); + if (numericGenre !== 255) { + numericGenres.push(`(${numericGenre})`); + break; + } + } + textGenres.push(s.replace(/\(/, "((")); + break; } } - v.addByteVector(ByteVector.fromString(data, encoding)); + // Put the entire string together + const genreString = `${numericGenres.join("")}${textGenres.join(";")}`; + v.addByteVector(ByteVector.fromString(genreString, encoding)); } else { + // Fields that have slashes in them and fields that don't v.addByteVector(ByteVector.fromString(text.join("/"), encoding)); } @@ -499,31 +556,6 @@ export class TextInformationFrame extends Frame { } // #endregion - - private static addBufferToFieldList(buffer: string[], previousStrings: string[]): void { - if (buffer.length === 0) { - return; - } - - const output = buffer.join(""); - - // Attempt to convert the string to a genre number - const genreNumber = Genres.audioToIndex(output); - if (genreNumber === 255) { - // String isn't a genre, so it should be stored - previousStrings.push(output); - } else { - // String is a genre, only store it if the genre number isn't stored in the previous strings - if ( - previousStrings.length === 0 || - previousStrings[previousStrings.length - 1] !== genreNumber.toString(10) - ) { - previousStrings.push(output); - } - } - - buffer.length = 0; - } } export class UserTextInformationFrame extends TextInformationFrame { diff --git a/src/id3v2/id3v2Settings.ts b/src/id3v2/id3v2Settings.ts index 25baa394..73a05712 100644 --- a/src/id3v2/id3v2Settings.ts +++ b/src/id3v2/id3v2Settings.ts @@ -6,6 +6,7 @@ export default class Id3v2Settings { private static _defaultVersion: number = 3; private static _forceDefaultEncoding: boolean = false; private static _forceDefaultVersion: boolean = false; + private static _strictFramesForVersion: boolean = false; private static _useNumericGenres: boolean = true; // @TODO: DO WE HAVE TO??? /** @@ -92,5 +93,22 @@ export default class Id3v2Settings { */ public static set useNumericGenres(value: boolean) { this._useNumericGenres = value; } + /** + * Gets whether or not attempting to write a frame that is unsupported in the desired version + * will throw an error. + * If `true` writing a frame that is not supported in the desired version will throw an error + * during the render process. If `false` if a frame is not supported in the desired version it + * will be omitted from rendering and no error will be thrown. + */ + public static get strictFrameForVersion(): boolean { return this._strictFramesForVersion; } + /** + * Sets whether or not attempting to write a frame that is unsupported in the desired version + * will throw an error. + * If `true` writing a frame that is not supported in the desired version will throw an error + * during the render process. If `false` if a frame is not supported in the desired version it + * will be omitted from rendering and no error will be thrown. + */ + public static set strictFrameForVersion(value: boolean) { this._strictFramesForVersion = value; } + // @TODO: Add flag for disabling iTunes-only frames } diff --git a/src/id3v2/id3v2Tag.ts b/src/id3v2/id3v2Tag.ts index 8d2dad8e..e88917f9 100644 --- a/src/id3v2/id3v2Tag.ts +++ b/src/id3v2/id3v2Tag.ts @@ -2,15 +2,15 @@ import * as DateFormat from "dateformat"; import AttachmentFrame from "./frames/attachmentFrame"; import CommentsFrame from "./frames/commentsFrame"; -import Id3v2ExtendedHeader from "./id3v2ExtendedHeader"; import FrameFactory from "./frames/frameFactory"; -import Genres from "../genres"; +import Id3v2ExtendedHeader from "./id3v2ExtendedHeader"; +import Id3v2TagFooter from "./id3v2TagFooter"; import Id3v2Settings from "./id3v2Settings"; import SyncData from "./syncData"; import UniqueFileIdentifierFrame from "./frames/uniqueFileIdentifierFrame"; import UnsynchronizedLyricsFrame from "./frames/unsynchronizedLyricsFrame"; import {ByteVector, StringType} from "../byteVector"; -import {CorruptFileError} from "../errors"; +import {CorruptFileError, NotImplementedError, NotSupportedError} from "../errors"; import {File, FileAccessMode, ReadStyle} from "../file"; import {Frame, FrameClassType} from "./frames/frame"; import {FrameIdentifier, FrameIdentifiers} from "./frameIdentifiers"; @@ -21,7 +21,6 @@ import {Tag, TagTypes} from "../tag"; import {TextInformationFrame, UserTextInformationFrame} from "./frames/textInformationFrame"; import {UrlLinkFrame} from "./frames/urlLinkFrame"; import {Guards} from "../utils"; -import Id3v2TagFooter from "./id3v2TagFooter"; export default class Id3v2Tag extends Tag { private static _language: string = undefined; // @TODO: Use the os-locale module to supply a @@ -370,23 +369,7 @@ export default class Id3v2Tag extends Tag { /** @inheritDoc via TCON frame */ get genres(): string[] { - const text = this.getTextAsArray(FrameIdentifiers.TCON); - if (text.length === 0) { return text; } - - const list = []; - for (const genre of text) { - if (!genre) { continue; } - - // The string may just be a genre number - const genreFromIndex = Genres.indexToAudio(genre); - if (genreFromIndex) { - list.push(genreFromIndex); - } else { - list.push(genre); - } - } - - return list; + return this.getTextAsArray(FrameIdentifiers.TCON); } /** @inheritDoc via TCON frame */ set genres(value: string[]) { @@ -396,15 +379,7 @@ export default class Id3v2Tag extends Tag { } // Clone the array so changes made won't affect the passed array - value = value.slice(); - for (let i = 0; i < value.length; i++) { - const index = Genres.audioToIndex(value[i]); - if (index !== 255) { - value[i] = index.toString(); - } - } - - this.setTextFrame(FrameIdentifiers.TCON, ...value); + this.setTextFrame(FrameIdentifiers.TCON, ...value.slice()); } /** @@ -467,8 +442,13 @@ export default class Id3v2Tag extends Tag { return; } - // Case 3: We have neither type of frame, create a TDRC frame - this.setNumberFrame(FrameIdentifiers.TDRC, value, 0); + // Case 3: We have neither type of frame, create the frame for the version of tag on disk + if (this.version > 3) { + this.setNumberFrame(FrameIdentifiers.TDRC, value, 0); + } else { + this.setNumberFrame(FrameIdentifiers.TYER, value, 0); + } + } /** @inheritDoc via TRCK frame */ @@ -872,9 +852,9 @@ export default class Id3v2Tag extends Tag { * @returns ByteVector The rendered tag. */ public render(): ByteVector { - // Convert the perfmers role to the TMCL frame + // Convert the performers role to the TMCL frame + let performersRoleList: string[] = []; if (this._performersRole) { - const ret: string[] = undefined; const map: {[key: string]: string} = {}; for (let i = 0; i < this._performersRole.length; i++) { const insts = this._performersRole[i]; @@ -898,14 +878,18 @@ export default class Id3v2Tag extends Tag { } // Convert dictionary to string - for (const key of map.keys) { - ret.push(key); - ret.push(map[key]); + performersRoleList = new Array(Object.keys(map).length * 2); + for (const key in map) { + if (!map.hasOwnProperty(key)) { + continue; + } + performersRoleList.push(key); + performersRoleList.push(map[key]); } - - this.setTextFrame(FrameIdentifiers.TMCL, ...ret); } + this.setTextFrame(FrameIdentifiers.TMCL, ...performersRoleList); + // We need to render the "tag data" first so that we have to correct size to render in the // tag's header. The "tag data" (everything that is included in Header.tagSize) includes // the extended header, frames and padding, but does not include the tag's header or footer @@ -935,8 +919,11 @@ export default class Id3v2Tag extends Tag { try { tagData.addByteVector(frame.render(this._header.majorVersion)); } catch (e) { - // Swallow unimplemented exceptions - if (!e.hasOwnProperty("isNotImplementedError")) { + if (NotImplementedError.errorIs(e)) { + // Swallow not implemented errors + } else if (NotSupportedError.errorIs(e) && !Id3v2Settings.strictFrameForVersion) { + // Ignore not supported errors if we're not in strict frame mode + } else { throw e; } } @@ -1211,8 +1198,11 @@ export default class Id3v2Tag extends Tag { const frames = this.getFramesByClassType(FrameClassType.UserTextInformationFrame); let frame = UserTextInformationFrame.findUserTextInformationFrame(frames, description, caseSensitive); - if (!text && frame) { - this.removeFrame(frame); + if (!text) { + // Remove the frame if it exists, otherwise do nothing + if (frame) { + this.removeFrame(frame); + } } else { frame = UserTextInformationFrame.fromDescription(description, Id3v2Settings.defaultEncoding); frame.text = text.split(";"); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..c778f437 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,73 @@ +// BASE EXPORTS //////////////////////////////////////////////////////////// +// Base/Support classes +export {ByteVector, StringType} from "./byteVector"; +export {CorruptFileError, NotImplementedError} from "./errors"; +export {File, FileAccessMode, FileTypeConstructor, FileTypeResolver, ReadStyle} from "./file"; +export {LocalFileAbstraction} from "./fileAbstraction"; + +// Base Tag Classes +export {default as CombinedTag} from "./combinedTag"; +export {default as Genres} from "./genres"; +export {ICodec, IAudioCodec, ILosslessAudioCodec, IVideoCodec, IPhotoCodec, MediaTypes} from "./iCodec"; +export {IPicture, Picture, PictureType} from "./picture"; +export {default as PictureLazy} from "./pictureLazy"; +export {default as Properties} from "./properties"; +export {Tag, TagTypes} from "./tag"; + +// ID3v2 /////////////////////////////////////////////////////////////////// +export {default as Id3v2ExtendedHeader} from "./id3v2/id3v2ExtendedHeader"; +export {default as Id3v2FrameFactory} from "./id3v2/frames/frameFactory"; +export { + FrameIdentifier as Id3v2FrameIdentifier, + FrameIdentifiers as Id3v2FrameIdentifiers, +} from "./id3v2/frameIdentifiers"; +export {default as Id3v2Settings} from "./id3v2/id3v2Settings"; +export {default as Id3v2Tag} from "./id3v2/id3v2Tag"; +export {default as Id3v2TagFooter} from "./id3v2/id3v2TagFooter"; +export {Id3v2TagHeader, Id3v2TagHeaderFlags} from "./id3v2/id3v2TagHeader"; +export { + SynchronizedTextType as Id3v2SynchronizedTextType, + TimestampFormat as Id3v2TimestampFormat, + EventType as Id3v2EventType +} from "./id3v2/utilTypes"; + +// Frames +export {default as Id3v2AttachmentFrame} from "./id3v2/frames/attachmentFrame"; +export {default as Id3v2CommentsFrame} from "./id3v2/frames/commentsFrame"; +export { + EventTimeCode as Id3v2EventTimeCode, + EventTimeCodeFrame as Id3v2EventTimeCodeFrame +} from "./id3v2/frames/eventTimeCodeFrame"; +export { + Frame as Id3v2Frame, + FrameClassType as Id3v2FrameClassType +} from "./id3v2/frames/frame"; +export {Id3v2FrameFlags, Id3v2FrameHeader} from "./id3v2/frames/frameHeader"; +export {default as Id3v2MusicCdIdentifierFrame} from "./id3v2/frames/musicCdIdentifierFrame"; +export {default as Id3v2PlayCountFrame} from "./id3v2/frames/playCountFrame"; +export {default as Id3v2PopularimeterFrame} from "./id3v2/frames/popularimeterFrame"; +export {default as Id3v2PrivateFrame} from "./id3v2/frames/privateFrame"; +export { + ChannelData as Id3v2RelativeVolumeFrameChannelData, + ChannelType as Id3v2RelativeVolumeFrameChannelType, + RelativeVolumeFrame as Id3v2RelativeVolumeFrame +} from "./id3v2/frames/relativeVolumeFrame"; +export { + SynchronizedLyricsFrame as Id3v2Synchronized, + SynchronizedText as Id3v2SynchronizedLyricsFrame +} from "./id3v2/frames/synchronizedLyricsFrame"; +export {default as Id3v2TermsOfUseFrame} from "./id3v2/frames/termsOfUseFrame"; +export { + TextInformationFrame as Id3v2TextInformationFrame, + UserTextInformationFrame as Id3v2UserTextInformationFrame +} from "./id3v2/frames/textInformationFrame"; +export {default as Id3v2UniqueFileIdentifierFrame} from "./id3v2/frames/uniqueFileIdentifierFrame"; +export {default as Id3v2UnknownFrame} from "./id3v2/frames/unknownFrame"; +export {default as Id3v2UnsynchronizedFrame} from "./id3v2/frames/unsynchronizedLyricsFrame"; +export { + UrlLinkFrame as Id3v2UrlLinkFrame, + UserUrlLinkFrame as Id3v2UserUrlLinkFrame +} from "./id3v2/frames/urlLinkFrame"; + +// MPEG //////////////////////////////////////////////////////////////////// +export {default as MpegAudioFile} from "./mpeg/audioFile"; diff --git a/src/mpeg/audioFile.ts b/src/mpeg/audioFile.ts new file mode 100644 index 00000000..e7a56311 --- /dev/null +++ b/src/mpeg/audioFile.ts @@ -0,0 +1,93 @@ +import NonContainerTag from "../nonContainer/nonContainerTag"; +import NonContainerFile from "../nonContainer/nonContainerFile"; +import Properties from "../properties"; +import {AudioHeader} from "./audioHeader"; +import {CorruptFileError} from "../errors"; +import {File, ReadStyle} from "../file"; +import {IFileAbstraction} from "../fileAbstraction"; +import {Tag, TagTypes} from "../tag"; + +export default class AudioFile extends NonContainerFile { + private _firstHeader: AudioHeader; + + public constructor(file: IFileAbstraction|string, propertiesStyle: ReadStyle) { + super(file, propertiesStyle); + } + + /** + * Gets a tag of a specified type from the current instance, optionally creating a new tag if + * possible. + * If an {@see Id3v2Tag} is added to the current instance, it will be placed at the start of + * the file. On the other hand, {@see Id3v1Tag} and {@see ApeTag} will be added to the end of + * the file. All other tag types will be ignored. + * @param type Type of tag to create + * @param create Whether or not to try and create the tag if one is not found + * @returns Tag Tag that was found in or added to the current instance. If no matching tag was + * found and none was created, `undefined` is returned. + */ + public getTag(type: TagTypes, create: boolean): Tag { + const t = ( this.tag).getTag(type); + if (t || !create) { + return t; + } + + switch (type) { + case TagTypes.Id3v1: + return this.endTag.addTag(type, this.tag); + case TagTypes.Id3v2: + return this.startTag.addTag(type, this.tag); + case TagTypes.Ape: + return this.endTag.addTag(type, this.tag); + default: + return undefined; + } + } + + protected readEnd(end: number, propertiesStyle: ReadStyle): void { + // Make sure we have Id3v1 and Id3v2 tags + // @TODO: This is a kinda sleazy way of adding a ID3v2 tag if we didn't read one at the start + // NOTE: The reason for adding the ID3v1 and ID3v2 tags is because this library is meant to + // be tag type agnostic if desired. That means, a user should be able to just add + // whatever fields they want and it goes into the right tag. Since ID3v1 doesn't support + // many fields, we need to create an ID3v2 tag to ensure all fields can be written to. + this.getTag(TagTypes.Id3v1, true); + this.getTag(TagTypes.Id3v2, true); + } + + protected readProperties(start: number, end: number, propertiesStyle: ReadStyle): Properties { + this._firstHeader.streamLength = end - start; + return new Properties(0, [this._firstHeader]); + } + + protected readStart(start: number, propertiesStyle: ReadStyle): void { + // Only check the first 16 bytes so we're not stuck reading a bad file forever + if ((propertiesStyle & ReadStyle.Average) !== 0) { + const findResult = AudioHeader.find(this, start, 0x4000); + this._firstHeader = findResult.header; + if (!findResult.success) { + throw new CorruptFileError("MPEG audio header not found"); + } + } + } +} + +//////////////////////////////////////////////////////////////////////////// +// Register the file type +const mimeTypes = [ + "taglib/mp3", + "audio/x-mp3", + "application/x-id3", + "audio/mpeg", + "audio/x-mpeg", + "audio/x-mpeg-3", + "audio/mpeg3", + "audio/mp3", + "taglib/m2a", + "taglib/mp2", + "taglib/mp1", + "audio/x-mp2", + "audio/x-mp1" +]; +for (const mimeType of mimeTypes) { + File.addFileType(mimeType, AudioFile); +} diff --git a/src/mpeg/audioHeader.ts b/src/mpeg/audioHeader.ts new file mode 100644 index 00000000..6c0d8958 --- /dev/null +++ b/src/mpeg/audioHeader.ts @@ -0,0 +1,408 @@ +import XingHeader from "./xingHeader"; +import VbriHeader from "./vbriHeader"; +import {ByteVector} from "../byteVector"; +import {CorruptFileError} from "../errors"; +import {File} from "../file"; +import {IAudioCodec, MediaTypes} from "../iCodec"; +import {ChannelMode, MpegVersion} from "./mpegEnums"; +import {Guards} from "../utils"; + +/** + * Provides information about an MPEG audio stream. For more information and definition of the + * header, see http://www.mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm + */ +export class AudioHeader implements IAudioCodec { + public static readonly Unknown: AudioHeader = AudioHeader.fromInfo(0, 0, XingHeader.unknown, VbriHeader.unknown); + + private static readonly bitrates: number[][][] = [ + [ // Version 1 + [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1], // layer 1 + [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1], // layer 2 + [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1] // layer 3 + ], + [ // Version 2 or 2.5 + [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, -1], // layer 1 + [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1], // layer 2 + [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1] // layer 3 + ] + ]; + + private static readonly blockSize: number[][] = [ + [0, 384, 1152, 1152], // Version 1 + [0, 384, 1152, 576], // Version 2 + [0, 384, 1152, 576] // Version 2.5 + ]; + + private static readonly sampleRates: number[][] = [ + [44100, 48000, 32000, 0], // Version 1 + [22050, 24000, 16000, 0], // Version 2 + [11025, 12000, 8000, 0] // Version 2.5 + ]; + + private _durationMilliseconds: number; + private _flags: number; + private _streamLength: number; + private _vbriHeader: VbriHeader; + private _xingHeader: XingHeader; + + // #region Constructors + + private constructor() {} + + /** + * Constructs and initializes a new instance by reading its contents from a data + * {@see ByteVector} and its Xing header from the appropriate location in the + * specified file. + * @param data The header data to read + * @param file File to read the Xing/VBRI header from + * @param position Position into {@paramref file} where the header begins, must be a positive + * 8-bit integer. + */ + public static fromData(data: ByteVector, file: File, position: number) { + Guards.truthy(data, "data"); + Guards.truthy(file, "file"); + Guards.uint(position, "position"); + + const header = new AudioHeader(); + header._durationMilliseconds = 0; + header._streamLength = 0; + + const error = this.getHeaderError(data); + if (error) { + throw new CorruptFileError(error); + } + + header._flags = data.toUInt(); + header._xingHeader = XingHeader.unknown; + header._vbriHeader = VbriHeader.unknown; + + // Check for a Xing header that will help us in gathering info about a VBR stream + file.seek(position + XingHeader.xingHeaderOffset(header.version, header.channelMode)); + + const xingData = file.readBlock(16); + if (xingData.length === 16 && xingData.startsWith(XingHeader.fileIdentifier)) { + header._xingHeader = XingHeader.fromData(xingData); + } + + if (header._xingHeader.isPresent) { + return header; + } + + // A Xing header could not be found, next check for a Fraunhofer VBRI header + file.seek(position + VbriHeader.vbriHeaderOffset); + + // Only get the first 24 bytes of the header. We're not interested in the TOC entries. + const vbriData = file.readBlock(24); + if (vbriData.length === 24 && vbriData.startsWith(VbriHeader.fileIdentifier)) { + header._vbriHeader = VbriHeader.fromData(vbriData); + } + + return header; + } + + /** + * Constructs and initializes a new instance by populating it with specified values. + * @param flags Flags for the new instance + * @param streamLength Stream length of the new instance + * @param xingHeader Xing header associated with the new instance + * @param vbriHeader VBRI header associated with the new instance + */ + public static fromInfo( + flags: number, + streamLength: number, + xingHeader: XingHeader, + vbriHeader: VbriHeader + ): AudioHeader { + Guards.uint(flags, "flags"); + Guards.uint(streamLength, "streamLength"); + Guards.truthy(xingHeader, "xingHeader"); + Guards.truthy(vbriHeader, "vbriHeader"); + + const header = new AudioHeader(); + header._flags = flags; + header._streamLength = streamLength; + header._xingHeader = xingHeader; + header._vbriHeader = vbriHeader; + header._durationMilliseconds = 0; + + return header; + } + + // #endregion + + // #region Properties + + /** @inheritDoc IAudioCodec.audioBitrate */ + public get audioBitrate(): number { + // NOTE: Although it would be *amazing* to store `this.durationMilliseconds / 1000` in a + // variable, we can't b/c it causes a stack overflow. Oh well. + if ( + this._xingHeader.totalSize > 0 && + this._xingHeader.totalFrames > 0 && + this.durationMilliseconds / 1000 > 0 + ) { + return Math.round(this._xingHeader.totalSize * 8 / (this.durationMilliseconds / 1000) / 1000); + } + if ( + this._vbriHeader.totalSize > 0 && + this._vbriHeader.totalFrames > 0 && + this.durationMilliseconds / 1000 > 0 + ) { + return Math.round(this._vbriHeader.totalSize * 8 / (this.durationMilliseconds / 1000) / 1000); + } + + const index1 = this.version === MpegVersion.Version1 ? 0 : 1; + const index2 = this.audioLayer - 1; + const index3 = (this._flags >> 12) & 0x0f; + return AudioHeader.bitrates[index1][index2][index3]; + } + + /** @inheritDoc IAudioCodec.audioChannels */ + public get audioChannels(): number { return this.channelMode === ChannelMode.SingleChannel ? 1 : 2; } + + /** + * Gets the length of the frames in the audio represented by the current instance. + */ + public get audioFrameLength(): number { + const audioLayer = this.audioLayer; + if (audioLayer === 1) { + return Math.floor(48000 * this.audioBitrate / this.audioSampleRate) + (this.isPadded ? 4 : 0); + } + if (audioLayer === 2 || this.version === MpegVersion.Version1) { + return Math.floor(144000 * this.audioBitrate / this.audioSampleRate) + (this.isPadded ? 1 : 0); + } + if (audioLayer === 3) { + return Math.floor(72000 * this.audioBitrate / this.audioSampleRate) + (this.isPadded ? 1 : 0); + } + return 0; + } + + /** + * Gets the MPEG audio layer used to encode the audio represented by the current instance. + */ + public get audioLayer(): number { + switch ((this._flags >> 17) & 0x03) { + case 1: + return 3; + case 2: + return 2; + default: + return 1; + } + } + + /** @inheritDoc IAudioCodec.audioSampleRate */ + public get audioSampleRate(): number { + const index1 = this.version; + const index2 = (this._flags >> 10) & 0x03; + return AudioHeader.sampleRates[index1][index2]; + } + + /** + * Gets the MPEG audio channel mode of the audio represented by the current instance. + */ + public get channelMode(): ChannelMode { return (this._flags >> 6) & 0x03; } + + /** @inheritDoc ICodec.description */ + public get description(): string { + let builder = "MPEG Version "; + switch (this.version) { + case MpegVersion.Version1: + builder += "1"; + break; + case MpegVersion.Version2: + builder += "2"; + break; + case MpegVersion.Version25: + builder += "2.5"; + break; + } + builder += ` Audio, Layer ${this.audioLayer}`; + + if (this._xingHeader.isPresent || this._vbriHeader.isPresent) { + builder += " VBR"; + } + + return builder; + } + + /** @inheritDoc ICodec.duration */ + public get durationMilliseconds(): number { + if (this._durationMilliseconds > 0) { return this._durationMilliseconds; } + + if (this._xingHeader.totalFrames > 0) { + // Read the length and the bitrate from the Xing header + const timePerFrameSeconds = AudioHeader.blockSize[this.version][this.audioLayer] / this.audioSampleRate; + const durationSeconds = timePerFrameSeconds * this._xingHeader.totalFrames; + this._durationMilliseconds = durationSeconds * 1000; + } else if (this._vbriHeader.totalFrames > 0) { + // Read the length and the bitrate from the VBRI header + const timePerFrameSeconds = AudioHeader.blockSize[this.version][this.audioLayer] / this.audioSampleRate; + const durationSeconds = Math.round(timePerFrameSeconds * this._vbriHeader.totalFrames); + this._durationMilliseconds = durationSeconds * 1000; + } else if (this.audioFrameLength > 0 && this.audioBitrate > 0) { + // Since there was no valid Xing or VBRI header found, we hope that we're in a constant + // bitrate file + + // Round off to upper integer value + const frames = Math.floor((this._streamLength + this.audioFrameLength - 1) / this.audioFrameLength); + const durationSeconds = (this.audioFrameLength * frames) / (this.audioBitrate * 125); + this._durationMilliseconds = durationSeconds * 1000; + } + + return this._durationMilliseconds; + } + + /** + * Whether or not the current audio is copyrighted. + */ + public get isCopyrighted(): boolean { return ((this._flags >> 3) & 1) === 1; } + + /** + * Whether or not the current audio is original. + */ + public get isOriginal(): boolean { return ((this._flags >> 2) & 1) === 1; } + + /** + * Whether or not the audio represented by the current instance is padded. + */ + public get isPadded(): boolean { return ((this._flags >> 9) & 1) === 1; } + + /** + * Gets whether the audio represented by the current instance is protected by CRC. + */ + public get isProtected(): boolean { return ((this._flags >> 16) & 1) === 0; } + + /** @inheritDoc ICodec.mediaTypes */ + public get mediaTypes(): MediaTypes { return MediaTypes.Audio; } + + /** + * Sets the length of the audio stream represented by the current instance. + * If this value has not been set, {@see durationMilliseconds} will return an incorrect value. + */ + public set streamLength(value: number) { + this._streamLength = value; + + // Force the recalculation of duration if it depends on the stream length. + if (this._xingHeader.totalFrames === 0 && this._vbriHeader.totalFrames === 0) { + this._durationMilliseconds = 0; + } + } + + /** + * Gets the VBRI header found in the audio. {@see VbriHeader.Unknown} is returned if no header + * was found. + */ + public get vbriHeader(): VbriHeader { return this._vbriHeader; } + + /** + * Gets the MPEG version used to encode the audio represented by the current instance. + */ + public get version(): MpegVersion { + switch ((this._flags >> 19) & 0x03) { + case 0: + return MpegVersion.Version25; + case 2: + return MpegVersion.Version2; + default: + return MpegVersion.Version1; + } + } + + /** + * Gets the Xing header found in the audio. {@see XingHeader.Unknown} is returned if no header + * was found. + */ + public get xingHeader(): XingHeader { return this._xingHeader; } + + // #endregion + + /** + * Searches for an audio header in a file starting at a specified position and searching + * through a specified number of bytes. + * @param file File to search + * @param position Position in {@paramref file} at which to start searching + * @param length Maximum number of bytes to search before giving up. Defaults to `-1` to + * have no maximum + * @returns {header: AudioHeader, success: boolean} + * * `header` - the header that was found or {@see AudioHeader.Unknown} if a header was not + * found + * * `success` - whether or not a header was found + */ + public static find(file: File, position: number, length: number = -1): { header: AudioHeader, success: boolean} { + Guards.truthy(file, "file"); + Guards.int(position, "position"); + Guards.int(length, "length"); + + const end = position + length; + + file.seek(position); + let buffer = file.readBlock(3); + + if (buffer.length < 3) { + return { + header: this.Unknown, + success: false + }; + } + + do { + file.seek(position + 3); + buffer = buffer.mid(buffer.length - 3); + buffer.addByteVector(file.readBlock(File.bufferSize)); + + for (let i = 0; i < buffer.length - 3 && (length < 0 || position + i < end); i++) { + if (buffer.get(i) === 0xFF && buffer.get(i + 1) > 0xE0) { + const data = buffer.mid(i, 4); + if (!this.getHeaderError(data)) { + try { + return { + header: AudioHeader.fromData(data, file, position + i), + success: true + }; + } catch (e) { + if (!CorruptFileError.errorIs(e)) { + throw e; + } + } + } + } + } + + position += File.bufferSize; + } while (buffer.length > 3 && (length < 0 || position < end)); + + return { + header: this.Unknown, + success: false + }; + } + + private static getHeaderError(data: ByteVector): string { + if (data.length < 4) { + return "Insufficient header length"; + } + if (data.get(0) !== 0xFF) { + return "First byte did not match MPEG sync"; + } + + // Checking bits from high to low: + // - First 3 bytes MUST be set + // - Bits 4 and 5 can be 00, 10, or 11 but not 01 + // - One or more of bits 6 and 7 must be set + // - Bit 8 can be anything + if ((data.get(1) & 0xE6) <= 0xE0 || (data.get(1) & 0x18) === 0x08) { + return "Second byte did not match MPEG sync"; + } + + const flags = data.toUInt(); + if (((flags >> 12) & 0x0F) === 0x0F) { + return "Header uses invalid bitrate index"; + } + if (((flags >> 10) & 0x03) === 0x03) { + return "Invalid sample rate"; + } + + return undefined; + } +} diff --git a/src/mpeg/mpegEnums.ts b/src/mpeg/mpegEnums.ts new file mode 100644 index 00000000..78c8c468 --- /dev/null +++ b/src/mpeg/mpegEnums.ts @@ -0,0 +1,33 @@ +/** + * Indicates the MPEG audio channel mode of a file or stream. + */ +export enum ChannelMode { + /** Stereo */ + Stereo = 0, + + /** Joint Stereo */ + JointStereo = 1, + + /** Dual Channel Mono */ + DualChannel = 2, + + /** Single Channel Mono */ + SingleChannel= 3 +} + +/** + * Indicates the MPEG version of a file or stream. + */ +export enum MpegVersion { + /** Unknown version */ + Unknown = -1, + + /** MPEG-1 */ + Version1 = 0, + + /** MPEG-2 */ + Version2 = 1, + + /** MPEG-2.5 */ + Version25 = 2 +} diff --git a/src/mpeg/vbriHeader.ts b/src/mpeg/vbriHeader.ts new file mode 100644 index 00000000..9412461e --- /dev/null +++ b/src/mpeg/vbriHeader.ts @@ -0,0 +1,90 @@ +import {ByteVector} from "../byteVector"; +import {CorruptFileError} from "../errors"; +import {Guards} from "../utils"; + +/** + * Information about a variable bitrate MPEG audio stream encoded by the Fraunhofer encoder + */ +export default class VbriHeader { + /** + * Identifier that appears in the file to indicate the start of the VBRI header. + */ + public static readonly fileIdentifier = ByteVector.fromString("VBRI", undefined, undefined, true); + + /** + * An empty and unset VBRI header. + */ + public static readonly unknown = VbriHeader.fromInfo(0, 0); + + /** + * Offset at which a VBRI header would appear in an MPEG audio packet. Always 32 bytes after + * the end of the first MPEG header. + */ + public static readonly vbriHeaderOffset = 0x24; + + private _isPresent: boolean; + private _totalFrames: number; + private _totalSize: number; + + // #region Constructors + + private constructor() {} + + /** + * Constructs a new instance with a specified frame count and size. + * @param frames Frame count of the audio + * @param size Stream size of the audio + */ + public static fromInfo(frames: number, size: number): VbriHeader { + Guards.uint(frames, "frame"); + Guards.uint(size, "size"); + + const header = new VbriHeader(); + header._isPresent = false; + header._totalFrames = frames; + header._totalSize = size; + return header; + } + + /** + * Constructs a new instance from the raw data of the header. + * @param data Data to read the VBRI header from + */ + public static fromData(data: ByteVector): VbriHeader { + Guards.truthy(data, "data"); + + // Check to see if a valid VBRI header is available + if (!data.startsWith(VbriHeader.fileIdentifier)) { + throw new CorruptFileError("Not a valid VBRI header"); + } + + // Size start at position 10 + const header = new VbriHeader(); + header._totalSize = data.mid(10, 4).toUInt(); + header._totalFrames = data.mid(14, 4).toUInt(); + header._isPresent = true; + + return header; + } + + // #endregion + + // #region Properties + + /** + * Whether or not a physical VBRI header is present in the file. + */ + public get isPresent(): boolean { return this._isPresent; } + + /** + * Gets the total number of frames in the file, as indicated by the current instance. + */ + public get totalFrames(): number { return this._totalFrames; } + + /** + * Gets the total size of the file, as indicated by the current instance. + */ + public get totalSize(): number { return this._totalSize; } + + // #endregion +} diff --git a/src/mpeg/xingHeader.ts b/src/mpeg/xingHeader.ts new file mode 100644 index 00000000..b3134312 --- /dev/null +++ b/src/mpeg/xingHeader.ts @@ -0,0 +1,113 @@ +import {ByteVector} from "../byteVector"; +import {CorruptFileError} from "../errors"; +import {ChannelMode, MpegVersion} from "./mpegEnums"; +import {Guards} from "../utils"; + +/** + * Information about a variable bitrate MPEG audio stream + */ +export default class XingHeader { + /** + * Identifier that appears in a file to indicate the start of a Xing header. + */ + public static readonly fileIdentifier = ByteVector.fromString("Xing", undefined, undefined, true); + + /** + * An empty an unset Xing header + */ + public static readonly unknown = XingHeader.fromInfo(0, 0); + + private _isPresent: boolean; + private _totalFrames: number; + private _totalSize: number; + + // #region Constructors + + private constructor() {} + + /** + * Constructs a new instance with a specified frame count and size. + * @param frames Frame count of the audio + * @param size Stream size of the audio + */ + public static fromInfo(frames: number, size: number): XingHeader { + Guards.uint(frames, "frames"); + Guards.uint(size, "size"); + + const header = new XingHeader(); + header._isPresent = false; + header._totalFrames = frames; + header._totalSize = size; + return header; + } + + /** + * Constructs a new instance by reading its raw contents. + * @param data Raw data of the Xing header + */ + public static fromData(data: ByteVector): XingHeader { + Guards.truthy(data, "data"); + + // Check to see if a valid Xing header is available + if (!data.startsWith(XingHeader.fileIdentifier)) { + throw new CorruptFileError("Not a valid Xing header"); + } + + const header = new XingHeader(); + let position = 8; + + if ((data.get(7) & 0x01) !== 0) { + header._totalFrames = data.mid(position, 4).toUInt(); + position += 4; + } else { + header._totalFrames = 0; + } + + if ((data.get(7) & 0x02) !== 0) { + header._totalSize = data.mid(position, 4).toUInt(); + } else { + header._totalSize = 0; + } + + header._isPresent = true; + + return header; + } + + // #endregion + + // #region Properties + + /** + * Whether or not a physical VBRI header is present in the file. + */ + public get isPresent(): boolean { return this._isPresent; } + + /** + * Gets the total number of frames in the file, as indicated by the current instance. + */ + public get totalFrames(): number { return this._totalFrames; } + + /** + * Gets the total size of the file, as indicated by the current instance. + */ + public get totalSize(): number { return this._totalSize; } + + // #endregion + + /** + * Gets the offset at which a Xing header would appear in an MPEG audio packet based on the + * version and channel mode. + * @param version Version of the MPEG audio packet + * @param channelModel Channel mode of the MPEG audio packet + * @returns Offset into an MPEG audio packet where the Xing header would appear. + */ + public static xingHeaderOffset(version: MpegVersion, channelModel: ChannelMode): number { + const singleChannel = channelModel === ChannelMode.SingleChannel; + + return version === MpegVersion.Version1 + ? (singleChannel ? 0x15 : 0x24) + : (singleChannel ? 0x0D : 0x15); + } + +} diff --git a/src/nonContainer/endTag.ts b/src/nonContainer/endTag.ts index f8605a07..c0c3db3d 100644 --- a/src/nonContainer/endTag.ts +++ b/src/nonContainer/endTag.ts @@ -1,224 +1,238 @@ -// import ApeTag from "../ape/apeTag" -// import CombinedTag from "../combinedTag"; -// import {ApeFooter} from "../ape/apeFooter"; -// import {File, ReadStyle} from "../file"; -// import {Tag, TagTypes} from "../tag"; -// import {ByteVector} from "../byteVector"; -// -// export default class EndTag extends CombinedTag { -// private readonly _file: File; -// -// // Number of bytes that must be read to hold all applicable indicators -// private readonly _readSize: number = Math.max(ApeFooter.size, Id3v2Footer.size, Id3v1Tag.size); -// -// /** -// * Constructs and initializes a new instance of {@see EndTag} for a specified file. -// * Constructing a new instance does not automatically read the contents from the disk. -// * {@see EndTag.read} must be called to read the tags. -// * @param file File on which the new instance will perform its operations. -// */ -// public constructor(file: File) { -// super(); -// this._file = file; -// } -// -// /** -// * Gets the total size of the tags located at the end of the file by reading from the file. -// */ -// public get totalSize(): number { -// let start = this._file.length; -// let tagInfo; -// do { -// tagInfo = this.readTagInfo(start); -// start = tagInfo.updatedPosition; -// } while(tagInfo.tagTypes !== TagTypes.None); -// return this._file.length - start; -// } -// -// // #region Public Methods -// -// /** -// * Adds a tag of a specified type to the current instance, optionally copying values from an -// * existing tag. Id3v2 tags are added at the end of the current instance, while other tags are -// * added at the beginning. -// * @param type Type of tag to add to the current instance. At the time of this writing, this is -// * limited to {@see TagTypes.Ape}, {@see TagTypes.Id3v1}, and {@see TagTypes.Id3v2}. -// * @param copy Optionally, a {@see Tag} from which to copy values from using {@see Tag.copyTo} -// */ -// public addTag(type: TagTypes, copy: Tag): Tag { -// let tag: Tag; -// -// switch (type) { -// case TagTypes.Id3v1: -// tag = new Id3v1Tag(); -// break; -// case TagTypes.Id3v2: -// const tag32 = new Id3v2Tag(); -// tag32.version = 4; -// tag32.flags |= Id3v2HeaderFlags.FooterPresent; -// tag = tag32; -// break; -// case TagTypes.Ape: -// tag = new ApeTag(); -// break; -// } -// -// if (tag) { -// if (copy) { -// copy.copyTo(tag, true); -// } -// if (type === TagTypes.Id3v1) { -// this._tags.push(tag); -// } else { -// this._tags.unshift(tag); -// } -// } -// -// return tag; -// } -// -// /** -// * Reads the tags stored at the end of the file into the current instance. -// * @returns Seek position in the file at which the read tags begin. This also marks the seek -// * position at which the media ends. -// */ -// public read(style: ReadStyle): number { -// this._tags.length = 0; -// let start = this._file.length; -// let tag; -// do { -// tag = this.readTag(start, style); -// start = tag.updatedEnd; -// if (tag.tag) { -// this._tags.unshift(tag.tag); -// } -// } while (tag.tag); -// -// return start; -// } -// -// /** -// * Removes a set of tag types from the current instance.} -// * @param types Bitwise combined {@see TagTypes} value containing the tag types to be removed -// * from the file. -// * In order to remove all tags from a file, pass {@see TagTypes.AllTags}. -// */ -// public removeTags(types: TagTypes): void { -// for (let i = this._tags.length - 1; i >= 0; i--) { -// const tag = this._tags[i]; -// if (types === TagTypes.AllTags || (tag.tagTypes & types) > 0) { -// this._tags.splice(i, 1); -// } -// } -// } -// -// /** -// * Renders the tags contained in the current instance. -// * The tags are rendered in the order that they are stored in the current instance. -// */ -// public render(): ByteVector { -// const data = ByteVector.fromSize(0); -// for (const t of this._tags) { -// switch (t.tagTypes) { -// case TagTypes.Ape: -// data.addByteVector(( t).render()); -// break; -// case TagTypes.Id3v2: -// data.addByteVector(( t).render()); -// break; -// case TagTypes.Id3v1: -// data.addByteVector(( t).render()); -// } -// } -// return data; -// } -// -// /** -// * Writes the tags contained in the current instance to the end of the file that created, -// * overwriting the existing tags. -// * @returns Seek position in the file at which the written tags begin. This also marks the seek -// * position at which the media ends. -// */ -// public write(): number { -// const totalSize = this.totalSize; -// const data = this.render(); -// this._file.insert(data, this._file.length - totalSize, totalSize); -// return this._file.length - data.length; -// } -// -// // #endregion -// -// // #region Private Helpers -// -// private readTag(end: number, style: ReadStyle): {tag: Tag, updatedEnd: number} { -// let start = end; -// -// const tagInfo = this.readTagInfo(start); -// start = tagInfo.updatedPosition; -// -// let tag: Tag; -// -// try { -// switch (tagInfo.tagTypes) { -// case TagTypes.Ape: -// tag = new ApeTag(this._file, end - ApeFooter.size); -// break; -// case TagTypes.Id3v2: -// tag = new Id3v2Tag(this._file, start, style); -// break; -// case TagTypes.Id3v1: -// tag = new Id3v1Tag(this._file, start); -// break; -// } -// -// end = start; -// } catch (e) { -// // @TODO: Can/should we filter out corrupt file exception -// } -// -// return {tag: tag, updatedEnd: end}; -// } -// -// private readTagInfo(position: number): {tagTypes: TagTypes, updatedPosition: number} { -// if (position - this._readSize < 0) { -// return {tagTypes: TagTypes.None, updatedPosition: position}; -// } -// -// this._file.seek(position - this._readSize); -// const data = this._file.readBlock(this._readSize); -// -// try { -// const offset = data.length - ApeFooter.size; -// if (data.containsAt(ApeFooter.fileIdentifier, offset)) { -// const footer = new ApeFooter(data.mid(offset)); -// -// // If the complete tag size is zero or the tag is a header this indicates some sort -// // of corruption. -// if (footer.completeTagSize === 0 || (footer.flags & ApeFooterFlags.IsHeader) > 0) { -// return {tagTypes: TagTypes.None, updatedPosition: position}; -// } -// -// position -= footer.completeTagSize; -// return {tagTypes: TagTypes.Ape, updatedPosition: position}; -// } -// -// offset = data.length - Id3v2Footer.size; -// if (data.containsAt(Id3v2Footer.fileIdentifier, offset)) { -// const footer = new Id3v2Footer(data.mid(offset)); -// position -= footer.completeTagSize; -// return {tagTypes: TagTypes.Id3v2, updatedPosition: position}; -// } -// -// if (data.startsWith(Id3v1Tag.fileIdentifier)) { -// position -= Id3v1Tag.size; -// return {tagTypes: TagTypes.Id3v1, updatedPosition: position}; -// } -// } catch (e) { -// // @TODO: should/can we specifically check for corrupt file exception? -// } -// -// return {tagTypes: TagTypes.None, updatedPosition: position}; -// } -// -// // #endregion -// } \ No newline at end of file +import CombinedTag from "../combinedTag"; +import Id3v2Settings from "../id3v2/id3v2Settings"; +import Id3v1Tag from "../id3v1/id3v1Tag"; +import Id3v2Tag from "../id3v2/id3v2Tag"; +import Id3v2TagFooter from "../id3v2/id3v2TagFooter"; +import {ByteVector} from "../byteVector"; +import {CorruptFileError} from "../errors"; +import {File, ReadStyle} from "../file"; +import {Id3v2TagHeaderFlags} from "../id3v2/id3v2TagHeader"; +import {Tag, TagTypes} from "../tag"; +import {Guards} from "../utils"; + +/** + * Provides support for accessing and modifying a collection of tags appearing at the end of a + * file. + * This class is used by {@see NonContainerFile} to read all tags appearing at the end of the file + * but could be used by other classes. It currently supports ID3v1, ID3v2, and APE tags. + * @TODO: JK, NO IT DON'T SUPPORT APE YET + */ +export default class EndTag extends CombinedTag { + private readonly _file: File; + private readonly _readSize: number = Math.max( + /* @TODO: APE footer */ + Id3v2Settings.footerSize, + Id3v1Tag.size + ); + + public constructor(file: File) { + super(); + Guards.truthy(file, "file"); + this._file = file; + } + + /** + * Gets the total size of the tags located at the end of the file by reading from the file. + */ + public get totalSize(): number { + let start = this._file.length; + let lastReadTagType = TagTypes.AllTags; + while (lastReadTagType !== TagTypes.None) { + const readResult = this.readTagInfo(start); + lastReadTagType = readResult.tagType; + start = readResult.tagStarted; + } + + return this._file.length - start; + } + + // #region Public Methods + + /** + * Adds a tag of a specified type to the current instance, optionally copying values from an + * existing type. + * Id3v1 tags are added at the end of the current instance, while other tags are added at the + * beginning. + * @param type Type of the tag to add to the current instance. At the time of this writing, + * this is limited to {@see TagTypes.Ape}, {@see TagTypes.Id3v1}, and {@see TagTypes.Id3v2} + * @param copy Tag to copy values from using {@see Tag.copyTo}, or `undefined` if no tag is to + * be copied. + * @returns Tag Tag added to the current instance. `undefined` if a tag could not be created. + */ + public addTag(type: TagTypes, copy: Tag): Tag { + let tag: Tag; + + // @TODO: Add Id3v1 + if (type === TagTypes.Id3v2) { + const tag32 = Id3v2Tag.fromEmpty(); + tag32.version = 4; + tag32.flags |= Id3v2TagHeaderFlags.FooterPresent; + + tag = tag32; + } + // @TODO: Add APE + + if (tag) { + if (copy) { + copy.copyTo(tag, true); + } + + if (type === TagTypes.Id3v1) { + this.addTagInternal(tag); + } else { + this.insertTag(0, tag); + } + } + + return tag; + } + + /** + * Reads the tags stored at the end of the file into the current instance. + * @returns number Seek position in the file at which the read tags begin + */ + public read(style: ReadStyle): number { + this.clearTags(); + + let start = this._file.length; + let tag: Tag; + do { + const readResult = this.readTag(start, style); + tag = readResult.tag; + start = readResult.tagStarted; + + if (tag) { + this.insertTag(0, tag); + } + } while (tag); + + return start; + } + + /** + * Removes a set of tag types from the current instance. + * @param types Tag types to be removed from the file. To remove all tags, use + * {@see TagTypes.AllTags} + */ + public removeTags(types: TagTypes): void { + for (let i = this.tags.length - 1; i >= 0; i--) { + const tag = this.tags[i]; + if (types === TagTypes.AllTags || (tag.tagTypes & types) === tag.tagTypes) { + this.removeTag(tag); + } + } + } + + /** + * Renders the tags contained in the current instance. + * The tags are rendered in the order that they are stored. + * @returns ByteVector Physical representation of the tags stored in the current instance + */ + public render(): ByteVector { + const tagData = this.tags.map((t) => { + switch (t.tagTypes) { + // @TODO: Add APE + case TagTypes.Id3v2: + return ( t).render(); + case TagTypes.Id3v1: + return ( t).render(); + } + }); + return ByteVector.concatenate(... tagData); + } + + /** + * Writes the tags contained in the current instance to the end of the file that created it, + * overwriting the existing tags. + * @returns number Seek position in the file at which the written tags begin. This also marks + * the seek position at which the media ends. + */ + public write(): number { + const totalSize = this.totalSize; + const data = this.render(); + this._file.insert(data, this._file.length - totalSize, totalSize); + return this._file.length - data.length; + } + + // #endregion + + // #region Private Methods + + private readTag(end: number, style: ReadStyle): {tagStarted: number, tag: Tag} { + let start = end; + + const readResult = this.readTagInfo(start); + start = readResult.tagStarted; + + let tag: Tag; + try { + switch (readResult.tagType) { + // TODO: Add APE support + case TagTypes.Id3v2: + tag = Id3v2Tag.fromFile(this._file, readResult.tagStarted, style); + break; + case TagTypes.Id3v1: + tag = Id3v1Tag.fromFile(this._file, readResult.tagStarted); + break; + } + + end = start; + } catch (e) { + if (!CorruptFileError.errorIs(e)) { + throw e; + } + } + + return { + tag: tag, + tagStarted: end + }; + } + + private readTagInfo(position: number): {tagStarted: number, tagType: TagTypes} { + if (position - this._readSize < 0) { + return { + tagStarted: position, + tagType: TagTypes.None + }; + } + + this._file.seek(position - this._readSize); + const data = this._file.readBlock(this._readSize); + + try { + // TODO: Try to find APE footer + + // Try to find ID3v2 footer + const offset = data.length - Id3v2Settings.footerSize; + if (data.containsAt(Id3v2TagFooter.fileIdentifier, offset)) { + const footer = Id3v2TagFooter.fromData(data.mid(offset)); + position -= footer.completeTagSize; + return { + tagStarted: position, + tagType: TagTypes.Id3v2 + }; + } + + // Try to find ID3v1 footer + if (data.startsWith(Id3v1Tag.fileIdentifier)) { + position -= Id3v1Tag.size; + return { + tagStarted: position, + tagType: TagTypes.Id3v1 + }; + } + } catch (e) { + if (!CorruptFileError.errorIs(e)) { + throw e; + } + } + + return { + tagStarted: position, + tagType: TagTypes.None + }; + } + + // #endregion +} diff --git a/src/nonContainer/nonContainerFile.ts b/src/nonContainer/nonContainerFile.ts new file mode 100644 index 00000000..6b54ee8d --- /dev/null +++ b/src/nonContainer/nonContainerFile.ts @@ -0,0 +1,156 @@ +import EndTag from "./endTag"; +import NonContainerTag from "./nonContainerTag"; +import Properties from "../properties"; +import StartTag from "./startTag"; +import {File as BaseFile, FileAccessMode, ReadStyle} from "../file"; +import {IFileAbstraction} from "../fileAbstraction"; +import {TagTypes} from "../tag"; + +/** + * Abstract class that provides tagging and properties for files that contain an indeterminate + * number of tags at their beginning or end. + * When extending this class, {@see NonContainerFile.readStart}, + * + * {@see NonContainerFile.readEnd}, and {@see NonContainerFile.readProperties} should be overridden + * and read the format specific information from the file. + * The file is read upon construction in the following manner: + * 1. The file is opened for reading + * 2. The tags at the start of the file are read + * 3. {@see NonContainerFile.readStart} is called + * 4. The tags at the end of the file are read + * 5. {@see NonContainerFile.readEnd} is called + * 6. If reading with a style other than {@see ReadStyle.None}, + * {@see NonContainerFile.readProperties} is called + * 7. The file is closed + */ +export default abstract class NonContainerFile extends BaseFile { + private _tag: NonContainerTag; + private _properties: Properties; + + protected constructor(fileToRead: IFileAbstraction | string, propertiesStyle: ReadStyle = ReadStyle.Average) { + super(fileToRead); + + this.read(propertiesStyle); + } + + // #region Properties + + /** + * Gets the collection of tags appearing at the end of the file. + */ + protected get endTag(): EndTag { return this._tag.endTag; } + + /** + * Gets the collection of tags appearing at the start of the file. + */ + protected get startTag(): StartTag { return this._tag.startTag; } + + /** + * Gets an abstract representation of all tags stored in the current instance. + */ + public get tag(): NonContainerTag { return this._tag; } + + /** + * Gets the media properties of the file represented by the current instance. + */ + public get properties(): Properties { return this._properties; } + + // #endregion + + // #region Public Methods + + /** @inheritDoc BaseFile.save */ + public save(): void { + this.preSave(); + this.mode = FileAccessMode.Write; + + try { + const writeResult = this._tag.write(); + this._invariantStartPosition = writeResult.start; + this._invariantEndPosition = writeResult.end; + this._tagTypesOnDisk = this.tagTypes; + } finally { + this.mode = FileAccessMode.Closed; + } + } + + /** @inheritDoc BaseFile.removeTags */ + public removeTags(types: TagTypes): void { + this._tag.removeTags(types); + } + + // #endregion + + // #region Private/Protected Methods + + /** + * Reads format specific information at the end of the file. + * This method is called by the constructor immediately after the tags at the end of the file + * have been read and as such (so the internal seek mechanism is close to the end). It should + * be used for reading any content-specific information such as an audio header from the end of + * the file. + * @param end Seek position at which the media data ends and the tags begin + * @param propertiesStyle Level of accuracy to read the media properties or + * {@see ReadStyle.None} to ignore the properties + */ + protected readEnd(end: number, propertiesStyle: ReadStyle): void { + /* No-op in base implementation */ + } + + /** + * Reads the audio properties from the file represented by the current instance. + * This method is called ONLY IF the file is constructed with a read style other than + * {@see ReadStyle.None}, and as such MUST NOT return `undefined`/`null`. It is guaranteed that + * {@see readStart} and {@see readEnd} will have been called first and this method should be + * strictly used to perform final processing on already read data. + * @param start Seek position at which the tags end and the media data begins + * @param end Seek position at which the media data ends and the tags begin + * @param propertiesStyle Level of accuracy to read the media properties or + * {@see ReadStyle.None} to ignore the properties + * @returns Properties Media properties of the file represented by the current instance + */ + protected abstract readProperties(start: number, end: number, propertiesStyle: ReadStyle): Properties; + + /** + * Reads format specific information from the start of the file. + * This method is called by the constructor immediately after the tags at the start of the + * file have been read (so the internal seek mechanism is close to the start). It should be + * used for reading any content specific information, such as an audio header from the start of + * the file. + * @param start Seek position at which the tags end and the media data begins + * @param propertiesStyle Level of accuracy to read the media properties or + * {@see ReadStyle.None} to ignore the properties + */ + protected readStart(start: number, propertiesStyle: ReadStyle): void { + /* No-op in base implementation */ + } + + private read(propertiesStyle: ReadStyle): void { + this.mode = FileAccessMode.Read; + + try { + this._tag = new NonContainerTag(this); + + // Read the tags and property data at the beginning of the file + this._invariantStartPosition = this._tag.readStart(propertiesStyle); + this._tagTypesOnDisk |= this.startTag.tagTypes; + this.readStart(this.invariantStartPosition, propertiesStyle); + + // Read the tags and property data at the end of the file + this._invariantEndPosition = this.invariantStartPosition === this.length + ? this.length + : this._tag.readEnd(propertiesStyle); + this._tagTypesOnDisk |= this.endTag.tagTypes; + this.readEnd(this._invariantEndPosition, propertiesStyle); + + // Read the audio properties + this._properties = (propertiesStyle & ReadStyle.Average) !== 0 + ? this.readProperties(this.invariantStartPosition, this.invariantEndPosition, propertiesStyle) + : undefined; + } finally { + this.mode = FileAccessMode.Closed; + } + } + + // #endregion +} diff --git a/src/nonContainer/nonContainerTag.ts b/src/nonContainer/nonContainerTag.ts new file mode 100644 index 00000000..fd089f59 --- /dev/null +++ b/src/nonContainer/nonContainerTag.ts @@ -0,0 +1,142 @@ +import CombinedTag from "../combinedTag"; +import EndTag from "./endTag"; +import NonContainerFile from "./nonContainerFile"; +import StartTag from "./startTag"; +import {Tag, TagTypes} from "../tag"; +import {ReadStyle} from "../file"; + +/** + * This class extends {@see CombinedTag}, combining {@see StartTag} and {@see EndTag} in such a way + * as their children appear as its children. + */ +export default class NonContainerTag extends CombinedTag { + private readonly _endTag: EndTag; + private readonly _startTag: StartTag; + + /** + * Constructs a new instance for a specified file. + * Constructing a new instance does not automatically read the contents from the disk. + * {@see read} must be called to read the tags + * @param file File to pull tags from + */ + public constructor(file: NonContainerFile) { + super(); + + this._startTag = new StartTag(file); + this._endTag = new EndTag(file); + this.addTagInternal(this.startTag); + this.addTagInternal(this.endTag); + } + + // #region Properties + + /** + * Gets the collection of tags appearing at the end of the file. + */ + public get endTag(): EndTag { return this._endTag; } + + /** + * Gets the collection of tags appearing at the start of the file. + */ + public get startTag(): StartTag { return this._startTag; } + + /** + * Gets the tags combined in the current instance. + */ + public get tags(): Tag[] { + const tags = []; + tags.push(... this.startTag.tags); + tags.push(... this.endTag.tags); + return tags; + } + + /** + * Gets the tag types contained in the current instance. + */ + public get tagTypes(): TagTypes { return this.startTag.tagTypes | this.endTag.tagTypes; } + + // #endregion + + // #region Public Methods + + /** + * Gets a tag of a specified type from the current instance. + * @param type Type of tag to read + * @returns Tag that was found in the current instance. If no matching tag was found, + * `undefined` is returned + */ + public getTag(type: TagTypes): Tag { + for (const tag of this.tags) { + if (type === TagTypes.Id3v1 && tag.tagTypes === TagTypes.Id3v1) { + return tag; + } + if (type === TagTypes.Id3v2 && tag.tagTypes === TagTypes.Id3v2) { + return tag; + } + if (type === TagTypes.Ape && tag.tagTypes === TagTypes.Ape) { + return tag; + } + } + + return undefined; + } + + /** + * Removes a set of tag types from the current instance. + * @param types Tag types to be removed from the file. To remove all tags from a file, use + * {@see TagTypes.AllTags} + */ + public removeTags(types: TagTypes): void { + this.startTag.removeTags(types); + this.endTag.removeTags(types); + } + + /** + * Reads the tags at the start and end of the file. + * @returns {start: number, end: number} + * start - Position in the file where tags at the beginning of the file end + * end - Position in the file where tags at the end of the file begin + */ + public read(): {start: number, end: number} { + return { + end: this.readEnd(ReadStyle.None), + start: this.readStart(ReadStyle.None) + }; + } + + /** + * Reads the tags stored at the end of the file into the current instance. + * @returns number Position in the file where tags at the end of the file begin + */ + public readEnd(style: ReadStyle): number { + return this.endTag.read(style); + } + + /** + * Reads the tags stored at the beginning of the file into the current instance. + * @returns number Position in the file where tags at the beginning of the file end + */ + public readStart(style: ReadStyle): number { + return this.startTag.read(style); + } + + /** + * Writes the tags to the start and end of the file. + * @returns {start: number, end: number} + * start - Position in the file where tags at the beginning of the file end + * end - Position in the file where tags at the end of the file begin + */ + public write(): { start: number, end: number} { + // NOTE: As nice as it would be to do this in an object initialization block, we need to + // write the start tag first then the end tag. Otherwise we'll get the wrong position + // for the end of the media. + const start = this.startTag.write(); + const end = this.endTag.write(); + return { + end: end, + start: start + }; + } + + // #endregion +} diff --git a/src/nonContainer/startTag.ts b/src/nonContainer/startTag.ts new file mode 100644 index 00000000..3fa2ff07 --- /dev/null +++ b/src/nonContainer/startTag.ts @@ -0,0 +1,195 @@ +import CombinedTag from "../combinedTag"; +import Id3v2Tag from "../id3v2/id3v2Tag"; +import Id3v2Settings from "../id3v2/id3v2Settings"; +import {ByteVector} from "../byteVector"; +import {CorruptFileError} from "../errors"; +import {File, ReadStyle} from "../file"; +import {Id3v2TagHeader} from "../id3v2/id3v2TagHeader"; +import {Tag, TagTypes} from "../tag"; +import {Guards} from "../utils"; + +/** + * Provides support for accessing and modifying a collection of tags appearing at the start of a + * file. + * This class is used by {@see NonContainerFile} to read all the tags appearing at the start of the + * file but could be used by other classes. It currently supports ID3v2 and APE tags. + * @TODO: JK NO IT DON'T SUPPORT APE YET. + */ +export default class StartTag extends CombinedTag { + private readonly _file: File; + private readonly _readSize = Math.max(/* TODO: APE */ Id3v2Settings.headerSize); + + /** + * Constructs a new instance for a specified file. + * @param file File on which the new instance will perform its operations + */ + public constructor(file: File) { + super(); + + Guards.truthy(file, "file"); + this._file = file; + } + + /** + * Gets the total size of the tags located at the beginning of the file by reading from + * the file. + */ + public get totalSize(): number { + let size = 0; + let lastReadTagType = TagTypes.AllTags; + while (lastReadTagType !== TagTypes.None) { + const readResult = this.readTagInfo(size); + lastReadTagType = readResult.tagType; + size = readResult.position; + } + + return size; + } + + // #region Public Methods + + /** + * Adds a tag of a specified type to the current instance, optionally copying values from an + * existing type. + * @param type Type of the tag to add to the current instance. At the time of this writing, + * this is limited to {@see TagTypes.Ape}, {@see TagTypes.Id3v1}, and {@see TagTypes.Id3v2} + * @param copy Tag to copy values from using {@see Tag.copyTo}, or `undefined` if no tag is to + * be copied. + * @returns Tag Tag added to the current instance. `undefined` if a tag could not be created. + */ + public addTag(type: TagTypes, copy: Tag) { + let tag: Tag; + + if (type === TagTypes.Id3v2) { + tag = Id3v2Tag.fromEmpty(); + } + // @TODO: Add APE + + if (tag) { + if (copy) { + copy.copyTo(tag, true); + } + + this.addTagInternal(tag); + } + + return tag; + } + + /** + * Reads the tags stored at the start of the file into the current instance. + * @returns Seek position in the file at which the read tags end. This also marks where the + * media begins. + */ + public read(style: ReadStyle): number { + this.clearTags(); + + let end = 0; + let tag: Tag; + do { + const readResult = this.readTag(end, style); + tag = readResult.tag; + end = readResult.start; + + if (tag) { + this.addTagInternal(tag); + } + } while (tag); + + return end; + } + + /** + * Removes a set of tag types from the current instance. + * @param types Tag types to be removed from the file. To remove all tags, use + * {@see TagTypes.AllTags} + */ + public removeTags(types: TagTypes): void { + for (let i = this.tags.length - 1; i >= 0; i--) { + const tag = this.tags[0]; + if (types === TagTypes.AllTags || (tag.tagTypes & types) === tag.tagTypes) { + this.removeTag(tag); + } + } + } + + /** + * Renders the tags contained in the current instance. + * The tags are rendered in the order that they are stored in the current instance. + * @returns ByteVector Physical representation of the tags stored in the current instance + */ + public render(): ByteVector { + const tagData = this.tags.map((t) => { + switch (t.tagTypes) { + // @TODO: Add APE + case TagTypes.Id3v2: + return ( t).render(); + } + }); + return ByteVector.concatenate(... tagData); + } + + /** + * Writes the tags contained in the current instance to the beginning of the file that created + * it, overwriting the existing tags. + * @returns number Seek position in the file at which the written tags end. This also marks the + * seek position at which the media begins. + */ + public write(): number { + const data = this.render(); + this._file.insert(data, 0, this.totalSize); + return data.length; + } + + // #endregion + + // #region Protected/Private Methods + + private readTag(start: number, style: ReadStyle): {start: number, tag: Tag} { + const readResult = this.readTagInfo(start); + const end = readResult.position; + let tag: Tag; + + switch (readResult.tagType) { + // @TODO: APE + case TagTypes.Id3v2: + tag = Id3v2Tag.fromFile(this._file, start, style); + break; + } + + start = end; + return { + start: start, + tag: tag + }; + } + + private readTagInfo(position: number): {position: number, tagType: TagTypes} { + this._file.seek(position); + const data = this._file.readBlock(this._readSize); + + try { + // @TODO: Add APE + if (data.startsWith(Id3v2TagHeader.fileIdentifier)) { + const header = Id3v2TagHeader.fromData(data); + + position += header.completeTagSize; + return { + position: position, + tagType: TagTypes.Id3v2 + }; + } + } catch (e) { + if (!CorruptFileError.errorIs(e)) { + throw e; + } + } + + return { + position: position, + tagType: TagTypes.None + }; + } + + // #endregion +} diff --git a/src/pictureLazy.ts b/src/pictureLazy.ts index 7af4c968..198d3cdf 100644 --- a/src/pictureLazy.ts +++ b/src/pictureLazy.ts @@ -56,8 +56,8 @@ export default class PictureLazy implements IPicture, ILazy { * Constructs a new instance from a file abstraction. The content will be lazily loaded. * @param file File abstraction containing the file to read * @param offset Index into the file where the picture is located, must be a 32-bit integer - * @param size Optionally, size of the picture in bytes. If omitted, all bytes will be read - * when lazily loaded. Must be a 32-bit integer or `undefined` + * @param size Optionally, size of the picture in bytes. If omitted, all bytes of file will be + * read when lazily loaded. Must be a 32-bit integer or `undefined` */ public static fromFile(file: IFileAbstraction, offset: number, size?: number): PictureLazy { Guards.truthy(file, "file"); diff --git a/src/tag.ts b/src/tag.ts index 92d193c3..67519379 100644 --- a/src/tag.ts +++ b/src/tag.ts @@ -337,19 +337,22 @@ export abstract class Tag { protected static isNullOrLikeEmpty(value: string|string[]): boolean { // This should match `undefined`, `null`, and `""` - if (!value) { return false; } + if (!value) { return true; } - // Handle pure string scenario + // Handle pure whitespace string scenario if (typeof(value) === "string") { return value.trim().length === 0; } // Handle array scenario for (const s of value) { + // If one of the values in the array is not falsy, break out if (!Tag.isNullOrLikeEmpty(s)) { return false; } } + + // All elements of the array are falsy return true; } } diff --git a/src/utils.ts b/src/utils.ts index fa46dba2..5d063d60 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -116,3 +116,12 @@ export class ArrayUtils { export class NumberUtils { } + +export class StringUtils { + public static trimStart(trimee: string, chars: string) { + while (trimee.length > 0 && chars.indexOf(trimee[0]) > -1) { + trimee = trimee.substr(0); + } + return trimee; + } +} diff --git a/test-integration/id3v1_fileTests.ts b/test-integration/id3v1_fileTests.ts new file mode 100644 index 00000000..39bb9381 --- /dev/null +++ b/test-integration/id3v1_fileTests.ts @@ -0,0 +1,55 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {suite, test} from "mocha-typescript"; +import TestConstants from "./utilities/testConstants"; +import {File, ReadStyle} from "../src"; +import {StandardFileTests} from "./utilities/standardFileTests"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite class Id3v1_FileTests { + // NOTE: These tests are more integration level tests from the original .NET implementation + + private static readonly sampleFilePath = TestConstants.getSampleFilePath("sample_v1_only.mp3"); + private static readonly tmpFileName = "tmpwrite_v1_only.mp3"; + + private static file: File; + + public static before() { + Id3v1_FileTests.file = File.createFromPath(Id3v1_FileTests.sampleFilePath); + } + + public static after() { + Id3v1_FileTests.file.dispose(); + } + + @test + public readAudioProperties() { + assert.strictEqual(Id3v1_FileTests.file.properties.audioSampleRate, 44100); + assert.strictEqual(Id3v1_FileTests.file.properties.durationMilliseconds, 1352); + } + + @test + public readTags() { + assert.strictEqual(Id3v1_FileTests.file.tag.album, "MP3 album"); + assert.strictEqual(Id3v1_FileTests.file.tag.firstPerformer, "MP3 artist"); + assert.strictEqual(Id3v1_FileTests.file.tag.comment, "MP3 comment"); + assert.strictEqual(Id3v1_FileTests.file.tag.firstGenre, "Acid Punk"); + assert.strictEqual(Id3v1_FileTests.file.tag.track, 6); + assert.strictEqual(Id3v1_FileTests.file.tag.year, 1234); + } + + @test + public writeStandardPictures() { + const tmpFilePath = TestConstants.getTempFilePath(Id3v1_FileTests.tmpFileName); + StandardFileTests.writeStandardPictures(Id3v1_FileTests.sampleFilePath, tmpFilePath, ReadStyle.None); + } + + @test + public writeStandardTags() { + const tmpFilePath = TestConstants.getTempFilePath(Id3v1_FileTests.tmpFileName); + StandardFileTests.writeStandardTags(Id3v1_FileTests.sampleFilePath, tmpFilePath); + } +} diff --git a/test-integration/id3v2_fileTests.ts b/test-integration/id3v2_fileTests.ts new file mode 100644 index 00000000..38d35849 --- /dev/null +++ b/test-integration/id3v2_fileTests.ts @@ -0,0 +1,186 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import * as fs from "fs"; +import {suite, test} from "mocha-typescript"; + +import ExtendedFileTests from "./utilities/extendedFileTests"; +import TestConstants from "./utilities/testConstants"; +import Utilities from "./utilities/utilities"; +import {File, Id3v2FrameIdentifiers, Id3v2Tag, ReadStyle, TagTypes} from "../src"; +import {StandardFileTests} from "./utilities/standardFileTests"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite class Id3v2_FileTests { + // NOTE: These tests are more integration level tests from the original .NET implementation + + private static readonly corruptFilePath = TestConstants.getCorruptFilePath("null_title_v2.mp3"); + private static readonly extHeaderFilePath = TestConstants.getSampleFilePath("sample_v2_3_ext_header.mp3"); + private static readonly sampleFilePath = TestConstants.getSampleFilePath("sample.mp3"); + private static readonly tmpFileName = "tmpwrite_v2_only.mp3"; + private static readonly v2sampleFilePath = TestConstants.getSampleFilePath("sample_v2_only.mp3"); + + private static file: File; + + public static before() { + Id3v2_FileTests.file = File.createFromPath(Id3v2_FileTests.v2sampleFilePath); + } + + public static after() { + Id3v2_FileTests.file.dispose(); + } + + @test + public multiGenresTest() { + const rgFile = File.createFromPath(Id3v2_FileTests.sampleFilePath); + try { + const tag = rgFile.tag; + const genres = tag.genres; + + assert.strictEqual(genres.length, 3); + assert.strictEqual(genres[0], "Genre 1"); + assert.strictEqual(genres[1], "Genre 2"); + assert.strictEqual(genres[2], "Genre 3"); + } finally { + rgFile.dispose(); + } + } + + @test + public readAudioProperties() { + assert.strictEqual(Id3v2_FileTests.file.properties.audioSampleRate, 44100); + assert.strictEqual(Id3v2_FileTests.file.properties.durationMilliseconds, 1352); + } + + @test + public readTags() { + assert.strictEqual(Id3v2_FileTests.file.tag.album, "MP3 album"); + assert.strictEqual(Id3v2_FileTests.file.tag.firstPerformer, "MP3 artist"); + assert.strictEqual(Id3v2_FileTests.file.tag.comment, "MP3 comment"); + assert.strictEqual(Id3v2_FileTests.file.tag.firstGenre, "Acid Punk"); + assert.strictEqual(Id3v2_FileTests.file.tag.title, "MP3 title"); + assert.strictEqual(Id3v2_FileTests.file.tag.track, 6); + assert.strictEqual(Id3v2_FileTests.file.tag.trackCount, 7); + assert.strictEqual(Id3v2_FileTests.file.tag.year, 1234); + } + + @test + public testExtendedHeaderSize() { + const file = File.createFromPath(Id3v2_FileTests.extHeaderFilePath); + try { + assert.strictEqual(file.tag.title, "Title v2"); + } finally { + file.dispose(); + } + } + + @test + public testTruncateOrFalsy() { + // Arrange + const tmpFilePath = TestConstants.getTempFilePath(Id3v2_FileTests.tmpFileName); + if (fs.existsSync(tmpFilePath)) { + fs.unlinkSync(tmpFilePath); + } + fs.copyFileSync(Id3v2_FileTests.corruptFilePath, tmpFilePath); + + try { + // Act + const tmp = File.createFromPath(tmpFilePath); + + // Assert + try { + assert.strictEqual(tmp.tag.title, "T"); + } finally { + tmp.dispose(); + } + } finally { + // Cleanup + Utilities.deleteBestEffort(tmpFilePath); + } + } + + @test + public urlLinkFrameTest() { + const tempFilePath = TestConstants.getTempFilePath(Id3v2_FileTests.tmpFileName); + fs.copyFileSync(Id3v2_FileTests.sampleFilePath, tempFilePath); + + try { + const urlLinkFile1 = File.createFromPath(tempFilePath); + try { + const id3v2Tag1 = urlLinkFile1.getTag(TagTypes.Id3v2, false); + id3v2Tag1.setTextFrame (Id3v2FrameIdentifiers.WCOM, "www.commercial.com"); + id3v2Tag1.setTextFrame (Id3v2FrameIdentifiers.WCOP, "www.copyright.com"); + id3v2Tag1.setTextFrame (Id3v2FrameIdentifiers.WOAF, "www.official-audio.com"); + id3v2Tag1.setTextFrame (Id3v2FrameIdentifiers.WOAR, "www.official-artist.com"); + id3v2Tag1.setTextFrame (Id3v2FrameIdentifiers.WOAS, "www.official-audio-source.com"); + id3v2Tag1.setTextFrame (Id3v2FrameIdentifiers.WORS, "www.official-internet-radio.com"); + id3v2Tag1.setTextFrame (Id3v2FrameIdentifiers.WPAY, "www.payment.com"); + id3v2Tag1.setTextFrame (Id3v2FrameIdentifiers.WPUB, "www.official-publisher.com"); + urlLinkFile1.save(); + } finally { + urlLinkFile1.dispose(); + } + + const urlLinkFile2 = File.createFromPath(tempFilePath); + try { + const id3v2Tag2 = urlLinkFile1.getTag(TagTypes.Id3v2, false); + assert.strictEqual( + id3v2Tag2.getTextAsString(Id3v2FrameIdentifiers.WCOM), + "www.commercial.com" + ); + assert.strictEqual( + id3v2Tag2.getTextAsString(Id3v2FrameIdentifiers.WCOP), + "www.copyright.com" + ); + assert.strictEqual( + id3v2Tag2.getTextAsString(Id3v2FrameIdentifiers.WOAF), + "www.official-audio.com" + ); + assert.strictEqual( + id3v2Tag2.getTextAsString(Id3v2FrameIdentifiers.WOAR), + "www.official-artist.com" + ); + assert.strictEqual( + id3v2Tag2.getTextAsString(Id3v2FrameIdentifiers.WOAS), + "www.official-audio-source.com" + ); + assert.strictEqual( + id3v2Tag2.getTextAsString(Id3v2FrameIdentifiers.WORS), + "www.official-internet-radio.com" + ); + assert.strictEqual( + id3v2Tag2.getTextAsString(Id3v2FrameIdentifiers.WPAY), + "www.payment.com" + ); + assert.strictEqual( + id3v2Tag2.getTextAsString(Id3v2FrameIdentifiers.WPUB), + "www.official-publisher.com" + ); + } finally { + urlLinkFile2.dispose(); + } + } finally { + fs.unlinkSync(tempFilePath); + } + } + + @test + public writeExtendedTags() { + const tmpFilePath = TestConstants.getTempFilePath(Id3v2_FileTests.tmpFileName); + ExtendedFileTests.writeExtendedTags(Id3v2_FileTests.sampleFilePath, tmpFilePath); + } + + @test + public writeStandardPictures() { + const tmpFilePath = TestConstants.getTempFilePath(Id3v2_FileTests.tmpFileName); + StandardFileTests.writeStandardPictures(Id3v2_FileTests.sampleFilePath, tmpFilePath); + } + + @test + public writeStandardTags() { + const tmpFilePath = TestConstants.getTempFilePath(Id3v2_FileTests.tmpFileName); + StandardFileTests.writeStandardTags(Id3v2_FileTests.sampleFilePath, tmpFilePath); + } +} diff --git a/test-integration/resources/corruptSamples/null_title_v2.mp3 b/test-integration/resources/corruptSamples/null_title_v2.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..c6cba9d3d5e3b6488bea547b776df9d21213743d GIT binary patch literal 12294 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 z zws8x03{=awx3Q~(PG7X$NcWLpNFjjf>Ot@LR#KF1DM@o&j%f7IyYFtMl}mCFK*{a4 zr4VUW+H^N_8;hYV%@>qkZZI=~-=#~ES+&98UcVufGG>RC0K!o28Aq z6krsrIRN`q-cM9zCft?Cqt$QjWMc$nyRwL}kb0V};<>K1O$r9J5574HOju*&9-h^; zG}N~uvQ9EyN0`f|#dYGug?GG74rC>9hwKej)wHFx)~$fzAcZ8)199n^^*OH6XUkO8 zv{JaU*~+E@k0qEMtlqSXV<;+0jKTYdz{%#EbdiOw%{!eu!*3s#^E6B&1x`Lz>r6UJ zTZ0*9CK5cdy$YVZ(3x(@9XEnB%UHGN`%9R_Q5hjBK|ML*wftkJBj~Y37%OojZ^<70 ze=1K|XlSh{oP*g$(lG$9+G7hA7a1$p6=Ew-K7Gj;33(WZWZ-3b){kO0CPMJ_Lxyzce$DNgIh7^%QkXhaY=F`LaRao7A&rK4IS(jPg&`u&tqn;%0k9 zV-&CiZNncvK4M4KvQlp2*&-V_%ztGf9DSBU9Giz+^xad#Z#)6KxRgDVzGEHOU=MNY zT1jX(rHk>{%Id|up@Jtu!y_Zp-nP6Y9Fgnoa2%AmgAKS~l27!_Dv5HJrP0#rcA#7| zY#4~wZo1gg{%rkw(zj%%C|oT87L+*X{?dZF4)`=lv0n<(>$-*jzxIwY+_vUR0E&D)QjSTzn{pFP{sy6MF4#>-iawa5u~D) z?9872`r)=(tlwutfCOiO^r@E9T9Q63rp8Vg!2|KDO3^d-Q5>2r{m+&)4=gBbDKcZA B4Cnv= literal 0 HcmV?d00001 diff --git a/test-integration/resources/samples/sample.mp3 b/test-integration/resources/samples/sample.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..f20553abf6c210bcea8725302ea1c30c6878a8f7 GIT binary patch literal 11815 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%0AYfiH+gC%k{Io0)&jS^VR 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 VBBElrJ8V7O-R(X869L4k{{};s5vBkD literal 0 HcmV?d00001 diff --git a/test-integration/resources/samples/sample_v2_3_ext_header.mp3 b/test-integration/resources/samples/sample_v2_3_ext_header.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..930e25e6b31aa61d425f29f47f7e67c654c6d238 GIT binary patch literal 65674 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%0Dc+ESctzGr3}t|4hySQ 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

9~D9F5v9#eYo&Z}>6PVf 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
hxDyYz%SFR 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 X&E13r1%;G6+}!Nl|NXylaD)C2)joQg literal 0 HcmV?d00001 diff --git a/test-integration/resources/samples/sample_v2_4_unsynch.mp3 b/test-integration/resources/samples/sample_v2_4_unsynch.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..fd2bc6da0fef1683b73ff6afa3492cf17aee980e GIT binary patch literal 12288 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 Luq*ffWuX5C@`$AM literal 0 HcmV?d00001 diff --git a/test-integration/resources/samples/sample_v2_only.mp3 b/test-integration/resources/samples/sample_v2_only.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..dda201f78fc82a725dc03abd30369c6f1d86b98b GIT binary patch literal 12288 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 + + // Insert new picture + pics = [...pics, ...Array(Math.max(3 - pics.length, 0)).fill(undefined)]; + raws[0] = ByteVector.fromPath(StandardFileTests.samplePicture); + pics[0] = Picture.fromPath(StandardFileTests.samplePicture); + pics[0].type = PictureType.BackCover; + pics[0].description = "TEST description 1"; + + raws[1] = ByteVector.fromPath(StandardFileTests.sampleOther); + pics[1] = Picture.fromPath(StandardFileTests.sampleOther); + pics[1].description = "TEST description 2"; + + raws[2] = ByteVector.fromPath(StandardFileTests.samplePicture); + pics[2] = Picture.fromPath(StandardFileTests.samplePicture); + pics[2].filename = "renamed.gif"; + pics[2].type = PictureType.Other; + pics[2].description = "TEST description 3"; + + file.tag.pictures = pics; + file.save(); + + // Read back the tags + file = File.createFromPath(tmpFile, undefined, readStyle); + assert.isOk(file); + pics = file.tag.pictures; + + assert.strictEqual(3, pics.length); + + // Lazy picture check + const isLazy = (readStyle & ReadStyle.PictureLazy) !== 0; + for (let i = 0; i < 3; i++) { + if (isLazy) { + assert.isTrue(pics[i].hasOwnProperty("isLoaded")); + assert.isFalse(( pics[i]).isLoaded); + } else { + if (pics[i].hasOwnProperty("isLoaded")) { + assert.isTrue(( pics[i]).isLoaded); + } + } + } + + assert.strictEqual(pics[0].description, "TEST description 1"); + assert.strictEqual(pics[0].mimeType, "image/gif"); + assert.strictEqual(pics[0].data.length, fs.statSync(this.samplePicture).size); + assert.isTrue(ByteVector.equal(pics[0].data, raws[0])); + + assert.strictEqual(pics[1].description, "TEST description 2"); + assert.strictEqual(pics[1].data.length, fs.statSync(this.sampleOther).size); + assert.isTrue(ByteVector.equal(pics[1].data, raws[1])); + + assert.strictEqual(pics[2].description, "TEST description 3"); + assert.strictEqual(pics[2].mimeType, "image/gif"); + assert.strictEqual(pics[2].data.length, fs.statSync(this.samplePicture).size); + assert.isTrue(ByteVector.equal(pics[2].data, raws[2])); + + // Types and mimetypes assumed to be properly supported at Medium level test + if (level >= TestTagLevel.Medium) { + assert.strictEqual(pics[1].mimeType, "audio/mp4"); + assert.strictEqual(pics[0].type, PictureType.BackCover); + assert.strictEqual(pics[1].type, PictureType.NotAPicture); + assert.strictEqual(pics[2].type, PictureType.Other); + } else { + assert.notStrictEqual(pics[0].type, PictureType.NotAPicture); + assert.strictEqual(pics[1].type, PictureType.NotAPicture); + assert.notStrictEqual(pics[2].type, PictureType.NotAPicture); + } + + // Filename assumed to be properly supported at high level test + if (level >= TestTagLevel.High) { + assert.strictEqual(pics[1].filename, "apple_tags.m4a"); + } else if (level >= TestTagLevel.Medium) { + if (pics[1].filename) { + assert.strictEqual(pics[1].filename, "apple_tags.m4a"); + } + } + } finally { + Utilities.deleteBestEffort(tmpFile); + } + } + + public static writeStandardTags( + sampleFile: string, + tmpFile: string, + level: TestTagLevel = TestTagLevel.Normal, + types: TagTypes = TagTypes.AllTags + ) { + if (sampleFile !== tmpFile && fs.existsSync(tmpFile)) { + fs.unlinkSync(tmpFile); + } + + const shouldCreateTemp = sampleFile !== tmpFile; + if (shouldCreateTemp) { + fs.copyFileSync(sampleFile, tmpFile); + } + + try { + let tmp = File.createFromPath(tmpFile); + if (types !== TagTypes.AllTags) { + tmp.removeTags(~types); + } + + StandardFileTests.setTags(tmp.tag, level); + tmp.save(); + + tmp = File.createFromPath(tmpFile); + StandardFileTests.checkTags(tmp.tag, level); + } finally { + if (shouldCreateTemp) { + Utilities.deleteBestEffort(tmpFile); + } + } + } + + private static checkTags(tag: Tag, level: TestTagLevel) { + assert.strictEqual(tag.album, "TEST album"); + assert.strictEqual(tag.joinedAlbumArtists, "TEST artist 1; TEST artist 2"); + assert.strictEqual(tag.comment, "TEST comment"); + assert.strictEqual(tag.joinedComposers, "TEST composer 1; TEST composer 2"); + assert.strictEqual(tag.conductor, "TEST conductor"); + assert.strictEqual(tag.copyright, "TEST copyright"); + assert.strictEqual(tag.disc, 100); + assert.strictEqual(tag.discCount, 101); + assert.strictEqual(tag.joinedGenres, "TEST genre 1; TEST genre 2"); + assert.strictEqual(tag.grouping, "TEST grouping"); + assert.strictEqual(tag.lyrics, "TEST lyrics 1\r\nTEST lyrics 2"); + assert.strictEqual(tag.joinedPerformers, "TEST performer 1; TEST performer 2"); + assert.strictEqual(tag.title, "TEST title"); + assert.strictEqual(tag.subtitle, "TEST subtitle"); + assert.strictEqual(tag.description, "TEST description"); + assert.strictEqual(tag.track, 98); + assert.strictEqual(tag.trackCount, 99); + assert.strictEqual(tag.year, 1999); + + if (level >= TestTagLevel.Medium) { + assert.strictEqual(tag.titleSort, "title sort, TEST"); + assert.strictEqual(tag.albumSort, "album sort, TEST"); + assert.strictEqual(tag.joinedPerformersSort, "performer sort 1, TEST; performer sort 2, TEST"); + assert.strictEqual(tag.composersSort.join(";"), "composer sort 1, TEST; composer sort 2, TEST"); + assert.strictEqual(tag.albumArtistsSort.join(";"), "album artist sort 1, TEST; album artist sort 2, TEST"); + assert.strictEqual(tag.beatsPerMinute, 120); + assert.strictEqual(tag.performersRole.join("\n"), "TEST role 1a; TEST role 1b\nTEST role 2"); + + const dateTagged = (new Date(2017, 9, 12, 22, 47, 42)).getTime(); + assert.strictEqual(tag.dateTagged.getTime(), dateTagged); + // @TODO: This doesn't correctly handle what happens if the field isn't supported on the version of the tag + } + } + + private static checkNoTags(tag: Tag) { + assert.isNotOk(tag.album); + assert.isNotOk(tag.joinedAlbumArtists); + assert.isNotOk(tag.comment); + assert.isNotOk(tag.conductor); + assert.isNotOk(tag.copyright); + assert.isNotOk(tag.grouping); + assert.isNotOk(tag.lyrics); + + assert.strictEqual(tag.beatsPerMinute, 0); + assert.strictEqual(tag.disc, 0); + assert.strictEqual(tag.discCount, 0); + assert.strictEqual(tag.track, 0); + assert.strictEqual(tag.trackCount, 0); + assert.strictEqual(tag.year, 0); + + assert.isNotOk(tag.joinedComposers); + assert.isNotOk(tag.joinedGenres); + assert.isNotOk(tag.joinedPerformers); + + assert.isNotOk(tag.title); + assert.isNotOk(tag.description); + assert.isNotOk(tag.dateTagged); + assert.isTrue(!tag.performers || tag.performers.length === 0); + assert.isTrue(!tag.performersSort || tag.performersSort.length === 0); + assert.isTrue(!tag.performersRole || tag.performersRole.length === 0); + assert.isTrue(!tag.albumArtistsSort || tag.albumArtistsSort.length === 0); + assert.isTrue(!tag.albumArtists || tag.albumArtists.length === 0); + assert.isTrue(!tag.composers || tag.composers.length === 0); + assert.isTrue(!tag.composersSort || tag.composersSort.length === 0); + + assert.isTrue (tag.isEmpty); + } + + private static setTags(tag: Tag, level: TestTagLevel) { + if (level >= TestTagLevel.Medium) { + tag.titleSort = "title sort, TEST"; + tag.albumSort = "album sort, TEST"; + tag.performersSort = ["performer sort 1, TEST", "performer sort 2, TEST"]; + tag.composersSort = ["composer sort 1, TEST", "composer sort 2, TEST"]; + tag.albumArtistsSort = ["album artist sort 1, TEST", "album artist sort 2, TEST"]; + } + + tag.album = "TEST album"; + tag.albumArtists = ["TEST artist 1", "TEST artist 2"]; + tag.beatsPerMinute = 120; + tag.comment = "TEST comment"; + tag.composers = ["TEST composer 1", "TEST composer 2"]; + tag.conductor = "TEST conductor"; + tag.copyright = "TEST copyright"; + tag.dateTagged = new Date("2017-09-12 22:47:42"); + tag.disc = 100; + tag.discCount = 101; + tag.genres = ["TEST genre 1", "TEST genre 2"]; + tag.grouping = "TEST grouping"; + tag.lyrics = "TEST lyrics 1\r\nTEST lyrics 2"; + tag.performers = ["TEST performer 1", "TEST performer 2"]; + tag.performersRole = ["TEST role 1a; TEST role 1b", "TEST role 2"]; + tag.title = "TEST title"; + tag.subtitle = "TEST subtitle"; + tag.description = "TEST description"; + tag.track = 98; + tag.trackCount = 99; + tag.year = 1999; + } + + private static testCorruptionResistance(path: string): void { + try { + File.createFromPath(path); + } catch (e) { + if (!CorruptFileError.errorIs(e)) { + throw e; + } + } + } +} diff --git a/test-integration/utilities/testConstants.ts b/test-integration/utilities/testConstants.ts new file mode 100644 index 00000000..0362d479 --- /dev/null +++ b/test-integration/utilities/testConstants.ts @@ -0,0 +1,19 @@ +import * as Path from "path"; +import {v4 as Uuidv4} from "uuid"; + +export default class TestConstants { + public static testFileFolderPath: string = "./test-integration/resources"; + + public static getCorruptFilePath: (fileName: string) => string = (fileName: string ) => { + return Path.join(TestConstants.testFileFolderPath, "corruptSamples", fileName); + } + + public static getSampleFilePath: (fileName: string) => string = (fileName: string) => { + return Path.join(TestConstants.testFileFolderPath, "samples", fileName); + } + + public static getTempFilePath: (fileName: string) => string = (fileName: string) => { + const fileUid: string = Uuidv4(); + return Path.join(TestConstants.testFileFolderPath, `${fileUid}_${fileName}`); + } +} diff --git a/test-integration/utilities/utilities.ts b/test-integration/utilities/utilities.ts new file mode 100644 index 00000000..678d431a --- /dev/null +++ b/test-integration/utilities/utilities.ts @@ -0,0 +1,11 @@ +import * as fs from "fs"; + +export default class Utilities { + public static deleteBestEffort(path: string) { + try { + fs.unlinkSync(path); + } catch { + // Ignore failed deletions + } + } +} diff --git a/test-unit/fileTests.ts b/test-unit/fileTests.ts index 8fbdc950..77578110 100644 --- a/test-unit/fileTests.ts +++ b/test-unit/fileTests.ts @@ -78,7 +78,7 @@ const assert = Chai.assert; ), TypeMoq.Times.once()); } finally { // Cleanup - File.clearFileTypesAndResolvers(); + File.removeFileTypeResolver(testResolver.object); } } @@ -113,7 +113,7 @@ const assert = Chai.assert; assert.strictEqual(file.tagTypesOnDisk, TagTypes.None); } finally { // Cleanup - File.clearFileTypesAndResolvers(); + File.removeFileType("taglib/qux"); } } @@ -165,7 +165,7 @@ const assert = Chai.assert; ), TypeMoq.Times.once()); } finally { // Cleanup - File.clearFileTypesAndResolvers(); + File.removeFileTypeResolver(testResolver.object); } } @@ -197,7 +197,7 @@ const assert = Chai.assert; assert.strictEqual(file.tagTypesOnDisk, TagTypes.None); } finally { // Cleanup - File.clearFileTypesAndResolvers(); + File.removeFileType("foo/bar"); } } @@ -219,7 +219,8 @@ const assert = Chai.assert; assert.throws(() => File.createFromPath(TestConstants.testFilePath, "foo/bar")); } finally { // Cleanup - File.clearFileTypesAndResolvers(); + File.removeFileTypeResolver(testResolver.object); + File.removeFileType("foobar/baz"); } } @@ -391,7 +392,7 @@ const assert = Chai.assert; // Act / Assert assert.throws(() => File.addFileType("foo/bar", TestFile, false)); } finally { - File.clearFileTypesAndResolvers(); + File.removeFileType("foo/bar"); } } @@ -707,7 +708,7 @@ const assert = Chai.assert; testAction(file, mockAbstraction); } finally { // Cleanup - File.clearFileTypesAndResolvers(); + File.removeFileTypeResolver(testResolver.object); } } @@ -739,7 +740,7 @@ const assert = Chai.assert; testAction(file, data); } finally { // Cleanup - File.clearFileTypesAndResolvers(); + File.removeFileTypeResolver(testResolver.object); } } } diff --git a/test-unit/id3v1/id3v1TagTests.ts b/test-unit/id3v1/id3v1TagTests.ts new file mode 100644 index 00000000..9d4632bd --- /dev/null +++ b/test-unit/id3v1/id3v1TagTests.ts @@ -0,0 +1,303 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {suite, test} from "mocha-typescript"; + +import Id3v1Tag from "../../src/id3v1/id3v1Tag"; + +// Setup Chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite class Id3v1TagTests { + // NOTE: These tests are just copied from the .NET implementation. ID3v1 is too simple for me + // to bother writing out 100% coverage tests + + @test + public testTitle() { + // New tag must be empty + let tag = Id3v1Tag.empty(); + assert.isTrue(tag.isEmpty); + assert.notOk(tag.title); + + // Make sure round-trip is still empty + let rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isTrue(tag.isEmpty); + assert.notOk(tag.title); + + // Set title, make sure it stays set + tag.title = "01234567890123456789012345678901234567890123456789"; + assert.isFalse(tag.isEmpty); + assert.strictEqual(tag.title, "01234567890123456789012345678901234567890123456789"); + + // Make sure title written out, title is trimmed + rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isFalse(tag.isEmpty); + assert.strictEqual(tag.title, "012345678901234567890123456789"); + + // Clear title, make sure it stays cleared + tag.title = ""; + assert.isTrue(tag.isEmpty); + assert.notOk(tag.title); + + // Make sure title is cleared when written + rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isTrue(tag.isEmpty); + assert.notOk(tag.title); + } + + @test + public testPerformers() { + // New tag must be empty + let tag = Id3v1Tag.empty(); + assert.isTrue(tag.isEmpty); + assert.strictEqual(tag.performers.length, 0); + + // Make sure round-trip is still empty + let rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isTrue(tag.isEmpty); + assert.strictEqual(tag.performers.length, 0); + + // Set performers, make sure it stays set + const performers = ["A123456789", "B123456789", "C123456789", "D123456789", "E123456789"]; + tag.performers = performers; + assert.isFalse(tag.isEmpty); + assert.deepStrictEqual(tag.performers, performers); + + // Make sure performers are written out, field is trimmed + rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isFalse(tag.isEmpty); + assert.deepStrictEqual(tag.performers, ["A123456789", "B123456789", "C1234567"]); + + // Clear performers, make sure it stays clear + tag.performers = []; + assert.isTrue(tag.isEmpty); + assert.deepStrictEqual(tag.performers, []); + + // Make sure it stays cleared when rendered + rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isTrue(tag.isEmpty); + assert.deepStrictEqual(tag.performers, []); + } + + @test + public testAlbum() { + // New tag must be empty + let tag = Id3v1Tag.empty(); + assert.isTrue(tag.isEmpty); + assert.notOk(tag.album); + + // Make sure round-trip is still empty + let rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isTrue(tag.isEmpty); + assert.notOk(tag.album); + + // Set album, make sure it stays set + tag.album = "01234567890123456789012345678901234567890123456789"; + assert.isFalse(tag.isEmpty); + assert.strictEqual(tag.album, "01234567890123456789012345678901234567890123456789"); + + // Make sure album written out, album is trimmed + rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isFalse(tag.isEmpty); + assert.strictEqual(tag.album, "012345678901234567890123456789"); + + // Clear album, make sure it stays cleared + tag.album = ""; + assert.isTrue(tag.isEmpty); + assert.notOk(tag.album); + + // Make sure album is cleared when written + rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isTrue(tag.isEmpty); + assert.notOk(tag.album); + } + + @test + public testYear() { + // New tag must be empty + let tag = Id3v1Tag.empty(); + assert.isTrue(tag.isEmpty); + assert.strictEqual(tag.year, 0); + + // Make sure round-trip is still empty + let rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isTrue(tag.isEmpty); + assert.notOk(tag.year); + + // Set year, make sure it stays set + tag.year = 1999; + assert.isFalse(tag.isEmpty); + assert.strictEqual(tag.year, 1999); + + // Make sure year written out + rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isFalse(tag.isEmpty); + assert.strictEqual(tag.year, 1999); + + // Clear year, make sure it stays cleared + tag.year = 20000; + assert.isTrue(tag.isEmpty); + assert.strictEqual(tag.year, 0); + + // Make sure year is cleared when written + rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isTrue(tag.isEmpty); + assert.strictEqual(tag.year, 0); + } + + @test + public testComment() { + // New tag must be empty + let tag = Id3v1Tag.empty(); + assert.isTrue(tag.isEmpty); + assert.notOk(tag.comment); + + // Make sure round-trip is still empty + let rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isTrue(tag.isEmpty); + assert.notOk(tag.comment); + + // Set comment, make sure it stays set + tag.comment = "01234567890123456789012345678901234567890123456789"; + assert.isFalse(tag.isEmpty); + assert.strictEqual(tag.comment, "01234567890123456789012345678901234567890123456789"); + + // Make sure comment written out, comment is trimmed + rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isFalse(tag.isEmpty); + assert.strictEqual(tag.comment, "0123456789012345678901234567"); + + // Clear comment, make sure it stays cleared + tag.comment = ""; + assert.isTrue(tag.isEmpty); + assert.notOk(tag.comment); + + // Make sure comment is cleared when written + rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isTrue(tag.isEmpty); + assert.notOk(tag.comment); + } + + + @test + public testTrack() { + // New tag must be empty + let tag = Id3v1Tag.empty(); + assert.isTrue(tag.isEmpty); + assert.strictEqual(tag.track, 0); + + // Make sure round-trip is still empty + let rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isTrue(tag.isEmpty); + assert.notOk(tag.track); + + // Set track, make sure it stays set + tag.track = 123; + assert.isFalse(tag.isEmpty); + assert.strictEqual(tag.track, 123); + + // Make sure track written out + rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isFalse(tag.isEmpty); + assert.strictEqual(tag.track, 123); + + // Clear track, make sure it stays cleared + tag.track = 0; + assert.isTrue(tag.isEmpty); + assert.strictEqual(tag.track, 0); + + // Make sure track is cleared when written + rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isTrue(tag.isEmpty); + assert.strictEqual(tag.track, 0); + } + + @test + public testGenres() { + // New tag must be empty + let tag = Id3v1Tag.empty(); + assert.isTrue(tag.isEmpty); + assert.strictEqual(tag.genres.length, 0); + + // Make sure round-trip is still empty + let rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isTrue(tag.isEmpty); + assert.strictEqual(tag.genres.length, 0); + + // Set genres, make sure it only sets it to the first one + tag.genres = ["Rap", "Jazz", "Non-Genre", "Blues"]; + assert.isFalse(tag.isEmpty); + assert.deepStrictEqual(tag.genres, ["Rap"]); + + // Make sure genres are written out + rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isFalse(tag.isEmpty); + assert.deepStrictEqual(tag.genres, ["Rap"]); + + // Set to an unsupported genre, should be empty + tag.genres = ["Non-Genre", "Rap"]; + assert.isTrue(tag.isEmpty); + assert.deepStrictEqual(tag.genres, []); + + // Clear genres, make sure it stays clear + tag.genres = []; + assert.isTrue(tag.isEmpty); + assert.deepStrictEqual(tag.genres, []); + + // Make sure it stays cleared when rendered + rendered = tag.render(); + tag = Id3v1Tag.fromData(rendered); + assert.isTrue(tag.isEmpty); + assert.deepStrictEqual(tag.genres, []); + } + + @test + public testClear() { + const tag = Id3v1Tag.empty(); + tag.title = "A"; + tag.performers = ["B"]; + tag.album = "C"; + tag.year = 123; + tag.comment = "D"; + tag.track = 234; + tag.genres = ["Blues"]; + assert.isFalse(tag.isEmpty); + + tag.clear(); + assert.notOk(tag.title); + assert.deepStrictEqual(tag.performers, []); + assert.notOk(tag.album); + assert.strictEqual(tag.year, 0); + assert.notOk(tag.comment); + assert.strictEqual(tag.track, 0); + assert.deepStrictEqual(tag.genres, []); + assert.isTrue(tag.isEmpty); + } + + @test + public testRender() { + const rendered = Id3v1Tag.empty().render(); + assert.strictEqual(rendered.length, 128); + assert.isTrue(rendered.startsWith(Id3v1Tag.fileIdentifier)); + } +} diff --git a/test-unit/id3v2/attachmentsFrameTests.ts b/test-unit/id3v2/attachmentsFrameTests.ts index 47cbb2c7..dd07756b 100644 --- a/test-unit/id3v2/attachmentsFrameTests.ts +++ b/test-unit/id3v2/attachmentsFrameTests.ts @@ -232,7 +232,7 @@ function getCustomTestFrame(data: ByteVector, desc: string, filename: string, mi "image.gif", "image/gif", StringType.UTF16BE, - undefined + PictureType.NotAPicture ); } diff --git a/test-unit/id3v2/id3v2TagTests.ts b/test-unit/id3v2/id3v2TagTests.ts index bbd84951..a4d55e58 100644 --- a/test-unit/id3v2/id3v2TagTests.ts +++ b/test-unit/id3v2/id3v2TagTests.ts @@ -605,7 +605,7 @@ function getTestTagHeader(version: number, flags: Id3v2TagHeaderFlags, tagSize: // Arrange const tag = Id3v2Tag.fromEmpty(); const frame = TextInformationFrame.fromIdentifier(FrameIdentifiers.TCON); - frame.text = ["32", "foo"]; + frame.text = ["Classical", "foo"]; tag.addFrame(frame); // Act @@ -627,7 +627,7 @@ function getTestTagHeader(version: number, flags: Id3v2TagHeaderFlags, tagSize: assert.strictEqual(tag.frames.length, 1); assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TCON); - assert.deepStrictEqual(( tag.frames[0]).text, ["32", "foo"]); + assert.deepStrictEqual(( tag.frames[0]).text, ["Classical", "foo"]); } @test @@ -645,7 +645,7 @@ function getTestTagHeader(version: number, flags: Id3v2TagHeaderFlags, tagSize: assert.strictEqual(tag.frames.length, 1); assert.strictEqual(tag.frames[0].frameClassType, FrameClassType.TextInformationFrame); assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TCON); - assert.deepStrictEqual(( tag.frames[0]).text, ["32", "foo"]); + assert.deepStrictEqual(( tag.frames[0]).text, ["Classical", "foo"]); } @test @@ -736,9 +736,10 @@ function getTestTagHeader(version: number, flags: Id3v2TagHeaderFlags, tagSize: } @test - public year_noExistingFrame() { + public year_v4noExistingFrame() { // Arrange const tag = Id3v2Tag.fromEmpty(); + tag.version = 4; // Act / Assert assert.strictEqual(tag.year, 0); @@ -754,6 +755,26 @@ function getTestTagHeader(version: number, flags: Id3v2TagHeaderFlags, tagSize: assert.strictEqual(tag.frames.length, 0); } + @test + public year_v3noExistingFrame() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + tag.version = 3; + + // Act / Assert + assert.strictEqual(tag.year, 0); + + tag.year = 1234; + assert.strictEqual(tag.year, 1234); + assert.strictEqual(tag.frames.length, 1); + assert.strictEqual(tag.frames[0].frameId, FrameIdentifiers.TYER); + assert.deepStrictEqual(( tag.frames[0]).text, ["1234"]); + + tag.year = 99999; + assert.strictEqual(tag.year, 0); + assert.strictEqual(tag.frames.length, 0); + } + @test public track_invalidValue() { // Arrange @@ -1905,16 +1926,48 @@ function getTestTagHeader(version: number, flags: Id3v2TagHeaderFlags, tagSize: } @test - public render_v4_unsupportedFrameForVersion() { + public render_v4_unsupportedFrameForVersion_disallowedViaSettings() { // Arrange const tag = Id3v2Tag.fromEmpty(); tag.version = 4; + const originalSetting = Id3v2Settings.strictFrameForVersion; + Id3v2Settings.strictFrameForVersion = true; + const frame1 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TYER); tag.frames.push(frame1); - // Act / Assert - assert.throws(() => { const _ = tag.render(); }); + try { + // Act / Assert + assert.throws(() => { const _ = tag.render(); }); + } finally { + Id3v2Settings.strictFrameForVersion = originalSetting; + } + } + + @test + public render_v4_unsupportedFrameForVersion_allowedViaSettings() { + // Arrange + const tag = Id3v2Tag.fromEmpty(); + tag.version = 4; + + const originalSetting = Id3v2Settings.strictFrameForVersion; + Id3v2Settings.strictFrameForVersion = false; + + const frame1 = TextInformationFrame.fromIdentifier(FrameIdentifiers.TYER); + tag.frames.push(frame1); + + try { + // Act + const bytes = tag.render(); + const rehydratedTag = Id3v2Tag.fromData(bytes); + + // Assert + assert.strictEqual(rehydratedTag.version, 4); + assert.strictEqual(rehydratedTag.frames.length, 0); + } finally { + Id3v2Settings.strictFrameForVersion = originalSetting; + } } @test diff --git a/test-unit/id3v2/textInformationFrameTests.ts b/test-unit/id3v2/textInformationFrameTests.ts index a807e4c1..f3b11758 100644 --- a/test-unit/id3v2/textInformationFrameTests.ts +++ b/test-unit/id3v2/textInformationFrameTests.ts @@ -1,6 +1,6 @@ import * as Chai from "chai"; import * as ChaiAsPromised from "chai-as-promised"; -import {slow, suite, test, timeout} from "mocha-typescript"; +import {suite, test} from "mocha-typescript"; import FrameConstructorTests from "./frameConstructorTests"; import PropertyTests from "../utilities/propertyTests"; @@ -102,13 +102,13 @@ function getTestFrame(): TextInformationFrame { public fromRawData_v4NotTxxx_returnsFrameSplitByDelimiter() { // Arrange const header = new Id3v2FrameHeader(FrameIdentifiers.TCOP); - header.frameSize = 11; + header.frameSize = 17; const data = ByteVector.concatenate( header.render(4), - StringType.Latin1, - ByteVector.fromString("fux", StringType.Latin1), - ByteVector.getTextDelimiter(StringType.Latin1), - ByteVector.fromString("bux", StringType.Latin1), + StringType.UTF16BE, + ByteVector.fromString("fux", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("bux", StringType.UTF16BE), 0x0, 0x0, // Extra nulls to trigger null stripping logic ); @@ -116,20 +116,20 @@ function getTestFrame(): TextInformationFrame { const frame = TextInformationFrame.fromRawData(data, 4); // Assert - this.assertFrame(frame, FrameIdentifiers.TCOP, ["fux", "bux"]); + this.assertFrame(frame, FrameIdentifiers.TCOP, ["fux", "bux"], StringType.UTF16BE); } @test public fromRawData_txxx_returnsFrameSplitByDelimiter() { // Arrange const header = new Id3v2FrameHeader(FrameIdentifiers.TXXX); - header.frameSize = 11; + header.frameSize = 17; const data = ByteVector.concatenate( header.render(4), - StringType.Latin1, - ByteVector.fromString("fux", StringType.Latin1), - ByteVector.getTextDelimiter(StringType.Latin1), - ByteVector.fromString("bux", StringType.Latin1), + StringType.UTF16BE, + ByteVector.fromString("fux", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("bux", StringType.UTF16BE), 0x0, 0x0, // Extra nulls to trigger null stripping logic ); @@ -137,7 +137,7 @@ function getTestFrame(): TextInformationFrame { const frame = TextInformationFrame.fromRawData(data, 4); // Assert - this.assertFrame(frame, FrameIdentifiers.TXXX, ["fux", "bux"]); + this.assertFrame(frame, FrameIdentifiers.TXXX, ["fux", "bux"], StringType.UTF16BE); } @test @@ -163,39 +163,77 @@ function getTestFrame(): TextInformationFrame { // Arrange // - Let's get crazy and try to handle all the cases at once const header = new Id3v2FrameHeader(FrameIdentifiers.TCON); - header.frameSize = 67; + header.frameSize = 137; const data = ByteVector.concatenate( header.render(3), - StringType.Latin1, - ByteVector.fromString("SomeGenre(32)(32)Classical(CR)(RX)Whoa here's some cra((z)y string") + StringType.UTF16BE, + ByteVector.fromString( + "(32)Classical(CR)(RX)Whoa here's some cra((z)y string;here's another", + StringType.UTF16BE + ) ); // Act const frame = TextInformationFrame.fromRawData(data, 3); // Assert - this.assertFrame(frame, FrameIdentifiers.TCON, [ - "SomeGenre", - "32", - "32", - "Cover", - "Remix", - "Whoa here's some cra(z)y string" - ]); + this.assertFrame( + frame, + FrameIdentifiers.TCON, + [ + "Classical", + "Cover", + "Remix", + "Whoa here's some cra(z)y string", + "here's another" + ], + StringType.UTF16BE + ); + } + + @test + public fromRawData_v4Tcon_returnsListOfStrings() { + // Arrange + const header = new Id3v2FrameHeader(FrameIdentifiers.TCON); + header.frameSize = 37; + const data = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("32", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("(32)", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("some genre", StringType.UTF16BE) + ); + + // Act + const frame = TextInformationFrame.fromRawData(data, 4); + + // Assert + this.assertFrame( + frame, + FrameIdentifiers.TCON, + [ + "Classical", + "(32)", + "some genre" + ], + StringType.UTF16BE + ); } @test public fromOffsetRawData_v4_returnsFrameSplitByDelimiter() { // Arrange const header = new Id3v2FrameHeader(FrameIdentifiers.TCOP); - header.frameSize = 11; + header.frameSize = 19; const data = ByteVector.concatenate( header.render(4), 0x00, 0x00, - StringType.Latin1, - ByteVector.fromString("fux", StringType.Latin1), - ByteVector.getTextDelimiter(StringType.Latin1), - ByteVector.fromString("bux", StringType.Latin1), + StringType.UTF16BE, + ByteVector.fromString("fux", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("bux", StringType.UTF16BE), 0x0, 0x0, // Extra nulls to trigger null stripping logic ); @@ -203,17 +241,22 @@ function getTestFrame(): TextInformationFrame { const frame = TextInformationFrame.fromOffsetRawData(data, 2, header, 4); // Assert - this.assertFrame(frame, FrameIdentifiers.TCOP, ["fux", "bux"]); + this.assertFrame(frame, FrameIdentifiers.TCOP, ["fux", "bux"], StringType.UTF16BE); } - private assertFrame(frame: TextInformationFrame, frameId: FrameIdentifier, text: string[]): void { + private assertFrame( + frame: TextInformationFrame, + frameId: FrameIdentifier, + text: string[], + encoding: StringType = StringType.Latin1 + ): void { assert.isOk(frame); assert.strictEqual(frame.frameClassType, FrameClassType.TextInformationFrame); assert.strictEqual(frame.frameId, frameId); assert.isOk(frame.text); assert.deepStrictEqual(frame.text, text); - assert.strictEqual(frame.textEncoding, StringType.Latin1); + assert.strictEqual(frame.textEncoding, encoding); } } @@ -365,17 +408,19 @@ function getTestFrame(): TextInformationFrame { assert.isTrue(ByteVector.equal(output, data)); } + // @TODO: Test for render version not same as input w/o read forces a read + @test public render_readV4NotTxxx() { // Arrange const header = new Id3v2FrameHeader(FrameIdentifiers.TCOP); - header.frameSize = 8; + header.frameSize = 15; const data = ByteVector.concatenate( header.render(4), - Id3v2Settings.defaultEncoding, - ByteVector.fromString("fux", StringType.Latin1), - ByteVector.getTextDelimiter(StringType.Latin1), - ByteVector.fromString("bux", StringType.Latin1), + StringType.UTF16BE, + ByteVector.fromString("fux", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("bux", StringType.UTF16BE), ); const frame = TextInformationFrame.fromRawData(data, 4); const _ = frame.text; // Force a read @@ -421,11 +466,11 @@ function getTestFrame(): TextInformationFrame { public render_readV3IsTxxxSingleValue() { // Arrange let header = new Id3v2FrameHeader(FrameIdentifiers.TXXX); - header.frameSize = 4; + header.frameSize = 7; const data = ByteVector.concatenate( header.render(3), - StringType.UTF8, - ByteVector.fromString("fux", StringType.UTF8) + StringType.UTF16BE, + ByteVector.fromString("fux", StringType.UTF16BE) ); const frame = TextInformationFrame.fromRawData(data, 3); const _ = frame.text; // Force a read @@ -437,59 +482,217 @@ function getTestFrame(): TextInformationFrame { assert.ok(output); header = new Id3v2FrameHeader(FrameIdentifiers.TXXX); - header.frameSize = 11; + header.frameSize = 9; const expected = ByteVector.concatenate( header.render(3), - StringType.UTF16, - ByteVector.fromString("fux", StringType.UTF16), - ByteVector.getTextDelimiter(StringType.UTF16) + StringType.UTF16BE, + ByteVector.fromString("fux", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE) ); assert.isTrue(ByteVector.equal(output, expected)); } @test - public render_readV3Tcon() { - // Arrange - let header = new Id3v2FrameHeader(FrameIdentifiers.TCON); - header.frameSize = 67; - const data = ByteVector.concatenate( - header.render(3), - StringType.Latin1, - ByteVector.fromString( - "SomeGenre(32)(32)Classical(CR)(RX)Whoa here's some cra((z)y string", - StringType.Latin1 - ) - ); - const frame = TextInformationFrame.fromRawData(data, 3); - const _ = frame.text; // Force a read - - // Act - const output = frame.render(3); + public render_readV3Tcon_numericGenresEnabled() { + // Ensure numeric genres are enabled + const oldNumericGenresValue = Id3v2Settings.useNumericGenres; + Id3v2Settings.useNumericGenres = true; + + try { + // Arrange + let header = new Id3v2FrameHeader(FrameIdentifiers.TCON); + header.frameSize = 135; + const data = ByteVector.concatenate( + header.render(3), + StringType.UTF16BE, + ByteVector.fromString( + "(32)Classical(CR)(32)(RX)SomeGenre;Whoa here's some cra((z)y string", + StringType.UTF16BE + ) + ); + const frame = TextInformationFrame.fromRawData(data, 3); + const _ = frame.text; // Force a read + + // Act + const output = frame.render(3); + + // Assert + assert.ok(output); + + header = new Id3v2FrameHeader(FrameIdentifiers.TCON); + header.frameSize = 117; + const expected = ByteVector.concatenate( + header.render(3), + StringType.UTF16BE, + ByteVector.fromString("(32)(CR)(32)(RX)SomeGenre;Whoa here's some cra((z)y string", StringType.UTF16BE) + ); + + assert.isTrue(ByteVector.equal(output, expected)); + } finally { + // Cleanup - Reset numeric genres setting + Id3v2Settings.useNumericGenres = oldNumericGenresValue; + } + } - // Assert - assert.ok(output); + @test + public render_readV3Tcon_numericGenresDisabled() { + // Disable numeric genres + const oldNumericGenresValue = Id3v2Settings.useNumericGenres; + Id3v2Settings.useNumericGenres = false; + + try { + // Arrange + let header = new Id3v2FrameHeader(FrameIdentifiers.TCON); + header.frameSize = 135; + const data = ByteVector.concatenate( + header.render(3), + StringType.UTF16BE, + ByteVector.fromString( + "(32)Classical(CR)(32)(RX)SomeGenre;Whoa here's some cra((z)y string", + StringType.UTF16BE + ) + ); + const frame = TextInformationFrame.fromRawData(data, 3); + const _ = frame.text; // Force a read + + // Act + const output = frame.render(3); + + // Assert + assert.ok(output); + + header = new Id3v2FrameHeader(FrameIdentifiers.TCON); + header.frameSize = 141; + const expected = ByteVector.concatenate( + header.render(3), + StringType.UTF16BE, + ByteVector.fromString( + "(CR)(RX)Classical;Classical;SomeGenre;Whoa here's some cra((z)y string", + StringType.UTF16BE + ) + ); + + assert.isTrue(ByteVector.equal(output, expected)); + } finally { + // Cleanup: Reset numeric genres setting + Id3v2Settings.useNumericGenres = oldNumericGenresValue; + } + } - header = new Id3v2FrameHeader(FrameIdentifiers.TCON); - header.frameSize = 58; - const expected = ByteVector.concatenate( - header.render(3), - StringType.Latin1, - ByteVector.fromString("SomeGenre(32)(32)(CR)(RX)Whoa here's some cra((z)y string", StringType.Latin1) - ); + @test + public render_readV4Tcon_numericGenresEnabled() { + // Ensure numeric genres are enabled + const oldNumericGenresValue = Id3v2Settings.useNumericGenres; + Id3v2Settings.useNumericGenres = true; + + try { + // Arrange + let header = new Id3v2FrameHeader(FrameIdentifiers.TCON); + header.frameSize = 135; + const data = ByteVector.concatenate( + header.render(3), + StringType.UTF16BE, + ByteVector.fromString( + "(32)Classical(CR)(32)(RX)SomeGenre;Whoa here's some cra((z)y string", + StringType.UTF16BE + ) + ); + const frame = TextInformationFrame.fromRawData(data, 3); + const _ = frame.text; // Force a read + + // Act + const output = frame.render(4); + + // Assert + assert.ok(output); + + header = new Id3v2FrameHeader(FrameIdentifiers.TCON); + header.frameSize = 107; + const expected = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("32", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("CR", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("32", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("RX", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("SomeGenre", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("Whoa here's some cra(z)y string", StringType.UTF16BE) + ); + + assert.isTrue(ByteVector.equal(output, expected)); + } finally { + // Cleanup - Reset numeric genres setting + Id3v2Settings.useNumericGenres = oldNumericGenresValue; + } + } - assert.isTrue(ByteVector.equal(output, expected)); + @test + public render_readV4Tcon_numericGenresDisabled() { + // Ensure numeric genres are disabled + const oldNumericGenresValue = Id3v2Settings.useNumericGenres; + Id3v2Settings.useNumericGenres = false; + + try { + // Arrange + let header = new Id3v2FrameHeader(FrameIdentifiers.TCON); + header.frameSize = 135; + const data = ByteVector.concatenate( + header.render(3), + StringType.UTF16BE, + ByteVector.fromString( + "(32)Classical(CR)(32)(RX)SomeGenre;Whoa here's some cra((z)y string", + StringType.UTF16BE + ) + ); + const frame = TextInformationFrame.fromRawData(data, 3); + const _ = frame.text; // Force a read + + // Act + const output = frame.render(4); + + // Assert + assert.ok(output); + + header = new Id3v2FrameHeader(FrameIdentifiers.TCON); + header.frameSize = 135; + const expected = ByteVector.concatenate( + header.render(4), + StringType.UTF16BE, + ByteVector.fromString("Classical", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("CR", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("Classical", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("RX", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("SomeGenre", StringType.UTF16BE), + ByteVector.getTextDelimiter(StringType.UTF16BE), + ByteVector.fromString("Whoa here's some cra(z)y string", StringType.UTF16BE) + ); + + assert.isTrue(ByteVector.equal(output, expected)); + } finally { + // Cleanup - Reset numeric genres setting + Id3v2Settings.useNumericGenres = oldNumericGenresValue; + } } @test public render_readV3WithSplit() { // Arrange const header = new Id3v2FrameHeader(FrameIdentifiers.TCOP); - header.frameSize = 8; + header.frameSize = 15; const data = ByteVector.concatenate( header.render(3), - StringType.Latin1, - ByteVector.fromString("fux/bux", StringType.Latin1) + StringType.UTF16BE, + ByteVector.fromString("fux/bux", StringType.UTF16BE) ); const frame = TextInformationFrame.fromRawData(data, 3); const _ = frame.text; // Force a read @@ -506,11 +709,11 @@ function getTestFrame(): TextInformationFrame { public render_readV3WithoutSplit() { // Arrange const header = new Id3v2FrameHeader(FrameIdentifiers.TCOP); - header.frameSize = 4; + header.frameSize = 7; const data = ByteVector.concatenate( header.render(3), - StringType.Latin1, - ByteVector.fromString("fux", StringType.Latin1) + StringType.UTF16BE, + ByteVector.fromString("fux", StringType.UTF16BE) ); const frame = TextInformationFrame.fromRawData(data, 3); const _ = frame.text; // Force a read diff --git a/test-unit/mpeg/audioHeaderTests.ts b/test-unit/mpeg/audioHeaderTests.ts new file mode 100644 index 00000000..4d78b073 --- /dev/null +++ b/test-unit/mpeg/audioHeaderTests.ts @@ -0,0 +1,799 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import * as TypeMoq from "typemoq"; +import TestFile from "../utilities/testFile"; +import {suite, test} from "mocha-typescript"; + +import VbriHeader from "../../src/mpeg/vbriHeader"; +import XingHeader from "../../src/mpeg/xingHeader"; +import {AudioHeader} from "../../src/mpeg/audioHeader"; +import {ByteVector} from "../../src/byteVector"; +import {File} from "../../src/file"; +import {MediaTypes} from "../../src/iCodec"; +import {ChannelMode, MpegVersion} from "../../src/mpeg/mpegEnums"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite class MpegAudioHeader_ConstructorTests { + private mockFile = TypeMoq.Mock.ofType().object; + + @test + public unknownHeader() { + // Act + const header = AudioHeader.Unknown; + + // Assert + this.assertHeader( + header, + 0, + 2, + 0, + 1, + 11025, + ChannelMode.Stereo, + "MPEG Version 2.5 Audio, Layer 1", + 0, + false, + false, + false, + true, + MediaTypes.Audio, + MpegVersion.Version25 + ); + assert.strictEqual(header.vbriHeader, VbriHeader.unknown); + assert.strictEqual(header.xingHeader, XingHeader.unknown); + } + + @test + public fromInfo_invalidArguments() { + // Act/Assert + assert.throws(() => { + AudioHeader.fromInfo( + -1, + 123, + XingHeader.unknown, + VbriHeader.unknown + ); + }); + assert.throws(() => { + AudioHeader.fromInfo( + 1.23, + 123, + XingHeader.unknown, + VbriHeader.unknown + ); + }); + assert.throws(() => { + AudioHeader.fromInfo( + Number.MAX_SAFE_INTEGER + 1, + 123, + XingHeader.unknown, + VbriHeader.unknown + ); + }); + assert.throws(() => { + AudioHeader.fromInfo( + 123, + -1, + XingHeader.unknown, + VbriHeader.unknown + ); + }); + assert.throws(() => { + AudioHeader.fromInfo( + 123, + 1.23, + XingHeader.unknown, + VbriHeader.unknown + ); + }); + assert.throws(() => { + AudioHeader.fromInfo( + 123, + Number.MAX_SAFE_INTEGER + 1, + XingHeader.unknown, + VbriHeader.unknown + ); + }); + assert.throws(() => {AudioHeader.fromInfo(123, 123, undefined, VbriHeader.unknown); }); + assert.throws(() => {AudioHeader.fromInfo(123, 123, null, VbriHeader.unknown); }); + assert.throws(() => {AudioHeader.fromInfo(123, 123, XingHeader.unknown, undefined); }); + assert.throws(() => {AudioHeader.fromInfo(123, -1, XingHeader.unknown, null); }); + } + + @test + public fromInfo_validArguments() { + // Act + const header = AudioHeader.fromInfo(0, 0, XingHeader.unknown, VbriHeader.unknown); + + // Assert + this.assertHeader( + header, + 0, + 2, + 0, + 1, + 11025, + ChannelMode.Stereo, + "MPEG Version 2.5 Audio, Layer 1", + 0, + false, + false, + false, + true, + MediaTypes.Audio, + MpegVersion.Version25 + ); + assert.strictEqual(header.vbriHeader, VbriHeader.unknown); + assert.strictEqual(header.xingHeader, XingHeader.unknown); + } + + @test + public fromData_invalidArguments() { + // Arrange + const data = ByteVector.empty(); + + // Act / Assert + assert.throws(() => { AudioHeader.fromData(undefined, this.mockFile, 1); }); + assert.throws(() => { AudioHeader.fromData(null, this.mockFile, 1); }); + assert.throws(() => { AudioHeader.fromData(data, undefined, 1); }); + assert.throws(() => { AudioHeader.fromData(data, null, 1); }); + assert.throws(() => { AudioHeader.fromData(data, this.mockFile, -1); }); + assert.throws(() => { AudioHeader.fromData(data, this.mockFile, 1.23); }); + assert.throws(() => { AudioHeader.fromData(data, this.mockFile, Number.MAX_SAFE_INTEGER + 1); }); + } + + @test + public fromData_headerTooShort() { + // Arrange + const data = ByteVector.fromSize(2); + + // Act / Assert + assert.throws(() => { AudioHeader.fromData(data, this.mockFile, 1); }); + } + + @test + public fromData_headerDoesNotStartWithSyncByte() { + // Arrange + const data = ByteVector.fromSize(4); + data.set(0, 0xEE); + + // Act / Assert + assert.throws(() => { AudioHeader.fromData(data, this.mockFile, 1); }); + } + + @test + public fromData_secondByteDoesNotMatchMpegSync() { + // Arrange + const data1 = ByteVector.fromSize(4); + data1.set(0, 0xFF); + data1.set(1, 0x10); + const data2 = ByteVector.fromSize(4); + data2.set(0, 0xFF); + data2.set(1, 0xE8); + + // Act / Assert + assert.throws(() => { AudioHeader.fromData(data1, this.mockFile, 1); }); + assert.throws(() => { AudioHeader.fromData(data2, this.mockFile, 1); }); + } + + @test + public fromData_invalidBitrate() { + // Arrange + const data = ByteVector.fromSize(4); + data.set(0, 0xFF); + data.set(1, 0xF2); + data.set(2, 0xF0); + + // Act / Assert + assert.throws(() => { AudioHeader.fromData(data, this.mockFile, 1); }); + } + + @test + public fromData_invalidSampleRate() { + // Arrange + const data = ByteVector.fromSize(4); + data.set(0, 0xFF); + data.set(1, 0xF2); + data.set(2, 0xEC); + + // Act / Assert + assert.throws(() => { AudioHeader.fromData(data, this.mockFile, 1); }); + } + + @test + public fromData_noVbrheaders() { + // Arrange - MPEG2, Layer2, 64kbps, 22050kHz - padded + const data = ByteVector.fromUInt(0xFFF48200, true); + data.addByteVector(ByteVector.fromSize(100)); + const mockFile = TestFile.getFile(data); + + // Act + const header = AudioHeader.fromData(data, mockFile, 0); + + // Assert + this.assertHeader( + header, + 64, + 2, + 418, + 2, + 22050, + ChannelMode.Stereo, + "MPEG Version 2 Audio, Layer 2", + 0, + false, + false, + true, + true, + MediaTypes.Audio, + MpegVersion.Version2 + ); + assert.strictEqual(header.xingHeader, XingHeader.unknown); + assert.strictEqual(header.vbriHeader, VbriHeader.unknown); + } + + @test + public fromData_hasXingHeader() { + // Arrange - MPEG1, Layer1, 256kbps (via flags), 44100kHz, Stereo, not padded + const mpegFlags = ByteVector.fromUInt(0xFFFE8000, true); + const data = ByteVector.concatenate( + ByteVector.fromSize(1), + mpegFlags, + ByteVector.fromSize(32), + ByteVector.concatenate( + XingHeader.fileIdentifier, + 0x00, 0x00, 0x00, 0x03, + ByteVector.fromUInt(12), + ByteVector.fromUInt(23) + ) // Calculates to 2kbps via Xing header + ); + const mockFile = TestFile.getFile(data); + + // Act + const header = AudioHeader.fromData(mpegFlags, mockFile, 1); + + // Assert + this.assertHeader( + header, + 2, + 2, + 2, + 1, + 44100, + ChannelMode.Stereo, + "MPEG Version 1 Audio, Layer 1 VBR", + 104.4, + false, + false, + false, + true, + MediaTypes.Audio, + MpegVersion.Version1 + ); + assert.isOk(header.xingHeader); + assert.notEqual(header.xingHeader, XingHeader.unknown); + assert.strictEqual(header.xingHeader.totalFrames, 12); + assert.strictEqual(header.xingHeader.totalSize, 23); + assert.isTrue(header.xingHeader.isPresent); + + assert.strictEqual(header.vbriHeader, VbriHeader.unknown); + } + + @test + public fromData_hasVbriHeader() { + // Arrange - MPEG1, Layer1, 256kbps (via flags), 44100kHz, Stereo, not padded + const mpegFlags = ByteVector.fromUInt(0xFFFE8000, true); + const data = ByteVector.concatenate( + ByteVector.fromSize(1), + mpegFlags, + ByteVector.fromSize(32), + ByteVector.concatenate( + VbriHeader.fileIdentifier, + ByteVector.fromSize(6), + ByteVector.fromUInt(234), + ByteVector.fromUInt(123), + ByteVector.fromSize(6) + ) + ); + const mockFile = TestFile.getFile(data); + + // Act + const header = AudioHeader.fromData(mpegFlags, mockFile, 1); + + // Assert + this.assertHeader( + header, + 2, + 2, + 2, + 1, + 44100, + ChannelMode.Stereo, + "MPEG Version 1 Audio, Layer 1 VBR", + 1000, + false, + false, + false, + true, + MediaTypes.Audio, + MpegVersion.Version1 + ); + assert.isOk(header.vbriHeader); + assert.notEqual(header.vbriHeader, VbriHeader.unknown); + assert.strictEqual(header.vbriHeader.totalFrames, 123); + assert.strictEqual(header.vbriHeader.totalSize, 234); + assert.isTrue(header.vbriHeader.isPresent); + + assert.strictEqual(header.xingHeader, XingHeader.unknown); + } + + private assertHeader( + header: AudioHeader, + bitrate: number, + channels: number, + frameLength: number, + layer: number, + sampleRate: number, + channelMode: ChannelMode, + description: string, + duration: number, + isCopyrighted: boolean, + isOriginal: boolean, + isPadded: boolean, + isProtected: boolean, + mediaTypes: MediaTypes, + version: MpegVersion + ) { + assert.ok(header); + + assert.strictEqual(header.audioBitrate, bitrate); + assert.strictEqual(header.audioChannels, channels); + assert.strictEqual(header.audioFrameLength, frameLength); + assert.strictEqual(header.audioLayer, layer); + assert.strictEqual(header.audioSampleRate, sampleRate); + assert.strictEqual(header.channelMode, channelMode); + assert.strictEqual(header.description, description); + assert.approximately(header.durationMilliseconds, duration, 0.1); + assert.strictEqual(header.isCopyrighted, isCopyrighted); + assert.strictEqual(header.isOriginal, isOriginal); + assert.strictEqual(header.isPadded, isPadded); + assert.strictEqual(header.isProtected, isProtected); + assert.strictEqual(header.mediaTypes, mediaTypes); + assert.strictEqual(header.version, version); + } +} + +@suite class MpegAudioHeader_PropertyTests { + @test + public audioBitrateDuration_noVbrMpeg1Layer2_256() { + // Arrange + const flags = 0x1E8000; // MPEG1, Layer1, 256kbps, 44100kHz, 278 frame length + const header = AudioHeader.fromInfo(flags, 1024, XingHeader.unknown, VbriHeader.unknown); + + // Act/Assert + assert.strictEqual(header.audioBitrate, 256); + assert.strictEqual(header.durationMilliseconds, 34.75); + } + + @test + public audioBitrateDuration_noVbrMpeg2Layer2_64() { + // Arrange + const flags = 0x148000; // MPEG2, Layer2, 64kbps, 22050kHz, 417 frame length + const header = AudioHeader.fromInfo(flags, 1024, XingHeader.unknown, VbriHeader.unknown); + + // Act/Assert + assert.strictEqual(header.audioBitrate, 64); + assert.strictEqual(header.durationMilliseconds, 156.375); + } + + @test + public audioBitreateDuration_noVbrMpeg25Layer3_32() { + // Arrange + const flags = 0x24000; // MPEG2.5, Layer3, 32kbps, 11025kHz + const header = AudioHeader.fromInfo(flags, 1024, XingHeader.unknown, VbriHeader.unknown); + + // Act/Assert + assert.strictEqual(header.audioBitrate, 32); + assert.strictEqual(header.durationMilliseconds, 260); + } + + @test + public audioBitrateDuration_withXingHeaderNoTotalSize_defaultsToFlags() { + // Arrange + const flags = 0x1E8000; // MPEG1, Layer1, 256kbps, 44100kHz + const xingHeader = XingHeader.fromInfo(10, 0); + const header = AudioHeader.fromInfo(flags, 1024, xingHeader, VbriHeader.unknown); + + // Act/Assert + assert.strictEqual(header.audioBitrate, 256); + assert.approximately(header.durationMilliseconds, 87.07482993, 0.00000001); + } + + @test + public audioBitrate_withXingHeaderHasTotalSizeNoFrames_defaultsToFlags() { + // Arrange + const flags = 0x1E8000; // MPEG1, Layer1, 256kbps, 44100kHz + const xingHeader = XingHeader.fromInfo(0, 10); + const header = AudioHeader.fromInfo(flags, 1024, xingHeader, VbriHeader.unknown); + + // Act/Assert + assert.strictEqual(header.audioBitrate, 256); + assert.strictEqual(header.durationMilliseconds, 34.75); + } + + @test + public audioBitrateDuration_withXingHeaderHasTotalSizeHasFramesHasDuration() { + // Arrange + const flags = 0x1E8000; // MPEG1, Layer1, 256kbps, 44100kHz + const xingHeader = XingHeader.fromInfo(12, 23); + const header = AudioHeader.fromInfo(flags, 1024, xingHeader, VbriHeader.unknown); + + // Act/Assert + assert.strictEqual(header.audioBitrate, 2); + assert.approximately(header.durationMilliseconds, 104.4897959, 0.0000001); + } + + @test + public audioBitrateDuration_withVbriHeaderNoTotalSize_defaultsToFlags() { + // Arrange + const flags = 0x1E8000; // MPEG1, Layer1, 256kbps, 44100kHz + const vbriHeader = VbriHeader.fromInfo(123, 0); + const header = AudioHeader.fromInfo(flags, 1024, XingHeader.unknown, vbriHeader); + + // Act/Assert + assert.strictEqual(header.audioBitrate, 256); + assert.strictEqual(header.durationMilliseconds, 1000); + } + + @test + public audioBitrate_withVbriHeaderHasTotalSizeNoFrames_defaultsToFlags() { + // Arrange + const flags = 0x1E8000; // MPEG1, Layer1, 256kbps, 44100kHz + const vbriHeader = VbriHeader.fromInfo(0, 10); + const header = AudioHeader.fromInfo(flags, 1024, XingHeader.unknown, vbriHeader); + + // Act/Assert + assert.strictEqual(header.audioBitrate, 256); + assert.strictEqual(header.durationMilliseconds, 34.75); + } + + @test + public audioBitrate_withVbriHeaderHasTotalSizeHasFramesHasDuration() { + // Arrange + const flags = 0x1E8000; // MPEG1, Layer1, 256kbps, 44100kHz + const vbriHeader = VbriHeader.fromInfo(123, 234); + const header = AudioHeader.fromInfo(flags, 1024, XingHeader.unknown, vbriHeader); + + // Act/Assert + assert.strictEqual(header.audioBitrate, 2); + assert.strictEqual(header.durationMilliseconds, 1000); + } + + @test + public audioFrameLength_layer1() { + // Test 1: Padded + const flags1 = 0x1E8200; // MPEG1, Layer1, 256kbps, 44100kHz - padded + const header1 = AudioHeader.fromInfo(flags1, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header1.audioFrameLength, 282); + + // Test 2: Unpadded + const flags2 = 0x1E8000; // MPEG1, Layer1, 256kbps, 44100kHz - unpadded + const header2 = AudioHeader.fromInfo(flags2, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header2.audioFrameLength, 278); + } + + @test + public audioFrameLength_layer2Version2() { + // Test 1: Padded + const flags1 = 0x148200; // MPEG2, Layer2, 64kbps, 22050kHz - padded + const header1 = AudioHeader.fromInfo(flags1, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header1.audioFrameLength, 418); + + // Test 2: Unpadded + const flags2 = 0x148000; // MPEG2, Layer2, 64kbps, 22050kHz - unpadded + const header2 = AudioHeader.fromInfo(flags2, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header2.audioFrameLength, 417); + } + + @test + public audioFrameLength_layer3Version1() { + // Test 1: Padded + const flags1 = 0xA4200; // MPEG1, Layer3, 56kbps, 11025kHz - padded + const header1 = AudioHeader.fromInfo(flags1, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header1.audioFrameLength, 183); + + // Test 2: Unpadded + const flags2 = 0xA4000; // MPEG1, Layer3, 56kbps, 11025kHz - unpadded + const header2 = AudioHeader.fromInfo(flags2, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header2.audioFrameLength, 182); + } + + @test + public audioFrameLength_layer3Version25() { + // Test 1: Padded + const flags1 = 0x24200; // MPEG2.5, Layer3, 32kbps, 11025kHz - padded + const header1 = AudioHeader.fromInfo(flags1, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header1.audioFrameLength, 209); + + // Test 2: Unpadded + const flags2 = 0x24000; // MPEG2.5, Layer3, 32kbps, 11025kHz - unpadded + const header2 = AudioHeader.fromInfo(flags2, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header2.audioFrameLength, 208); + } + + @test + public audioLayer_layer1() { + // Test 1: 00 + const flags1 = 0xFFF9FFFF; + const header1 = AudioHeader.fromInfo(flags1, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header1.audioLayer, 1); + + // Test 2: 11 + const flags2 = 0xFFFFFFFF; + const header2 = AudioHeader.fromInfo(flags2, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header2.audioLayer, 1); + } + + @test + public audioLayer_layer2() { + const flags = 0xFFFDFFFF; + const header = AudioHeader.fromInfo(flags, 0, XingHeader.unknown, VbriHeader.unknown); + + // Act / Assert + assert.strictEqual(header.audioLayer, 2); + } + + @test + public audioLayer_layer3() { + const flags = 0xFFFBFFFF; + const header = AudioHeader.fromInfo(flags, 0, XingHeader.unknown, VbriHeader.unknown); + + // Act / Assert + assert.strictEqual(header.audioLayer, 3); + } + + @test + public audioSampleRate() { + // Test 1: Version 1 + const header1 = AudioHeader.fromInfo(0xFFFFF7FF, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header1.audioSampleRate, 48000); + + // Test 2: Version 2 + const header2 = AudioHeader.fromInfo(0xFFF7F7FF, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header2.audioSampleRate, 24000); + + // Test 3: Version 3 + const header3 = AudioHeader.fromInfo(0xFFE7F7FF, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header3.audioSampleRate, 12000); + } + + @test + public channels() { + // Test 1: Stereo + const header1 = AudioHeader.fromInfo(0xFFFFFF3F, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header1.channelMode, ChannelMode.Stereo); + assert.strictEqual(header1.audioChannels, 2); + + // Test 2: Joint Stereo + const header2 = AudioHeader.fromInfo(0xFFFFFF7F, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header2.channelMode, ChannelMode.JointStereo); + assert.strictEqual(header2.audioChannels, 2); + + // Test 3: Dual Stereo + const header3 = AudioHeader.fromInfo(0xFFFFFFBF, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header3.channelMode, ChannelMode.DualChannel); + assert.strictEqual(header3.audioChannels, 2); + + // Test 4: Mono + const header4 = AudioHeader.fromInfo(0xFFFFFFFF, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header4.channelMode, ChannelMode.SingleChannel); + assert.strictEqual(header4.audioChannels, 1); + } + + @test + public isCopyrighted() { + // Test 1: 00 => false + const header1 = AudioHeader.fromInfo(0xFFFFFFE7, 0, XingHeader.unknown, VbriHeader.unknown); + assert.isFalse(header1.isCopyrighted); + + // Test 2: 01 => true + const header2 = AudioHeader.fromInfo(0xFFFFFFEF, 0, XingHeader.unknown, VbriHeader.unknown); + assert.isTrue(header2.isCopyrighted); + } + + @test + public isOriginal() { + // Test 1: 00 => false + const header1 = AudioHeader.fromInfo(0xFFFFFFF3, 0, XingHeader.unknown, VbriHeader.unknown); + assert.isFalse(header1.isOriginal); + + // Test 2: 01 => true + const header2 = AudioHeader.fromInfo(0xFFFFFFFF, 0, XingHeader.unknown, VbriHeader.unknown); + assert.isTrue(header2.isOriginal); + } + + @test + public isPadded() { + // Test 1: 0 => false + const header1 = AudioHeader.fromInfo(0xFFFFFFF3, 0, XingHeader.unknown, VbriHeader.unknown); + assert.isFalse(header1.isOriginal); + + // Test 2: 1 => true + const header2 = AudioHeader.fromInfo(0xFFFFFFFF, 0, XingHeader.unknown, VbriHeader.unknown); + assert.isTrue(header2.isOriginal); + } + + @test + public isProtected() { + // Test 1: 0 => true + const header1 = AudioHeader.fromInfo(0xFFFEFFFF, 0, XingHeader.unknown, VbriHeader.unknown); + assert.isTrue(header1.isProtected); + + // Test 2: 1 => false + const header2 = AudioHeader.fromInfo(0xFFFFFFFF, 0, XingHeader.unknown, VbriHeader.unknown); + assert.isFalse(header2.isProtected); + } + + @test + public streamLength_set_noVbr() { + // Arrange - MPEG2, Layer2, 64kbps, 22050kHz - padded + const header = AudioHeader.fromInfo(0xFFF48200, 1234, XingHeader.unknown, VbriHeader.unknown); + const _ = header.durationMilliseconds; // Force calculation of durationMilliseconds + + // Act + header.streamLength = 2345; + + // Assert - duration has been recalculated with new stream length + assert.strictEqual(header.durationMilliseconds, 313.5); + } + + @test + public streamLength_set_withXingHeader() { + // Arrange - MPEG2, Layer2, 64kbps, 22050kHz - padded + const xingHeader = XingHeader.fromInfo(123, 234); + const header = AudioHeader.fromInfo(0xFFF48200, 1234, xingHeader, VbriHeader.unknown); + const originalDuration = header.durationMilliseconds; // Force calculation of durationMilliseconds + + // Act + header.streamLength = 2345; + + // Assert - Duration has not been recalculated + assert.strictEqual(header.durationMilliseconds, originalDuration); + } + + @test + public streamLength_set_withVbriHeader() { + // Arrange - MPEG2, Layer2, 64kbps, 22050kHz - padded + const vbriHeader = VbriHeader.fromInfo(123, 234); + const header = AudioHeader.fromInfo(0xFFF48200, 1234, XingHeader.unknown, vbriHeader); + const originalDuration = header.durationMilliseconds; // Force calculation of durationMilliseconds + + // Act + header.streamLength = 2345; + + // Assert - Duration has not been recalculated + assert.strictEqual(header.durationMilliseconds, originalDuration); + } + + @test + public version_version1() { + // Test 1: 01 + const flags1 = 0xFFFEFFFF; + const header1 = AudioHeader.fromInfo(flags1, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header1.version, MpegVersion.Version1); + + // Test 2: 11 + const flags2 = 0xFFFFFFFF; + const header2 = AudioHeader.fromInfo(flags2, 0, XingHeader.unknown, VbriHeader.unknown); + assert.strictEqual(header2.version, MpegVersion.Version1); + } + + @test + public version_version2() { + // Arrange + const flags = 0xFFF7FFFF; + const header = AudioHeader.fromInfo(flags, 0, XingHeader.unknown, VbriHeader.unknown); + + // Act / Assert + assert.strictEqual(header.version, MpegVersion.Version2); + } + + @test + public version_version25() { + // Arrange + const flags = 0xFFE7FFFF; + const header = AudioHeader.fromInfo(flags, 0, XingHeader.unknown, VbriHeader.unknown); + + // Act / Assert + assert.strictEqual(header.version, MpegVersion.Version25); + } +} + +@suite class MpegAudioHeader_MethodTests { + @test + public find_invalidParameters() { + // Arrange + const mockFile = TestFile.getFile(ByteVector.empty()); + + // Act / Assert + assert.throws(() => { AudioHeader.find(undefined, 123, 234); }); + assert.throws(() => { AudioHeader.find(null, 123, 234); }); + assert.throws(() => { AudioHeader.find(mockFile, 1.23, 234); }); + assert.throws(() => { AudioHeader.find(mockFile, Number.MIN_SAFE_INTEGER - 1, 234); }); + assert.throws(() => { AudioHeader.find(mockFile, Number.MAX_SAFE_INTEGER + 1, 234); }); + assert.throws(() => { AudioHeader.find(mockFile, 123, 2.34); }); + assert.throws(() => { AudioHeader.find(mockFile, 123, Number.MIN_SAFE_INTEGER - 1); }); + assert.throws(() => { AudioHeader.find(mockFile, 123, Number.MAX_SAFE_INTEGER + 1); }); + } + + @test + public find_lessThan3BytesAtPosition() { + // Arrange + const data = ByteVector.fromSize(3); + const mockFile = TestFile.getFile(data); + + // Act + const result = AudioHeader.find(mockFile, 1); + + // Assert + assert.isOk(result); + assert.strictEqual(result.header, AudioHeader.Unknown); + assert.isFalse(result.success); + } + + @test + public find_notInFile_noLimit() { + // Arrange + const data = ByteVector.fromSize(File.bufferSize * 2); + const mockFile = TestFile.getFile(data); + + // Act + const result = AudioHeader.find(mockFile, 1); + + // Assert + assert.isOk(result); + assert.strictEqual(result.header, AudioHeader.Unknown); + assert.isFalse(result.success); + } + + @test + public find_inFile_notFoundWithinLimit() { + // Arrange + const data = ByteVector.concatenate( + ByteVector.fromSize(File.bufferSize * 2), + ByteVector.fromUInt(0xFFF48200, true), + ByteVector.fromSize(100) + ); + const mockFile = TestFile.getFile(data); + + // Act + const result = AudioHeader.find(mockFile, 1, 1024); + + // Assert + assert.isOk(result); + assert.strictEqual(result.header, AudioHeader.Unknown); + assert.isFalse(result.success); + } + + @test + public find_inFile() { + // Arrange + const data = ByteVector.concatenate( + ByteVector.fromSize(File.bufferSize), + ByteVector.fromUInt(0xFFF48200, true), + ByteVector.fromSize(100) + ); + const mockFile = TestFile.getFile(data); + + // Act + const result = AudioHeader.find(mockFile, 1); + + // Assert + assert.isOk(result); + assert.notEqual(result.header, AudioHeader.Unknown); + assert.isTrue(result.success); + } +} diff --git a/test-unit/mpeg/vbriHeaderTests.ts b/test-unit/mpeg/vbriHeaderTests.ts new file mode 100644 index 00000000..957af820 --- /dev/null +++ b/test-unit/mpeg/vbriHeaderTests.ts @@ -0,0 +1,71 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {suite, test} from "mocha-typescript"; + +import VbriHeader from "../../src/mpeg/vbriHeader"; +import {ByteVector} from "../../src/byteVector"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite class MpegXingHeaderTests { + @test + public fromInfo_invalidParameters() { + // Act / Assert + assert.throws(() => { VbriHeader.fromInfo(-1, 123); }); + assert.throws(() => { VbriHeader.fromInfo(1.23, 123); }); + assert.throws(() => { VbriHeader.fromInfo(Number.MAX_SAFE_INTEGER + 1, 123); }); + assert.throws(() => { VbriHeader.fromInfo(123, -1); }); + assert.throws(() => { VbriHeader.fromInfo(123, 1.23); }); + assert.throws(() => { VbriHeader.fromInfo(123, Number.MAX_SAFE_INTEGER + 1); }); + } + + @test + public fromInfo_validParams() { + // Act + const header = VbriHeader.fromInfo(123, 234); + + // Assert + assert.isOk(header); + assert.strictEqual(header.totalFrames, 123); + assert.strictEqual(header.totalSize, 234); + assert.isFalse(header.isPresent); + } + + @test + public fromData_invalidParameters() { + // Act / Assert + assert.throws(() => { VbriHeader.fromData(undefined); }); + assert.throws(() => { VbriHeader.fromData(null); }); + } + + @test + public fromData_invalidVbriHeader() { + // Arrange + const data = ByteVector.fromSize(16); + + // Act / Assert + assert.throws(() => { VbriHeader.fromData(data); }); + } + + @test + public fromData_validParameters() { + // Arrange + const data = ByteVector.concatenate( + VbriHeader.fileIdentifier, + ByteVector.fromSize(6), + ByteVector.fromUInt(123), + ByteVector.fromUInt(234) + ); + + // Act + const header = VbriHeader.fromData(data); + + // Assert + assert.strictEqual(header.totalFrames, 234); + assert.strictEqual(header.totalSize, 123); + assert.isTrue(header.isPresent); + } +} + diff --git a/test-unit/mpeg/xingHeaderTests.ts b/test-unit/mpeg/xingHeaderTests.ts new file mode 100644 index 00000000..6f623f91 --- /dev/null +++ b/test-unit/mpeg/xingHeaderTests.ts @@ -0,0 +1,124 @@ +import * as Chai from "chai"; +import * as ChaiAsPromised from "chai-as-promised"; +import {suite, test} from "mocha-typescript"; + +import XingHeader from "../../src/mpeg/xingHeader"; +import {ByteVector} from "../../src/byteVector"; +import {ChannelMode, MpegVersion} from "../../src/mpeg/mpegEnums"; + +// Setup chai +Chai.use(ChaiAsPromised); +const assert = Chai.assert; + +@suite class MpegXingHeaderTests { + @test + public fromInfo_invalidParameters() { + // Act / Assert + assert.throws(() => { XingHeader.fromInfo(-1, 123); }); + assert.throws(() => { XingHeader.fromInfo(1.23, 123); }); + assert.throws(() => { XingHeader.fromInfo(Number.MAX_SAFE_INTEGER + 1, 123); }); + assert.throws(() => { XingHeader.fromInfo(123, -1); }); + assert.throws(() => { XingHeader.fromInfo(123, 1.23); }); + assert.throws(() => { XingHeader.fromInfo(123, Number.MAX_SAFE_INTEGER + 1); }); + } + + @test + public fromInfo_validParams() { + // Act + const header = XingHeader.fromInfo(123, 234); + + // Assert + assert.isOk(header); + assert.strictEqual(header.totalFrames, 123); + assert.strictEqual(header.totalSize, 234); + assert.isFalse(header.isPresent); + } + + @test + public fromData_invalidParameters() { + // Act / Assert + assert.throws(() => { XingHeader.fromData(undefined); }); + assert.throws(() => { XingHeader.fromData(null); }); + } + + @test + public fromData_invalidXingHeader() { + // Arrange + const data = ByteVector.fromSize(16); + + // Act / Assert + assert.throws(() => { XingHeader.fromData(data); }); + } + + @test + public fromData_zeroTotalFrames() { + // Arrange + const data = ByteVector.concatenate( + XingHeader.fileIdentifier, + 0x00, 0x00, 0x00, 0x02, + ByteVector.fromUInt(123) + ); + + // Act + const header = XingHeader.fromData(data); + + // Assert + assert.strictEqual(header.totalFrames, 0); + assert.strictEqual(header.totalSize, 123); + assert.isTrue(header.isPresent); + } + + @test + public fromData_zeroTotalSize() { + // Arrange + const data = ByteVector.concatenate( + XingHeader.fileIdentifier, + 0x00, 0x00, 0x00, 0x01, + ByteVector.fromUInt(123) + ); + + // Act + const header = XingHeader.fromData(data); + + // Assert + assert.strictEqual(header.totalFrames, 123); + assert.strictEqual(header.totalSize, 0); + assert.isTrue(header.isPresent); + } + + @test + public fromData_totalSizeAndFrames() { + // Arrange + const data = ByteVector.concatenate( + XingHeader.fileIdentifier, + 0x00, 0x00, 0x00, 0x03, + ByteVector.fromUInt(123), + ByteVector.fromUInt(234) + ); + + // Act + const header = XingHeader.fromData(data); + + // Assert + assert.strictEqual(header.totalFrames, 123); + assert.strictEqual(header.totalSize, 234); + assert.isTrue(header.isPresent); + } + + @test + public xingHeaderOffset() { + // Act / Assert + assert.strictEqual(XingHeader.xingHeaderOffset(MpegVersion.Version1, ChannelMode.DualChannel), 0x24); + assert.strictEqual(XingHeader.xingHeaderOffset(MpegVersion.Version1, ChannelMode.JointStereo), 0x24); + assert.strictEqual(XingHeader.xingHeaderOffset(MpegVersion.Version1, ChannelMode.SingleChannel), 0x15); + assert.strictEqual(XingHeader.xingHeaderOffset(MpegVersion.Version1, ChannelMode.Stereo), 0x24); + assert.strictEqual(XingHeader.xingHeaderOffset(MpegVersion.Version2, ChannelMode.DualChannel), 0x15); + assert.strictEqual(XingHeader.xingHeaderOffset(MpegVersion.Version2, ChannelMode.JointStereo), 0x15); + assert.strictEqual(XingHeader.xingHeaderOffset(MpegVersion.Version2, ChannelMode.SingleChannel), 0x0D); + assert.strictEqual(XingHeader.xingHeaderOffset(MpegVersion.Version2, ChannelMode.Stereo), 0x15); + assert.strictEqual(XingHeader.xingHeaderOffset(MpegVersion.Version25, ChannelMode.DualChannel), 0x15); + assert.strictEqual(XingHeader.xingHeaderOffset(MpegVersion.Version25, ChannelMode.JointStereo), 0x15); + assert.strictEqual(XingHeader.xingHeaderOffset(MpegVersion.Version25, ChannelMode.SingleChannel), 0x0D); + assert.strictEqual(XingHeader.xingHeaderOffset(MpegVersion.Version25, ChannelMode.Stereo), 0x15); + } +} diff --git a/test-unit/utilities/testFile.ts b/test-unit/utilities/testFile.ts index 6ad76de5..ab337127 100644 --- a/test-unit/utilities/testFile.ts +++ b/test-unit/utilities/testFile.ts @@ -13,6 +13,12 @@ export default { }); mockFile.setup((f) => f.readBlock(TypeMoq.It.isAnyNumber())) .returns((s) => { + if (position + s > data.length) { + s = data.length - position; + } + if (s <= 0) { + return ByteVector.empty(); + } return data.mid(position, s); }); From 4cf7dbce7988e5bf4846a6fae4c7b2e3f9ed348f Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Mon, 14 Sep 2020 02:49:00 +0000 Subject: [PATCH 68/71] Fixing Dependency Stuff (#11) * fixing some depency stuff * Upgrade the node version for appveyor? :hammer: * :hammer: --- appveyor.yml | 2 +- package-lock.json | 1713 ++++++++++++++++++++++++++------------------- package.json | 6 +- 3 files changed, 984 insertions(+), 737 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index f71e596b..f9405b6d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ environment: - nodejs_version: "8.12" + nodejs_version: "8.16" install: - ps: Install-Product node $env:nodejs_version diff --git a/package-lock.json b/package-lock.json index 390d71b5..e99637c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,51 +5,38 @@ "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.0.0" + "@babel/highlight": "^7.10.4" } }, "@babel/core": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.7.tgz", - "integrity": "sha512-jlSjuj/7z138NLZALxVgrx13AOtqip42ATZP7+kYl53GvDV6+4dCek1mVUo8z8c8Xnw/mx2q3d9HWh3griuesQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.7", - "@babel/helpers": "^7.7.4", - "@babel/parser": "^7.7.7", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@babel/types": "^7.7.4", + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.6.tgz", + "integrity": "sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.6", + "@babel/helper-module-transforms": "^7.11.0", + "@babel/helpers": "^7.10.4", + "@babel/parser": "^7.11.5", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.11.5", + "@babel/types": "^7.11.5", "convert-source-map": "^1.7.0", "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.13", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", "resolve": "^1.3.2", "semver": "^5.4.1", "source-map": "^0.5.0" }, "dependencies": { - "@babel/parser": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", - "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", - "dev": true - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -59,14 +46,13 @@ } }, "@babel/generator": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.7.tgz", - "integrity": "sha512-/AOIBpHh/JU1l0ZFS4kiRCBnLi6OTHzh0RPk3h9isBxkkqELtQNFi1Vr/tiG9p1yfoUdKVwISuXWQR+hwwM4VQ==", + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", + "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", "dev": true, "requires": { - "@babel/types": "^7.7.4", + "@babel/types": "^7.11.5", "jsesc": "^2.5.1", - "lodash": "^4.17.13", "source-map": "^0.5.0" }, "dependencies": { @@ -79,53 +65,123 @@ } }, "@babel/helper-function-name": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz", - "integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.7.4", - "@babel/template": "^7.7.4", - "@babel/types": "^7.7.4" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz", - "integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", + "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-module-imports": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-module-transforms": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", + "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/template": "^7.10.4", + "@babel/types": "^7.11.0", + "lodash": "^4.17.19" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", + "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-replace-supers": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", + "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-simple-access": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", + "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", "dev": true, "requires": { - "@babel/types": "^7.7.4" + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz", - "integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.7.4" + "@babel/types": "^7.11.0" } }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, "@babel/helpers": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.7.4.tgz", - "integrity": "sha512-ak5NGZGJ6LV85Q1Zc9gn2n+ayXOizryhjSUBTdu5ih1tlVCJeuQENzc4ItyCVhINVXvIT/ZQ4mheGIsfBkpskg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", + "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", "dev": true, "requires": { - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", - "@babel/types": "^7.7.4" + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/highlight": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", - "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" }, "dependencies": { @@ -138,83 +194,59 @@ } }, "@babel/parser": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", - "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", + "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", "dev": true }, "@babel/template": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", - "integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4" - }, - "dependencies": { - "@babel/parser": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", - "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", - "dev": true - } + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/traverse": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz", - "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", + "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", "dev": true, "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.4", - "@babel/helper-function-name": "^7.7.4", - "@babel/helper-split-export-declaration": "^7.7.4", - "@babel/parser": "^7.7.4", - "@babel/types": "^7.7.4", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.5", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.5", + "@babel/types": "^7.11.5", "debug": "^4.1.0", "globals": "^11.1.0", - "lodash": "^4.17.13" - }, - "dependencies": { - "@babel/parser": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", - "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", - "dev": true - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } + "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz", - "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==", + "version": "7.11.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", + "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", "dev": true, "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, "@istanbuljs/load-nyc-config": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", - "integrity": "sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, "requires": { "camelcase": "^5.3.1", "find-up": "^4.1.0", + "get-package-type": "^0.1.0", "js-yaml": "^3.13.1", "resolve-from": "^5.0.0" }, @@ -236,9 +268,9 @@ } }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -255,9 +287,9 @@ } }, "p-limit": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", - "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -356,27 +388,55 @@ "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz", - "integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", + "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", "dev": true, "requires": { - "lodash.unescape": "4.0.1", - "semver": "5.5.0" + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" }, "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } } } }, "aggregate-error": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", - "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, "requires": { "clean-stack": "^2.0.0", @@ -413,6 +473,16 @@ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "app-module-path": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz", @@ -476,9 +546,9 @@ "dev": true }, "ast-module-types": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ast-module-types/-/ast-module-types-2.5.0.tgz", - "integrity": "sha512-dP6vhvatex3Q+OThhvcyGRvHn4noQBg1b8lCNKUAFL05up80hr2pAExveU3YQNDGMhfNPhQit/vzIkkvBPbSXw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/ast-module-types/-/ast-module-types-2.6.0.tgz", + "integrity": "sha512-zXSoVaMrf2R+r+ISid5/9a8SXm1LLdkhHzh6pSRhj9jklzruOOl1hva1YmFT33wAstg/f9ZndJAlq1BSrFLSGA==", "dev": true }, "asynckit": { @@ -545,6 +615,12 @@ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.42.tgz", "integrity": "sha512-3UQFKcRMx+5Z+IK5vYTMYK2jzLRJkt+XqyDdacgWgtMjjuifKpKTFneJLEgeBElOE2/lXZ1LcMcb5s8pwG2U8Q==" }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -555,6 +631,15 @@ "concat-map": "0.0.1" } }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -586,9 +671,9 @@ }, "dependencies": { "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "requires": { "semver": "^6.0.0" @@ -674,6 +759,22 @@ "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", "dev": true }, + "chokidar": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", + "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.2.0" + } + }, "circular-json": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", @@ -687,18 +788,18 @@ "dev": true }, "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, "requires": { - "restore-cursor": "^2.0.0" + "restore-cursor": "^3.1.0" } }, "cli-spinners": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.2.0.tgz", - "integrity": "sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.4.0.tgz", + "integrity": "sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA==", "dev": true }, "cliui": { @@ -822,9 +923,9 @@ } }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true } } @@ -856,9 +957,9 @@ "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" }, "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { "ms": "^2.1.1" @@ -870,6 +971,15 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, + "decomment": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/decomment/-/decomment-0.9.3.tgz", + "integrity": "sha512-5skH5BfUL3n09RDmMVaHS1QGCiZRnl2nArUwmsE9JRY93Ueh3tihYl5wIrDdAuXnoFhxVis/DmRWREO2c6DG3w==", + "dev": true, + "requires": { + "esprima": "4.0.1" + } + }, "deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -925,25 +1035,23 @@ "dev": true }, "dependency-tree": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/dependency-tree/-/dependency-tree-7.0.2.tgz", - "integrity": "sha512-yh3qCLHl/o/ZUPq14HUF6AGEbNTMyCAu92D6AmY3SEynJHkB25o3hTDzvt3Tu/KpR0093ATyrhr4aGwcx8NnVw==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/dependency-tree/-/dependency-tree-7.2.1.tgz", + "integrity": "sha512-nBxnjkqDW4LqAzBazy60V4lE0mAtIQ+oers/GIIvVvGYVdCD9+RNNd4G9jjstyz7ZFVg/j/OiYCvK5MjoVqA2w==", "dev": true, "requires": { "commander": "^2.19.0", "debug": "^4.1.1", - "filing-cabinet": "^2.3.1", - "precinct": "^6.1.1" + "filing-cabinet": "^2.5.1", + "precinct": "^6.2.0", + "typescript": "^3.7.5" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } + "typescript": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", + "dev": true } } }, @@ -970,9 +1078,9 @@ } }, "detective-es6": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detective-es6/-/detective-es6-2.1.0.tgz", - "integrity": "sha512-QSHqKGOp/YBIfmIqKXaXeq2rlL+bp3bcIQMfZ+0PvKzRlELSOSZxKRvpxVcxlLuocQv4QnOfuWGniGrmPbz8MQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/detective-es6/-/detective-es6-2.2.0.tgz", + "integrity": "sha512-fSpNY0SLER7/sVgQZ1NxJPwmc9uCTzNgdkQDhAaj8NPYwr7Qji9QBcmbNvtMCnuuOGMuKn3O7jv0An+/WRWJZQ==", "dev": true, "requires": { "node-source-walk": "^4.0.0" @@ -987,17 +1095,6 @@ "debug": "^4.0.0", "gonzales-pe": "^4.2.3", "node-source-walk": "^4.0.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } } }, "detective-postcss": { @@ -1010,17 +1107,6 @@ "is-url": "^1.2.4", "postcss": "^7.0.2", "postcss-values-parser": "^1.5.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } } }, "detective-sass": { @@ -1032,17 +1118,6 @@ "debug": "^4.1.1", "gonzales-pe": "^4.2.3", "node-source-walk": "^4.0.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } } }, "detective-scss": { @@ -1054,17 +1129,6 @@ "debug": "^4.1.1", "gonzales-pe": "^4.2.3", "node-source-walk": "^4.0.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } } }, "detective-stylus": { @@ -1074,20 +1138,21 @@ "dev": true }, "detective-typescript": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/detective-typescript/-/detective-typescript-5.5.0.tgz", - "integrity": "sha512-zVpte7i+EB+4wqrBiVdV6U2i5K+6c6/OwE2DombEp/DdI4b6ChlUy83UewiVdjw1iqJ4Wkw4r2CJVOXtgLmosg==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/detective-typescript/-/detective-typescript-5.8.0.tgz", + "integrity": "sha512-SrsUCfCaDTF64QVMHMidRal+kmkbIc5zP8cxxZPsomWx9vuEUjBlSJNhf7/ypE5cLdJJDI4qzKDmyzqQ+iz/xg==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "^1.9.0", + "@typescript-eslint/typescript-estree": "^2.29.0", + "ast-module-types": "^2.6.0", "node-source-walk": "^4.2.0", - "typescript": "^3.4.5" + "typescript": "^3.8.3" }, "dependencies": { "typescript": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", - "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", "dev": true } } @@ -1123,13 +1188,13 @@ } }, "enhanced-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", - "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", + "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", "dev": true, "requires": { "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", + "memory-fs": "^0.5.0", "tapable": "^1.0.0" } }, @@ -1143,23 +1208,28 @@ } }, "es-abstract": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", - "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "dev": true, "requires": { - "es-to-primitive": "^1.2.0", + "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "has": "^1.0.3", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-keys": "^1.0.12" + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" } }, "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, "requires": { "is-callable": "^1.1.4", @@ -1180,26 +1250,24 @@ "dev": true }, "escodegen": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.12.0.tgz", - "integrity": "sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==", + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", "dev": true, "requires": { - "esprima": "^3.1.3", + "esprima": "^4.0.1", "estraverse": "^4.2.0", "esutils": "^2.0.2", "optionator": "^0.8.1", "source-map": "~0.6.1" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - } } }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -1269,14 +1337,15 @@ "dev": true }, "filing-cabinet": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/filing-cabinet/-/filing-cabinet-2.3.3.tgz", - "integrity": "sha512-Lp9FNBm74UnZI/0tVcH8WlJZmnYf9/qImt1/VUaEj3rlBl+V7M5yVAzYPJ7X1T2WxQeCrSQN4jN64SlQa6Rbew==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/filing-cabinet/-/filing-cabinet-2.5.1.tgz", + "integrity": "sha512-GWOdObzou2L0HrJUk8MpJa01q0ZOwuTwTssM2+P+ABJWEGlVWd6ueEatANFdin94/3rdkVSdqpH14VqCNqp3RA==", "dev": true, "requires": { "app-module-path": "^2.2.0", "commander": "^2.13.0", "debug": "^4.1.1", + "decomment": "^0.9.2", "enhanced-resolve": "^4.1.0", "is-relative-path": "^1.0.2", "module-definition": "^3.0.0", @@ -1288,19 +1357,10 @@ "typescript": "^3.0.3" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, "resolve": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", - "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", "dev": true, "requires": { "path-parse": "^1.0.6" @@ -1308,6 +1368,15 @@ } } }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, "find": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/find/-/find-0.3.0.tgz", @@ -1318,20 +1387,20 @@ } }, "find-cache-dir": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.2.0.tgz", - "integrity": "sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", "dev": true, "requires": { "commondir": "^1.0.1", - "make-dir": "^3.0.0", + "make-dir": "^3.0.2", "pkg-dir": "^4.1.0" }, "dependencies": { "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "requires": { "semver": "^6.0.0" @@ -1364,9 +1433,9 @@ } }, "flatten": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", - "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz", + "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==", "dev": true }, "foreground-child": { @@ -1380,9 +1449,9 @@ }, "dependencies": { "cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -1440,9 +1509,9 @@ } }, "fromentries": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.0.tgz", - "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.1.tgz", + "integrity": "sha512-Xu2Qh8yqYuDhQGOhD5iJGninErSfI9A3FrriD3tjUgV5VbJFeH8vfgZ9HnC6jWN80QDVNQK5vmxRAmEAp7Mevw==", "dev": true }, "fs.realpath": { @@ -1451,12 +1520,25 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "dev": true + }, "get-amd-module-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-amd-module-type/-/get-amd-module-type-3.0.0.tgz", @@ -1480,9 +1562,15 @@ "dev": true }, "get-own-enumerable-property-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz", - "integrity": "sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, "get-stream": { @@ -1516,6 +1604,15 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -1523,32 +1620,32 @@ "dev": true }, "gonzales-pe": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.2.4.tgz", - "integrity": "sha512-v0Ts/8IsSbh9n1OJRnSfa7Nlxi4AkXIsWB6vPept8FDbL4bXn3FNuxjYtO/nmBGu7GDkL9MFeGebeSu6l55EPQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", + "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==", "dev": true, "requires": { - "minimist": "1.1.x" + "minimist": "^1.2.5" }, "dependencies": { "minimist": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz", - "integrity": "sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true } } }, "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "dev": true }, "graphviz": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/graphviz/-/graphviz-0.0.8.tgz", - "integrity": "sha1-5ZnkBzPvgOFlO/6JpfAx7PKqSqo=", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/graphviz/-/graphviz-0.0.9.tgz", + "integrity": "sha512-SmoY2pOtcikmMCqCSy2NO1YsRfu9OO0wpTlOYW++giGjfX1a6gax/m1Fo8IdUd0/3H15cTOfR1SMKwohj4LKsg==", "dev": true, "requires": { "temp": "~0.4.0" @@ -1601,15 +1698,15 @@ "dev": true }, "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "dev": true }, "hasha": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.1.0.tgz", - "integrity": "sha512-OFPDWmzPN1l7atOV1TgBVmNtBxaIysToK6Ve9DK+vT6pYuklw/nPNT+HJbZi0KDcI6vWB+9tgvZ5YD7fA3CXcA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", + "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", "dev": true, "requires": { "is-stream": "^2.0.0", @@ -1631,9 +1728,9 @@ "dev": true }, "html-escaper": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.0.tgz", - "integrity": "sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, "http-signature": { @@ -1700,22 +1797,37 @@ "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-3.0.0.tgz", "integrity": "sha512-JzF8q2BeZA1ZkE3XROwRpoMQ9ObMgTtp0JH8EXewlbkikuOj2GPLIpUipdO+VL8QsTr2teAJD02EFGGL5cO7uw==" }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", "dev": true }, "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.1.tgz", + "integrity": "sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg==", "dev": true }, "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, "is-fullwidth-code-point": { @@ -1724,6 +1836,27 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, "is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", @@ -1731,12 +1864,12 @@ "dev": true }, "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", "dev": true, "requires": { - "has": "^1.0.1" + "has-symbols": "^1.0.1" } }, "is-regexp": { @@ -1757,12 +1890,12 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", "dev": true, "requires": { - "has-symbols": "^1.0.0" + "has-symbols": "^1.0.1" } }, "is-typedarray": { @@ -1816,26 +1949,17 @@ } }, "istanbul-lib-instrument": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.0.tgz", - "integrity": "sha512-Nm4wVHdo7ZXSG30KjZ2Wl5SU/Bw7bDx1PdaiIFzEStdjs0H12mOTncn1GVYuqQSaZxpg87VGBRsVRPGD2cD1AQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "requires": { "@babel/core": "^7.7.5", - "@babel/parser": "^7.7.5", - "@babel/template": "^7.7.4", - "@babel/traverse": "^7.7.4", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.0.0", "semver": "^6.3.0" }, "dependencies": { - "@babel/parser": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", - "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", - "dev": true - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -1860,9 +1984,9 @@ }, "dependencies": { "cross-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", - "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -1871,9 +1995,9 @@ } }, "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "requires": { "semver": "^6.0.0" @@ -1941,9 +2065,9 @@ "dev": true }, "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "requires": { "semver": "^6.0.0" @@ -1956,9 +2080,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -1975,23 +2099,12 @@ "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } } }, "istanbul-reports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.0.tgz", - "integrity": "sha512-2osTcC8zcOSUkImzN2EWQta3Vdi4WjjKw99P2yWx5mLnigAM0Rd5uYFn1cf2i/Ois45GkNjaoTqc5CxgMSX80A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -2005,9 +2118,9 @@ "dev": true }, "js-yaml": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz", - "integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -2045,18 +2158,18 @@ "dev": true }, "json5": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", - "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", "dev": true, "requires": { - "minimist": "^1.2.0" + "minimist": "^1.2.5" }, "dependencies": { "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true } } @@ -2119,12 +2232,6 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, - "lodash.unescape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", - "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", - "dev": true - }, "log-driver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", @@ -2132,12 +2239,12 @@ "dev": true }, "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", "dev": true, "requires": { - "chalk": "^2.0.1" + "chalk": "^2.4.2" } }, "lru-cache": { @@ -2151,33 +2258,97 @@ } }, "madge": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/madge/-/madge-3.4.4.tgz", - "integrity": "sha512-ywk2Zca1Qn3FMH4btNcJN9q3z2+AZhJeUCzUMbUwSL/xmevCC4CzBQNF6i22V1SJ8cbXLKrXrJ6k0QQPtd9/KQ==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/madge/-/madge-3.9.2.tgz", + "integrity": "sha512-6ZvyKinAOOzcRpvpm1iyOuds+LvWIq3o3GmUYAHMJdIpDAgVY3mphxVzeWNo3agIOv0X0T/zbLycXQm9Rn19nA==", "dev": true, "requires": { - "chalk": "^2.4.1", - "commander": "^2.15.1", + "chalk": "^4.1.0", + "commander": "^5.1.0", "commondir": "^1.0.1", "debug": "^4.0.1", - "dependency-tree": "^7.0.2", - "graphviz": "^0.0.8", - "ora": "^3.0.0", - "pify": "^4.0.0", - "pluralize": "^7.0.0", - "pretty-ms": "^4.0.0", + "dependency-tree": "^7.2.1", + "detective-amd": "^3.0.0", + "detective-cjs": "^3.1.1", + "detective-es6": "^2.1.0", + "detective-less": "^1.0.2", + "detective-postcss": "^3.0.1", + "detective-sass": "^3.0.1", + "detective-scss": "^2.0.1", + "detective-stylus": "^1.0.0", + "detective-typescript": "^5.8.0", + "graphviz": "0.0.9", + "ora": "^4.0.4", + "pify": "^5.0.0", + "pluralize": "^8.0.0", + "precinct": "^6.3.1", + "pretty-ms": "^7.0.0", "rc": "^1.2.7", - "walkdir": "^0.0.12" + "typescript": "^3.9.5", + "walkdir": "^0.4.1" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", "dev": true, "requires": { - "ms": "^2.1.1" + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "typescript": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", + "dev": true } } }, @@ -2189,6 +2360,14 @@ "requires": { "pify": "^4.0.1", "semver": "^5.6.0" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + } } }, "make-error": { @@ -2216,9 +2395,9 @@ } }, "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", "dev": true, "requires": { "errno": "^0.1.3", @@ -2255,28 +2434,29 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "dev": true, "requires": { - "minimist": "0.0.8" + "minimist": "^1.2.5" } }, "mocha": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.2.tgz", - "integrity": "sha512-BgD2/RozoSC3uQK5R0isDcxjqaWw2n5HWdk8njYUyZf2NC79ErO5FtYVX52+rfqGoEgMfJf4fuG0IWh2TMzFoA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz", + "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==", "dev": true, "requires": { "ansi-colors": "3.2.3", "browser-stdout": "1.3.1", + "chokidar": "3.3.0", "debug": "3.2.6", "diff": "3.5.0", "escape-string-regexp": "1.0.5", @@ -2284,20 +2464,20 @@ "glob": "7.1.3", "growl": "1.10.5", "he": "1.2.0", - "js-yaml": "3.13.0", - "log-symbols": "2.2.0", + "js-yaml": "3.13.1", + "log-symbols": "3.0.0", "minimatch": "3.0.4", - "mkdirp": "0.5.1", + "mkdirp": "0.5.5", "ms": "2.1.1", - "node-environment-flags": "1.0.4", + "node-environment-flags": "1.0.6", "object.assign": "4.1.0", "strip-json-comments": "2.0.1", "supports-color": "6.0.0", "which": "1.3.1", "wide-align": "1.1.3", - "yargs": "13.2.2", - "yargs-parser": "13.0.0", - "yargs-unparser": "1.5.0" + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.0" }, "dependencies": { "ansi-regex": { @@ -2306,25 +2486,39 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "dev": true, "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" } }, "find-up": { @@ -2342,28 +2536,14 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "dev": true, "requires": { - "invert-kv": "^2.0.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" } }, "locate-path": { @@ -2376,38 +2556,16 @@ "path-exists": "^3.0.0" } }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -2463,6 +2621,17 @@ "has-flag": "^3.0.0" } }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", @@ -2470,28 +2639,27 @@ "dev": true }, "yargs": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", - "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "dev": true, "requires": { - "cliui": "^4.0.0", + "cliui": "^5.0.0", "find-up": "^3.0.0", "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" + "yargs-parser": "^13.1.2" } }, "yargs-parser": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", - "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -2513,12 +2681,12 @@ } }, "module-definition": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-3.2.0.tgz", - "integrity": "sha512-PO6o0BajpdRR+fb3FUSeDISgJpnyxg8UDUEalR8LPQajl0M5+m4jHWhgrMGGSEl6D9+sVl/l1fjOCvpBXIQ+2Q==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-3.3.0.tgz", + "integrity": "sha512-HTplA9xwDzH67XJFC1YvZMUElWJD28DV0dUq7lhTs+JKJamUOWA/CcYWSlhW5amJO66uWtY7XdltT+LfX0wIVg==", "dev": true, "requires": { - "ast-module-types": "^2.4.0", + "ast-module-types": "^2.6.0", "node-source-walk": "^4.0.0" } }, @@ -2534,23 +2702,18 @@ "find": "^0.3.0", "requirejs": "^2.3.5", "requirejs-config-file": "^3.1.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, "nice-try": { @@ -2559,12 +2722,21 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "node-environment-flags": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.4.tgz", - "integrity": "sha512-M9rwCnWVLW7PX+NUWe3ejEdiLYinRpsEre9hMkU/6NS4h+EEulYaDH1gCEZ2gyXsmw+RXYDaV2JkkTNcsPDJ0Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", + "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", "dev": true, "requires": { - "object.getownpropertydescriptors": "^2.0.3" + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "node-preload": { @@ -2585,6 +2757,12 @@ "@babel/parser": "^7.0.0" } }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -2600,9 +2778,9 @@ "dev": true }, "nyc": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.0.0.tgz", - "integrity": "sha512-qcLBlNCKMDVuKb7d1fpxjPR8sHeMVX0CHarXAVzrVWoFrigCkYR8xcrjfXSPi5HXM7EU78L6ywO7w1c5rZNCNg==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", "dev": true, "requires": { "@istanbuljs/load-nyc-config": "^1.0.0", @@ -2613,6 +2791,7 @@ "find-cache-dir": "^3.2.0", "find-up": "^4.1.0", "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", "glob": "^7.1.6", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-hook": "^3.0.0", @@ -2620,10 +2799,9 @@ "istanbul-lib-processinfo": "^2.0.2", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.0", - "js-yaml": "^3.13.1", + "istanbul-reports": "^3.0.2", "make-dir": "^3.0.0", - "node-preload": "^0.2.0", + "node-preload": "^0.2.1", "p-map": "^3.0.0", "process-on-spawn": "^1.0.0", "resolve-from": "^5.0.0", @@ -2631,7 +2809,6 @@ "signal-exit": "^3.0.2", "spawn-wrap": "^2.0.0", "test-exclude": "^6.0.0", - "uuid": "^3.3.3", "yargs": "^15.0.2" }, "dependencies": { @@ -2642,9 +2819,9 @@ "dev": true }, "ansi-styles": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.0.tgz", - "integrity": "sha512-7kFQgnEaMdRtwf6uSfUnVr9gSGC7faurn+J/Mv90/W+iTtN0405/nLdopfMWwchyxhbGYl6TC4Sccn9TUkGAgg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", "dev": true, "requires": { "@types/color-name": "^1.1.1", @@ -2725,16 +2902,6 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -2745,18 +2912,18 @@ } }, "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "requires": { "semver": "^6.0.0" } }, "p-limit": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", - "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -2815,12 +2982,6 @@ "ansi-regex": "^5.0.0" } }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -2839,9 +3000,9 @@ "dev": true }, "yargs": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.0.2.tgz", - "integrity": "sha512-GH/X/hYt+x5hOat4LMnCqMd8r5Cv78heOMIJn1hr7QPPBqfeC6p89Y78+WB9yGDvfpCvgasfmWLzNzEioOUD9Q==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "dev": true, "requires": { "cliui": "^6.0.0", @@ -2854,13 +3015,13 @@ "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^16.1.0" + "yargs-parser": "^18.1.2" } }, "yargs-parser": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-16.1.0.tgz", - "integrity": "sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -2875,6 +3036,12 @@ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "dev": true }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -2894,13 +3061,13 @@ } }, "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" } }, "once": { @@ -2912,63 +3079,107 @@ } }, "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "requires": { - "mimic-fn": "^1.0.0" - }, - "dependencies": { - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - } + "mimic-fn": "^2.1.0" } }, "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "dev": true, "requires": { "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", + "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", - "wordwrap": "~1.0.0" + "word-wrap": "~1.2.3" } }, "ora": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", - "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-4.1.1.tgz", + "integrity": "sha512-sjYP8QyVWBpBZWD6Vr1M/KwknSw6kJOz41tvGMlwWeClHBtYKTbHMki1PsLZnxKpXMPbTKv9b3pjQu3REib96A==", "dev": true, "requires": { - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-spinners": "^2.0.0", - "log-symbols": "^2.2.0", - "strip-ansi": "^5.2.0", + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.2.0", + "is-interactive": "^1.0.0", + "log-symbols": "^3.0.0", + "mute-stream": "0.0.8", + "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" } } } @@ -3084,10 +3295,16 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", "dev": true }, "pkg-dir": { @@ -3119,9 +3336,9 @@ } }, "p-limit": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", - "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -3151,15 +3368,15 @@ } }, "pluralize": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", - "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "dev": true }, "postcss": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", - "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "version": "7.0.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz", + "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -3196,34 +3413,31 @@ "dev": true }, "precinct": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/precinct/-/precinct-6.1.2.tgz", - "integrity": "sha512-Mk+oWvR7N2D2EY+5vKNnnXPGor1aU3ZbkcHp2ER68el5PL1nmZsvpq41s69emiNMSuL6TMoIeTabvwfe5w7vNg==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/precinct/-/precinct-6.3.1.tgz", + "integrity": "sha512-JAwyLCgTylWminoD7V0VJwMElWmwrVSR6r9HaPWCoswkB4iFzX7aNtO7VBfAVPy+NhmjKb8IF8UmlWJXzUkOIQ==", "dev": true, "requires": { - "commander": "^2.19.0", + "commander": "^2.20.3", "debug": "^4.1.1", "detective-amd": "^3.0.0", "detective-cjs": "^3.1.1", - "detective-es6": "^2.0.0", + "detective-es6": "^2.1.0", "detective-less": "^1.0.2", - "detective-postcss": "^3.0.0", - "detective-sass": "^3.0.0", - "detective-scss": "^2.0.0", + "detective-postcss": "^3.0.1", + "detective-sass": "^3.0.1", + "detective-scss": "^2.0.1", "detective-stylus": "^1.0.0", - "detective-typescript": "^5.1.1", - "module-definition": "^3.1.0", + "detective-typescript": "^5.8.0", + "module-definition": "^3.3.0", "node-source-walk": "^4.2.0" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true } } }, @@ -3234,12 +3448,12 @@ "dev": true }, "pretty-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-4.0.0.tgz", - "integrity": "sha512-qG66ahoLCwpLXD09ZPHSCbUWYTqdosB7SMP4OffgTgL2PBKXMuUsrk5Bwg8q4qPkjTXsKBMr+YK3Ltd/6F9s/Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.0.tgz", + "integrity": "sha512-J3aPWiC5e9ZeZFuSeBraGxSkGMOvulSWsxDByOcbD1Pr75YL3LSNIKIb52WXbCLE1sS5s4inBBbryjF4Y05Ceg==", "dev": true, "requires": { - "parse-ms": "^2.0.0" + "parse-ms": "^2.1.0" } }, "process-nextick-args": { @@ -3309,17 +3523,17 @@ }, "dependencies": { "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true } } }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -3331,6 +3545,15 @@ "util-deprecate": "~1.0.1" } }, + "readdirp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", + "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", + "dev": true, + "requires": { + "picomatch": "^2.0.4" + } + }, "release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -3427,19 +3650,19 @@ "dev": true }, "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, "requires": { - "onetime": "^2.0.0", + "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "rimraf": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", - "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -3525,9 +3748,9 @@ }, "dependencies": { "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "requires": { "semver": "^6.0.0" @@ -3606,6 +3829,26 @@ } } }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -3660,17 +3903,6 @@ "requires": { "commander": "^2.8.1", "debug": "^4.1.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } } }, "supports-color": { @@ -3724,6 +3956,15 @@ "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", @@ -3906,9 +4147,9 @@ } }, "walkdir": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.12.tgz", - "integrity": "sha512-HFhaD4mMWPzFSqhpyDG48KDdrjfn409YQuVW7ckZYhW4sE87mYtWifdB/+73RA7+p4s4K18n5Jfx1kHthE1gBw==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", + "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", "dev": true }, "wcwidth": { @@ -3943,10 +4184,10 @@ "string-width": "^1.0.2 || 2" } }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, "wrap-ansi": { @@ -3987,9 +4228,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.1.tgz", - "integrity": "sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", "dev": true, "requires": { "imurmurhash": "^0.1.4", @@ -4115,35 +4356,46 @@ } }, "yargs-unparser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", - "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", "dev": true, "requires": { "flat": "^4.1.0", - "lodash": "^4.17.11", - "yargs": "^12.0.5" + "lodash": "^4.17.15", + "yargs": "^13.3.0" }, "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "dev": true, "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" } }, "find-up": { @@ -4155,30 +4407,12 @@ "locate-path": "^3.0.0" } }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -4189,38 +4423,10 @@ "path-exists": "^3.0.0" } }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -4241,30 +4447,71 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "dev": true, "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", + "cliui": "^5.0.0", "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", + "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", + "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^2.0.0", + "string-width": "^3.0.0", "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" } }, "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "requires": { "camelcase": "^5.0.0", diff --git a/package.json b/package.json index 13d7e5e2..157994d7 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,10 @@ "chai": "^4.2.0", "chai-as-promised": "^7.1.1", "coveralls": "^3.0.9", - "madge": "^3.4.4", - "mocha": "^6.1.2", + "madge": "^3.9.2", + "mocha": "^7.0.0-esm1", "mocha-typescript": "^1.1.17", - "nyc": "^15.0.0", + "nyc": "^15.1.0", "stream-buffers": "^3.0.2", "source-map-support": "^0.5.16", "ts-node": "^8.5.4", From 001bd81c3354491a8d414b218f38b9012dce87be Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 15 Sep 2020 17:51:10 -0400 Subject: [PATCH 69/71] Updating to allow for publishing to NPM --- .npmignore | 13 +++++++++++++ README.md | 8 ++++---- package-lock.json | 2 +- package.json | 4 +++- tsconfig.json | 1 + 5 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 .npmignore diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..91341e2a --- /dev/null +++ b/.npmignore @@ -0,0 +1,13 @@ +## Override the .gitignore to make sure dist + +# Ignore test files +test-* + +# Ignore idea stuff +.idea + +# Ignore NYC coverage output files +.nyc_output/** + +# Output of 'npm pack' +*.tgz \ No newline at end of file diff --git a/README.md b/README.md index 7eec50f4..8ee3d10c 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ library for Node.js. This project is a mostly wholesale translation of the origi Note: A port of TagLib already exists for Node.js. Despite TagLib being the origin of TabLib#, it is substantially lacking in the variety of media formats that can be handled. TagLib# greatly -improved on the original TagLib, hence why this project exists +improved on the original TagLib, hence why this project exists. -## Supported Tagging Formats -* ID3v1 -* ID3v2 \ No newline at end of file +## Supported Tagging Formats (and File Formats) +* ID3v1: `MP1`, `MP2`, `MP3`, `M2A` +* ID3v2: `MP1`, `MP2`, `MP3`, `M2A` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e99637c1..216cd6e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "node-taglib-sharp", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 157994d7..6050795f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,9 @@ { "name": "node-taglib-sharp", - "version": "0.1.0", + "version": "1.0.0", "license": "LGPL-2.1-or-later", + "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { "build": "tsc -p ./", "madge": "node node_modules/madge/bin/cli.js --warning --circular --extensions ts ./", diff --git a/tsconfig.json b/tsconfig.json index ed747338..916009ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "declaration": true, "experimentalDecorators": true, "inlineSources": true, "module": "commonjs", From b0d9bad3607a01e9b8c6e3a9ac4ca67fcec343ca Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 15 Sep 2020 22:06:50 -0400 Subject: [PATCH 70/71] Update to pack package as part of appveyor build --- appveyor.yml | 3 ++- package.json | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index f9405b6d..f796eb01 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,9 +7,10 @@ install: build_script: - npm run build + - npm pack test_script: - npm run test-unit-with-coverage after_test: - - npm run publish-coverage \ No newline at end of file + - npm run publish-coverage diff --git a/package.json b/package.json index 6050795f..3d473e3a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,13 @@ { "name": "node-taglib-sharp", + "description": "Read and write audio/video/picture tags using a similar interface to TagLib#", "version": "1.0.0", "license": "LGPL-2.1-or-later", + + "author": "Ben Russell (https://github.com/benrr101)", + "repository": "github:benrr101/node-taglib-sharp", + "bugs": "https://github.com/benrr101/node-taglib-sharp/issues", + "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { From a15852fa5cba99c91aece925be99af6f2c0f4d6e Mon Sep 17 00:00:00 2001 From: Benjamin Russell Date: Tue, 15 Sep 2020 22:12:59 -0400 Subject: [PATCH 71/71] Appveyor, can you please capture the artifacts? --- appveyor.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index f796eb01..698719ce 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,3 +14,7 @@ test_script: after_test: - npm run publish-coverage + +artifacts: + - path: '*.tgz' + name: NPM Package

Xm{rDHRQL(I6k+Rshv(qu}q z@$JVcWvXL-s*nuNUwQYEDt>U13jb_pzW$Jc135y>feA%hmhgR8T`u&SalGT=U~J`S zvFfERV_bL_BPI2veZq0lTwMmNoOy6sW&Njy7Kvr~ynpt8tHNYmS`C&Aw4gbN5omTG zi+%R1R-=-_k!wBJfIj>CKMI(O$w^>VJZ@Q8M^@t`cak^M$=cZ3%6&xZ`F=2|Fsqbb zG&SXptKw7I?kg1|row)FVU%7v`zhEoD~=H&q;0-6%VDnu2P@Jv$K=2oZB`kWH(RrM zvm3c`g&8IY()1}PVHsN}qJCIYE%UF(A0}{7n<2sx!2+7p*(#anCgoGfU8&Vj-@0Q1 zZ`P!J4g{e&K!;OE!!wGCT1ab$+oJ{Ip;e(QWoD75Q(fRfsfJPi1Rc#5=a;)x?Z@+$ zbuUCI=l(~ji<**LQQ}|76(f<0wfLvrl@#lT%;ih~R@vfU3ff-fMw@Lkkl?enEtQK3 zLSYM;z|j+h*q=;^NfDsPPoBL6kONCiFd32Xt_ReFSU2Y#E5ESD%;JJEft{VTqKmfA z?3jj%l15E8(l&5+Z@=8lzFUZhZPBj!{D+y}Lwk%-2a}n4MFeaVNR!GYW;DVRH9c~U zo|^TOS!QA@IeN~#sF%?!{Y4qghreX=CxM3p{+JP{rOLi8o$)g3!a-2km3NgDzENH4_vOGa%#I@cssT+L0L+6barR*=9w5PAr(k#m{bmQ2XLG#TlV`r~* zS_NL^tNM0#>G_OV8PpK;CfmpwHCUONCrvc=h7E1(YxHf8Cxdfw^MO$P52z{1-=KmB zeik-5YYNw8vxA2XC)M4PkGgfXE-YF@=^V<{6)3^MCu)_V$-uB=%{%gpQs~6TaS}T~ zy$WS0`?87!Q2U#MYk-)e9ovnk0L4Q%`~4kk|;G&c^|8+QL^*R8V3@kR~9ElcFb5G z`H9GQV5|5dcccTbQkL|kts-rdWUUX_VxafiTEqLbzRE97GK+?kcR8B;l@qXJZD@@I zJlJB3A=GZkR$1xE7)c>N-SpXK-+Mzx)eYE8j#=as&!RbQjofWsV7|4Il6F?3etvfC zu+=X+R342GWUcj|T<-M&5;-69M~~z@0oUNsBhi56KYAp+ioCl>kHiP1Vn#zQO9GSA z(sb-jCImwX^K3)sF89|swlZJhW!*g)vMqxuu8PhM>r^Ix6v9qCMmLfSE;&VfPaYVt zdmWThX|T?W1}kHU+>N-|@Ag}PjD~kVQ_X5W8de(4Ers=mD&&FcX0|x=>k98;YvQjE zYJRTky7gxL@Z2E>YF$=?gc6tyhnl0Kk*Po8{y%Iu7{g|CTJH(z93o+>dfZ=~K!TiQ z-~nEWG2gPnFk2tK z6vBRy5NO~roDt|_PQvfb-=5#OLb;5>0SpRHP#N38UJQi12%nV>zI7Z*L&tV${*GP( zp{D*7*MzX9VG%q~h_#=7 z;Z>sJK4&-6I%g|M@_*M@9|P-pB@(#^%YSUS#G|Z73ykyGc~;J=f9S3gp_QPd$YWJq5NwZ^CF?!ineg>28~4tW#g*m zjQ~Wz>fY>U@ARLWha z|CT)lCTl?HiI#I}6^f^=wfRtFH$9I__zmoIjU^{v2?E5-7Q2VK+NHwr2tQ{Q3$Z2+XEY0f~ zlQo%ZUmlpcVh!#6`bHJPv6_(2RR~Hf(h5SEnZA1(v0&{`DkwnV#0O7XNoOYA33@Ts zN1t?sY~;co)OY`&sUm9jeaWuW*z*ZW51AeDO*tr?QA0j)os~uUf+5&uAH$)GetEDk z++fK8-KC$PB*lqJ4yIOB-C?O2LtycWtmAe$O6Yc$Y^w;8(G~Hb8Fyll!$H`yD)&~` zDU<1$S(o5+r<+(nZzxfBvCbIG z!^qHU)VE@mSaE2+%U_50|0TwWo;9?x7{tMW?6Bpus7W&jc8K$4L?LkBC^Rq@`F^LS z4qKHoq0!7=<_l;wRB^cL%U$@xz0aE2jw|$}6gomt(C&d9T_8p^5KWXs(FdsRx*Vkp zzoMHiE89Kci@x7JrO!@ux+G9|)BWzV8kA?j=?U7(kjp!O4j5*tIDoIv1+ObSL4RlK zsJ6jXfnaFS5iu<&Ry7|Pv%KWdj48elip-iGyo{lsh))WEI-RN4-J{q8x6P&UNL&iZ>eUIQGtgKlDC)GdUM( z$6S}V48QyJ2ct1b`3l)X(rWO=CkWCgmliNIm}@|PQ!JjFQ&eZ&%5f4r>M!b4C#qfg z!MKaYeo4z*%0GYBwd36sYg@wg6{9ERdt-|X<&cX7 zo$1T~m&jCRmImhF;h1qhSE)#tdotIMP3YX2fvxpG<4(%?u|s?>1z*-zO(hF2EwP`5 zLL^gKL1-(UWxpbcl^}LD;{kRHMdDe!H%p_em=10ydu-*6iz{dg1_swv6EdNBkS1mx zKNXUVnR3|x zHW&!`dXNb3$;tEvTg%FnMCf*A?g{r3F)3&x+B}7`n(V(EX;io}D)#B;ElQEbE^PY_ z1PqX--wCNapVg%^I>U)=5aXSuc?L55I82TkiDOr^582J}hcxYsTeR+yW~5Nrdt(TY z4ce0?t^M?y=K8GaaAxtoMA4ID9~GgK3W?eG@|FgyF4sapY`#B;D?N&h6fwwqFco+ zr94_#n*5+2T90My^GviSNl#QD#*8@^3D#*jOR-*mHX8vUPO9I_Hos0tHN5&nV1P;f zbRv+=OB)73;O<1UfLd!ATjTgF+$3T^V229pDA}=Dhue(KTXN(L@1H zskcBLvY?oHV7XvC#$jN`6=nE@V=b)m3FL0hq9z%j^Ft^QXdXl^7rOb`xt}K za0X%tbQv<7WlY1fNE! zUnZZf+gMtbGOYpj3zJE9im`IvJSvqV_g=gklTS3@y7PUN6ojMu44REn0f!Wia*u9! z@3k`gd66@*96?!?HePJDn=mZ*UfbdYM>3&CInBrX0h4EuazOINTh0Gcxlnv0a=I2X zb~Rk4(MNvleBrpnW-+3;6v@4g+%|t%LyrxM>Ofm$fSuFM!t9^RP1W&j#&C~x@WROFsYFk9JEVWEKla07hfLUFeCj>fcUWZ%OhFL>^_ z+v%36lMH&Q%3(654S?mUX6FRoqaK!55*ezg#SDc)B!*Y(D&4oWj;h2ODE2*nTx=a0 z>b<5>CH-75;I0uSU0(mfD&6@07ki3lT5-6Ojo079KlC{14w7#9!+?VS+@Te6J*r&6 z91^*B<0(mG!7FL3eebd0rJaNfthA$w4M-~(Bt%QGbcS9=Cw;c*{BwJrkxb}ZYyIHFA9v@XNPi5zcuF9 zA3pqPc&iC>_&PtW7w9@zQ}+5zL8y&QKOiHDU?{4yFI_!DZ+BciB_Sg7__O|#`EN4q=EN!;Q&Up|{r$hwv6cB}uR zt?f0|OiIUdRiN$4Lo}i9Gu|&{XGIoSE^l2q_J+;UE68<9iN?L#Z!&P`!^8Q=m5S^K z3r$J0n<2`E%(tK3qd@8s41&-%x6$JwG{y<#*?E8_S+BsAh;M6A#L;gtZe!I9M5L;! z!9h!;sx1p;MHGBVou!Ps*V^Tf7XD9)2DwL?m0jr*a}j@EdUD(iDijS^tu;tn+71-F z?_}}Bd9n4w(xZ{g@$HLj4`8qK9?;G5zQnMuwx|!`U9D{3sqGThd-B1}@P%!^QbE3( z+K0dIpOl9B&0{k?2ZJ1i(@cYf?2=F~XO?7DoUe}*ay0ii=gs|QHc7ShW$=fqJX;Wa zgd;U^Ys>=T4G5vq`sQ;m7`D2IpRO>D!sq)$da*g9K)&T<=D|NmNYBWPnoFb zOEVfR_KsExF##_n(UO^t>DIzL$zC{g8JN#iRflps_wUMJV(5(RR3AHlb_yw-8?oRkMs z<;UMn*Hv;L4gVi=Z`l?F*mVsnA`QaODKT_+BMn1$gER~&APv%;Gjw-%ODjEecPP>+ zAqWV(qt|gg@BJLlAGrU)`C%PEyk6;7r1VBGH@zBs`DXxwsC#J(Np?hBYbJayyh`nL=gzTxUvPRxB z)?WPdn~l{R#TPBr!S-^s41bST=s;SIiL{N+(29@r9WTPwjo6jO0C~*7c=1C83q;3Q zwKB_qh!km<$LKBQ+s4QuwS;nD285O;pYmpcE{;M3)!feeeM`4bbyA!;qA_67vQm6X z>P?#Xp5P7?3RROngx;Q!MR%|jH5X?ZvSC@roctE)-jvIbvTda~DD52ndZoar%Aj?V zp(pVj1|{~>h^lwW=OK;MDqPkg4b>orja!Yx*}i23d+PTUH_GU{i&Z+>nmUle_f7nG zK8;exNs9fI_SIheUlJ*=sTcRFFT2Yh3iVB9Td-nGuJYy@!fIJdFy0rIK5fok7oyUn zQirl;yt8{~WO!21s^;wdj#S_su+Z3)dAAdI7=oEfu8#I;Ld-*nkl@WLOEGYjM^AlI zbzxln)YI&!Gxw=|Qkaxs#CrVLl->J%l|0+eY0vUZ7&&&S;u5Fae|itb4sFcCQW1lY z3kcdoZBkC4R}-zum{}@h}@-XiCNq; z3NQNDQ%y?t(i9L`iO8Y;C{ptVeAQ_Bx!#ME>Pp$LgbHWSIZ z*p7#Zug0^`#|N2#-g}8!W;_>Q)JtTmnshb{Y?{7iMJhGwZR0N`d({c(hTRAgIy6x$ zz-NI`Le>m7H*o5wh(Z)lIJ7+!9i=~gMTT$1hDWPAQ;Ptk>6Gbsx=YWWGs#=9pLefY zka_U3aa4a#pFRrBVsDPvyR6oQY4HtavUB0F@bit{D6MJDsEP19#HB1(ed8h8cMSm_ zLj%8Qo&GM}FkQlr7Jy0zhBPjY6C$DDd^ey?EXQ$S`=)WFBNtY)8Dw3BGjIY8$hJaH z9dB=M9(yM>g9z*g8TGvmH5+#*%yMYTWHOmxLI+l~Df8W2CS^_TlT2StJu**Ee0;HD z#d5`xG!Vvl1&-~STM|gIM-Ct5pK;H5eqgZz)a&fBljjB7Pqeet{s81H_jX8%e_-p6Z3@ZZQqoQ~nTN?Sm=;s|8GC{B5iV`b~cuD<#EZXPHFMX_{-VH=4)E zna>`$B~?NAHgK(TZDn*!GR6$ZfPFNlj}4dbW1V2rI=Qbu_ZXAKwdz^Wd6@8bw^2u^ zmxCLUaCqDxcD+v)3R^{NzZaQKsXm%JqYwZ@UZ6;`IHV^AexBXIz>Vzy9_D3RMcd7W z9qLmpJGbxgHbi4+XzxCaGQD}3MTGDye$%ocgz?6qGv#4Kv(3hBiCek4QGKfmX=8J9 zQpW(#5S2<3;yrJ04sPP>xNHD$0)0e!e2n zc_j86YS0Cv7vXw|*F(O&kfgG(90+|qltyWqZ8bx$^ydeQrcN@u0t~C)e~PmvR?Qq* z$9|)(=7q8Rh43CheS$_?0TC;ctiR=z+6a%%G+_-jKA^UgZ#B%PRx)e#s7+TSKwEDx zD`141rW)4|j4;mktJZ7O@PkNOOVy-VY~?w;;5= z_2m0kVpp!0=yt&{DkM-$R63uEis|^897|{gfh>qBIcdWAnp+SNGOJ+#KHdDSNV4$s*sel6Xih5KBj1h~DS8#*SFvQF_WaPATAlWYrmwNi(SE{F6;YG(9g?Mc zuyY)xe)wj~6tSWca_UWs_e4}(iv3)2h{v@J<8aH^KrX0v%~QAyB~(a3QCYNe3)RB+d$)Kyvi>oA ze*J5g3;S!AgPKo)_7pxqmGloqU@8FCEO?beAC+mc{dh&?{SvjnO8h0@qWdcLtf=px z7^g8*6D+j@CDi3jVwN5x+D-9xVG(Lh=YLJ;xz!NAT@E!X`PQ;PixC2qnya+wL9MZ! zRV3+s7*{xgkerswbq&v1O)8_&3Q1KI0j6Uki(UkCUg{PzsEq<*N`d003jPr7Oo0Kt zLdVn6GYQXk@5p{;adh1_RL9+z_`SjiEb$eWwAEL8A>HNd_d~e+X9pvqLjm5Z+8r=# zVZw5BhErD1%EU+!tA-BqtIWpjiN)tiU^>`~)g2^7xs!N(lyM$xZ@>c`s(ZQubQH_N z7r@RruUFiXB%@Rkk8B5;X{Z^#P{#VD9HQZIg%%#dw`Ku(mfF_woCFM6zTnP8qGv8qC#eQ2(}pD#!`LOL*$2wwBw^SBvy9#*xC-sWZUxuu@wD++ z_(ak`RqAU!bSXS|&tdYhMvcXw;5L%u8GI z1lIK;BO^J&5+1xU^mZ1>BIFRa!aP)|zTwa5$C50gtr-G8UWbK_TAh8IZGSNUpI6oY zi4RNsaKuGesKYlOJ|v#lvGTyM0H8Q{kz?)yah7vj(RvZ69M|$~OB3Lv%HV2n1d1is z8UQM|)G0gon@UzS*2bU+4n7+8X=&{e@vzn|q|;YdISPF1B^a^CB2GWh+ABo$g`IVM z37~bqKJgpQhdP#(Fz4MtV|In@xzyeWwqvuoqeZT(#X_*#^9KwAA@q{!DUF#ctWR!c zDGRG3M(cMDmkWk8kepgXA=|g==SR~lIZH;41CcB=EYWnuW-t61jhbmSEDI$*I@>5V zm$an;5tpxvm)O}|L&%dHy)aji_l1%j+ZBk37ri!y@ZB_g8a*~^Z&+#HAN_G^t3X*( zRnUERMN2s9v1pl3;pR$dbcB!dxqz6O%1t7&?IpEJXc4m&Z=>O#y?*AZmc;h<%az^A zRyi7BBb1NRVntgE`9u(&v|~!~pX>52TZ`ws8NgvIgI1#$K^fd2aG1I+QKp?*Y5!<* z3Sk4T^Zj$-$2!x#Izq~`iXQ;+os{Y@V?#ig1leiT`e(VlBbODHU#XXl?Z$J6UeUne z%mY3;%3V|d?#ulFHpXa5MumJRi7-$oUVX)KAB&Mnd&H4oP|!W6&G#+O$8e0mNeqcv z_45NHpfc_K2(}16fXwq(=Y%zgvp=;aje1gD`kR57mH`r7W-?>_Weu&&iS*i2??Ddp zGDp?mh;XIXK%yY6J(aG`K!KW^u-z>i&Rs{ghoY!eSC*6R$FCIBQJi}nm{E~}lG34H zyf4tB%_Gq?%7NkWFs!0v4}HP@XnAc{Qke!7V;jfK;R$l85K8V4Z2M&2d$`!8F8-gd z+^`oMa{dbL_ijotx0TB@m4aY$E#+>2I}^{qfxh*G$5$vV$$%@z}zv<2(R0e>`XclCpZ00i7kY{>kyXd zHfn(*P-3PRMBRONM-oHfI^ALh(MVc9_g-&IpVfNk8n4d6YQ1?Ekale1UKH~j>*K`I zW-H3(D2?~EkQOR7TE9pLi1iT0ls`sfsZv*$*-w(Fn%hsOT(de>;YV6|#QeQ^?FG#- zO)Qpwxj)i6b~YJ9n1*E~9V;|)$zXEu>Qf!RL%u_Wev-gEMjr7i_@yvH9x5r0jbPYIHITOr}`5j%e*i zsEAqL2E>NYz56pUg)#)b1fTrK^WW!x#}cWxxtC-`6RRT>h1#0wR9>2YXPLJ+r=+MA zVS?%z=q;>5p8R|1@&Dm{|KA+VzZ}U+c{t>fZPhN|OfWPK4ZdvS1Yit;Cz7!C$#5`y zxT5|1f*T|OV5&mE7t4s_ei#aO5?S|MYqGEWlEg{Ca%J%AV6RVxw#FYCw?F-8dEF^l zeUnhJU1P|ym8E=$9+Nr`!JGGOlvs-h^BG^Oi8Vj)&E>@_EXYWSx;hCO7rq8|WvbFv(G2GqB*HMkijblE=TW1pQ0Msy8T4C7qf3v8n*vvF#d!Ng+&^U3| zyqWm9-4TSNEY}Gn|Lmn(J<+i|#1_&1LtTN1I72T_Y<-N<^iUL<3PFQ4HccluUroUHe zO@~6GC3B`Ll+*`UBA3!8<{g07(K4?tWOf;5xw(7H42!hpJiV?<8a2Vh3umI$7uwyc zuGHSLXIyzIS#=D(^^&EMp6Uu*L6JKo(rU4t95~0|0KHA%!FdZ zK~8VkWjW;EL#ypFMDld>)GWA3JtgRS_tGWls|i}6WW!-%f>_xPlJ+dQn)jMO$=1Px8 z*-x-55_9G?V)M-fksx?9Y~zSNtF{K(Hg)D+8hqojVihdiFe!_}mz+u_>a93mpEz4< z)nx&wQ6n*kJLeUWcRk7fCf2`@i^qjSPQ?=L&{X;vHJ%A~Xy)Q#X2DkqY5L5XY~U+} zUat#}T{fn%7Y1o9K4kItnDV`AES#Hp%pGpp&5=%F7hHHg`V?a}R^3DpwM5r`9*5r4Yc?THcX%bKbA|k~~%ae)$4;KBgaE+50XeR}E=`n3`cvaVcHAl(;^J&AY(m zr_mTyxf@@KsZ@7>R@c7nbHzPt1Y6^{0liV?eh-h}Pt=kjr6nFNq{QP1@hoF+y4b-I z%q<~ylB+GMm}guI-!`k)V@ge&Oqp>_FNuj_uptbz^tk28-)=Ic6{nm6Ds*N87W-2D zMQ<^wL*Kqp|GAp;=_fG^gsmjOGr{m8OlD%7C#qDlKWs#>QxXR5Q;xe;c_(D%CZ<4s z(!sXyzMZJeb}s4at-$9(p>lfVFHpQn!BukWt}b8H336GDl@8qCEPR^eohs_WQx*;S zr47Cd^9gJoHJ&kKrDh4*Wgz_rh-Snj3zg(LzU6m~!zq;;vh3t#3555fOtQpYF66tX zb%Om?Ka{>G8NEaCSer(Tq{}FIfnDC-9uDCd_4?3`%x|mDDgbKeIUL?sVzYrsb*IU! zR^8GNoRYYHve+lYlhZ~#{0xvxkrS4xUP?_iMb12BEK8w_PI46e>*G8R1C>`#Jbp-?K`$;kJ!W6^YVx9kfz zbb-@RV1y1B7|d8kMI^C$+4{Y=(kiIw>s3?>iJ62B01^os8EKB7Ql`7X zEcTZiSOM>_6?7NIQ>QpFgb_!Y%8nX_6(9=-Ql7;bPE%QNsq2pSWC3b}0 zL<>Y~e=fwcrV7Gy5KCkx*W?cL>xJjZ;;YLUK?>9}^1BCq`zwjZJ~1Ev&`J4`mIk=I znMYAaQ8i-tCYf#yBD(a#DRUKwCG~nt+IrpH&Z^8-$~aZ*3LfoOoO<=`x%)?v-k`7z{9bPPwChba63E-lU71vxk(+r*6xLj;@(HQ`P(K1Hvnj25q3b1f| z)cT+$b}XZOJEw+LUQ{bTT|Pc-vh62UN+}zF|LdYk;bLqo%G=itks*}67ViU<5+^X$ zTLfjUolF+9>l5recym3qG))t4e^!uZ*Q{he=x4y@#Z2`lYiZa?B~(3RA6?c%S|$li z_7x-a1U4JxwL%cpK&{LXmR39e7HrQSWw`@Q9&l}7+ixZ#AQEB{V0!CbSo(1IFH-Km z5jbB9LjP8|HWWDIybR#9oaN61bOQ{y_yEu@gL{`)`xKht-X+@4_=81Y_YoAYaX9H) zJh#p4VJuzZ>^{G6!AssUWtbgw4dPk%8y8+YEQZ}jH^=h9uubT)t@F)BNG#Jsgv62@ z^5oZaiV|1eTzRI>bl$We)UL30?5@W&(3I!6fF0r+Fw`z;7?R|MQwhgz%-nQb*4sXb zl3Ra1QtTHrT*_m^!o?$ExP7gF&7_7__nh~?>^T^81+3TkQC?SCVV+mYEJ*y-+HSFf z)dZ?b+Vtje8Whh=;yV#Ylr_uCK)rLNz(fo-~SQk*M+@#o6K|e8|}pbr56GyldQ; zhsji3#UFdvreKb9!0)@>C8_t%re?gT&LxP7!e5WGeOd6@sIqf^g*K^fDhgKlv9pOw!I2wjY)z8f^x9mKzw<5C0;525 z%Jg0J9kq@RjiCfE+6omQ{yRtvdnoUs+X-&Gm}5))s7oN`fW!a37ooP0*{(5Qi?|W+ z?zfGfaedya(_ZXozjAjfR8g(w<2aEH1CUy(RSer%JyL?bIXKy??PB8I}43S@G6mc>b1u=ca(iRj~)S}k{n=v}t z%F@=f(@JuyjGGW>Z0cH>_fFLLQ^~0E==hoG>p?Z$yi1WozWBws-cs%s_vg_cnccbu zNBa{C9Z`Z}^~?PY06cj|h-4XgMcPRg5wdr)!Bu%KAKlm~OOP(d4ZJi>#8_z5*4W2!5Q9QU00BEgVObO5Mg%r^7#V?pI(aX41eR9-RuFz1PtmNm} z9-X|hQQwm=yeuyf{?+=e3c2tv7Yx@P{y~ne4GuYNOSlhR>O*vOG~9>ogv*!)Uz|?p zv*Ln>Q*-{t60~y+xWukrs0f!Q2_o-%RhDU+CK%TsmpYKkM{qf&B^arXo7^#ALuBSe z@TV1bey}iOe^zVvb4j){ffR$Sr6Q(V9sXxoJ$c2qh)tCXOw~oBApISv?Pc<}Z=}c` zfLBuMB(E8M?x;x-Rbjei!YtI!zm+!lgwm^2q$zj%8XlN_puq=w`t8fRjA`0GCx%(q zC~=9;qnu%ri_A&4k2^+-v;r^c+foe*#wACxOBQJa`q4kM?bhd(vf@pNLCaVNS$=pm z5=%)@>)Gdc)%0=oN$UNk&C#jQwxZBbm;xrrC6^(;SQf5NB%$~=%_tW^9ZWQ+XP>>O zNRklnnm1;iO2}QQ!|`}^OX^h+QI%rnzK{ero%}0G1RZ8*&zRNafvaH`FG_^93!72CQ0!)>)x`)b-!p0+z6>iNIC1Ak4=rOb(Mv{X{kIYmy<=3uuY)Z09L-l_{KTy~7^v9( z00w1sGW$0Il&5KQyn;*9F^o4sHPo~8s)-T3A9EZ%#(P>aqWDdZEcb+w&wE?f*i)-s zMW86sIt(7TbTRrzkC1y}_@;sfL z7%3!ilOmAZJK=Cuj-=?o(Mtg`y5Cz8FxdCW) zms}%kUE546LS0lR10c`)#N{3MxtTDZo=MT|9Y1Cfr@keAwrtoead3|Y|b?J&to4(r?iADhmgDY^&cSP1Hp_AB; z2{PZB15`%@8uUxido~A zcXXb?FTybIOz>+N^!AtF>Kgf}zSLJ!i!YK4S;{rb6aqmJSKMcKBwwRQX1jx@ki^*K z9U^-{(Od^utJ;`8wuz0AG@qk1yQadaMazR7xA z4}Ev!*3v4x+87c1WjHYDKeXIG$O$LHtDLKi8VK&tj8{5>>s`4T65gUL@OTJ$cKQ-{ zJOtJKv0Y%$k1X-Qvgo@%8F8$Xo3EBmKK;%$y$=Hv#x^sTI7QoaN{2yAbO^*DO=YN|a<)4YedzA@6oQhm zUnZnFA=+cn!}fYqZ4}#lKWH#k_bBBkho%nF4V>NR4aq({2&QZ$qI~LD&8PI*o8|;> z_XN;cD9ZB({(5-ZL!T~IAYER>igsvNm{~hRWvQDImI!n>+^@EXlvocNKYcsa%4|B9 zf<(68IBE7;@oe4>Ga(J}EDe1@!q?~A^(qgZeYZmq)zc2mw?cejv6~e{(jRr{U6-^n zd%C%f7j4g;M6YQ^+W=}1?oQ=|^gK;kdF~*XfvH}+$mnj#Ibq)o-(CFBt`S_vti4-( zw%ao69qRw>Y=(E>>)^e5aj-GT>0I>+Pp_Six9{S!l;RQz$L{CnAZ*f-VxeEtkSe;d{t|BX7ubpO$G277W62h1g zB9!8Df5_+IfK)tkgc2H`dj`k2MS5;dGJ7V@)SG6u$hpnwD5m02`X+@JJ|s+-*k2$+