From f23c0644c58a72f9113da2d2816637842aa41e81 Mon Sep 17 00:00:00 2001 From: Ragnar-Oock Date: Sun, 20 Jul 2025 15:32:58 +0200 Subject: [PATCH 1/3] refactor: throw DicomParserError on parsing error instead of object BREAKING CHANGE: the exception property is replaced by the native cause property, the dataSet property is still available. --- src/parseDicom.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/parseDicom.js b/src/parseDicom.js index df0ebfee..c265ca5f 100644 --- a/src/parseDicom.js +++ b/src/parseDicom.js @@ -7,6 +7,7 @@ import readPart10Header from './readPart10Header.js'; import sharedCopy from './sharedCopy.js'; import * as byteArrayParser from './byteArrayParser.js'; import * as parseDicomDataSet from './parseDicomDataSet.js'; +import {DicomParserError} from "./util/errors"; // LEE (Little Endian Explicit) is the transfer syntax used in dimse operations when there is a split // between the header and data. @@ -26,8 +27,7 @@ const BEI = '1.2.840.10008.1.2.2'; * @param byteArray the byte array * @param options object to control parsing behavior (optional) * @returns {DataSet} - * @throws error if an error occurs while parsing. The exception object will contain a - * property dataSet with the elements successfully parsed before the error. + * @throws {DicomParserError} an error occurs while parsing, check the `clause` property to get the original error. */ export default function parseDicom(byteArray, options = {}) { @@ -143,13 +143,8 @@ export default function parseDicom(byteArray, options = {}) { } else { parseDicomDataSet.parseDicomDataSetImplicit(dataSet, dataSetByteStream, dataSetByteStream.byteArray.length, options); } - } catch (e) { - const ex = { - exception: e, - dataSet - }; - - throw ex; + } catch (error) { + throw new DicomParserError(dataSet, error); } return dataSet; From 1ebe0bf040418be9d2baf0dbc186d4204597d040 Mon Sep 17 00:00:00 2001 From: Ragnar-Oock Date: Sun, 20 Jul 2025 15:35:09 +0200 Subject: [PATCH 2/3] chore(deps): update caniuse-lite --- package-lock.json | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 202ab896..14736231 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3830,14 +3830,25 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001305", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001305.tgz", - "integrity": "sha512-p7d9YQMji8haf0f+5rbcv9WlQ+N5jMPfRAnUmZRlNxsNeBO3Yr7RYG6M2uTY1h9tCVdlkJg6YNNc4kiAiBLdWA==", + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" }, "node_modules/caseless": { "version": "0.12.0", @@ -18299,9 +18310,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001305", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001305.tgz", - "integrity": "sha512-p7d9YQMji8haf0f+5rbcv9WlQ+N5jMPfRAnUmZRlNxsNeBO3Yr7RYG6M2uTY1h9tCVdlkJg6YNNc4kiAiBLdWA==", + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", "dev": true }, "caseless": { From c7e17e46f5d9d8a5c99cfa61d46ec01d55886eb3 Mon Sep 17 00:00:00 2001 From: Ragnar-Oock Date: Sun, 20 Jul 2025 16:04:59 +0200 Subject: [PATCH 3/3] feat: throw errors instead of strings --- src/alloc.js | 4 +- src/bigEndianByteArrayParser.js | 50 +++++++------- src/byteArrayParser.js | 8 ++- src/byteStream.js | 65 ++++++++++--------- src/findAndSetUNElementLength.js | 5 +- src/findEndOfEncapsulatedPixelData.js | 8 ++- src/findItemDelimitationItem.js | 4 +- src/littleEndianByteArrayParser.js | 51 ++++++++------- src/parseDicom.js | 28 ++++---- src/parseDicomDataSet.js | 32 ++++++--- src/readDicomElementExplicit.js | 6 +- src/readDicomElementImplicit.js | 3 +- src/readEncapsulatedImageFrame.js | 50 +++++++------- src/readEncapsulatedPixelData.js | 45 +++++++------ src/readEncapsulatedPixelDataFromFragments.js | 61 ++++++++--------- src/readPart10Header.js | 19 ++++-- src/readSequenceElementExplicit.js | 5 +- src/readSequenceElementImplicit.js | 5 +- src/readSequenceItem.js | 7 +- src/readTag.js | 4 +- src/sharedCopy.js | 4 +- src/util/createJPEGBasicOffsetTable.js | 35 +++++----- src/util/dataSetToJS.js | 3 +- src/util/elementToString.js | 16 +++-- src/util/errors.js | 32 +++++++++ src/util/parseDA.js | 13 ++-- src/util/parseTM.js | 14 ++-- src/util/util.js | 12 ++-- 28 files changed, 352 insertions(+), 237 deletions(-) create mode 100644 src/util/errors.js diff --git a/src/alloc.js b/src/alloc.js index 62a58634..b90470e9 100644 --- a/src/alloc.js +++ b/src/alloc.js @@ -1,3 +1,5 @@ +import { invalidParameter } from './util/errors.js'; + /** * Creates a new byteArray of the same type (Uint8Array or Buffer) of the specified length. * @param byteArray the underlying byteArray (either Uint8Array or Buffer) @@ -10,5 +12,5 @@ export default function alloc (byteArray, length) { } else if (byteArray instanceof Uint8Array) { return new Uint8Array(length); } - throw 'dicomParser.alloc: unknown type for byteArray'; + throw new TypeError(invalidParameter('byteArray', 'should be of type Uint8Array or Buffer')); } diff --git a/src/bigEndianByteArrayParser.js b/src/bigEndianByteArrayParser.js index a18853f0..108add1c 100644 --- a/src/bigEndianByteArrayParser.js +++ b/src/bigEndianByteArrayParser.js @@ -1,3 +1,5 @@ +import { overflowErrorMessage, underflowErrorMessage } from './util/errors.js'; + /** * Internal helper functions for parsing different types from a big-endian byte array */ @@ -9,16 +11,16 @@ export default { * * @param byteArray the byte array to read from * @param position the position in the byte array to read from - * @returns {*} the parsed unsigned int 16 - * @throws error if buffer overread would occur + * @returns {number} the parsed unsigned int 16 + * @throws {RangeError} tried to read outside the buffer * @access private */ readUint16 (byteArray, position) { if (position < 0) { - throw 'bigEndianByteArrayParser.readUint16: position cannot be less than 0'; + throw new RangeError(underflowErrorMessage); } if (position + 2 > byteArray.length) { - throw 'bigEndianByteArrayParser.readUint16: attempt to read past end of buffer'; + throw new RangeError(overflowErrorMessage); } return (byteArray[position] << 8) + byteArray[position + 1]; @@ -30,16 +32,16 @@ export default { * * @param byteArray the byte array to read from * @param position the position in the byte array to read from - * @returns {*} the parsed signed int 16 - * @throws error if buffer overread would occur + * @returns {number} the parsed signed int 16 + * @throws {RangeError} tried to read outside the buffer * @access private */ readInt16 (byteArray, position) { if (position < 0) { - throw 'bigEndianByteArrayParser.readInt16: position cannot be less than 0'; + throw new RangeError(underflowErrorMessage); } if (position + 2 > byteArray.length) { - throw 'bigEndianByteArrayParser.readInt16: attempt to read past end of buffer'; + throw new RangeError(overflowErrorMessage); } var int16 = (byteArray[position] << 8) + byteArray[position + 1]; // fix sign @@ -56,17 +58,17 @@ export default { * * @param byteArray the byte array to read from * @param position the position in the byte array to read from - * @returns {*} the parsed unsigned int 32 - * @throws error if buffer overread would occur + * @returns {number} the parsed unsigned int 32 + * @throws {RangeError} tried to read outside the buffer * @access private */ readUint32 (byteArray, position) { if (position < 0) { - throw 'bigEndianByteArrayParser.readUint32: position cannot be less than 0'; + throw new RangeError(underflowErrorMessage); } if (position + 4 > byteArray.length) { - throw 'bigEndianByteArrayParser.readUint32: attempt to read past end of buffer'; + throw new RangeError(overflowErrorMessage); } var uint32 = (256 * (256 * (256 * byteArray[position] + @@ -82,17 +84,17 @@ export default { * * @param byteArray the byte array to read from * @param position the position in the byte array to read from - * @returns {*} the parsed signed int 32 - * @throws error if buffer overread would occur + * @returns {number} the parsed signed int 32 + * @throws {RangeError} tried to read outside the buffer * @access private */ readInt32 (byteArray, position) { if (position < 0) { - throw 'bigEndianByteArrayParser.readInt32: position cannot be less than 0'; + throw new RangeError(underflowErrorMessage); } if (position + 4 > byteArray.length) { - throw 'bigEndianByteArrayParser.readInt32: attempt to read past end of buffer'; + throw new RangeError(overflowErrorMessage); } var int32 = ((byteArray[position] << 24) + @@ -108,17 +110,17 @@ export default { * * @param byteArray the byte array to read from * @param position the position in the byte array to read from - * @returns {*} the parsed 32-bit float - * @throws error if buffer overread would occur + * @returns {number} the parsed 32-bit float + * @throws {RangeError} tried to read outside the buffer * @access private */ readFloat (byteArray, position) { if (position < 0) { - throw 'bigEndianByteArrayParser.readFloat: position cannot be less than 0'; + throw new RangeError(underflowErrorMessage); } if (position + 4 > byteArray.length) { - throw 'bigEndianByteArrayParser.readFloat: attempt to read past end of buffer'; + throw new RangeError(overflowErrorMessage); } // I am sure there is a better way than this but this should be safe @@ -139,17 +141,17 @@ export default { * * @param byteArray the byte array to read from * @param position the position in the byte array to read from - * @returns {*} the parsed 64-bit float - * @throws error if buffer overread would occur + * @returns {number} the parsed 64-bit float + * @throws {RangeError} tried to read outside the buffer * @access private */ readDouble (byteArray, position) { if (position < 0) { - throw 'bigEndianByteArrayParser.readDouble: position cannot be less than 0'; + throw new RangeError(underflowErrorMessage); } if (position + 8 > byteArray.length) { - throw 'bigEndianByteArrayParser.readDouble: attempt to read past end of buffer'; + throw new RangeError(overflowErrorMessage); } // I am sure there is a better way than this but this should be safe diff --git a/src/byteArrayParser.js b/src/byteArrayParser.js index 2dd225dd..4bdbeba3 100644 --- a/src/byteArrayParser.js +++ b/src/byteArrayParser.js @@ -2,6 +2,8 @@ * Internal helper functions common to parsing byte arrays of any type */ +import { overflowErrorMessage, underflowErrorMessage } from './util/errors.js'; + /** * Reads a string of 8-bit characters from an array of bytes and advances * the position by length bytes. A null terminator will end the string @@ -11,16 +13,16 @@ * @param position the position in the byte array to read from * @param length the maximum number of bytes to parse * @returns {string} the parsed string - * @throws error if buffer overread would occur + * @throws {RangeError} tried to read outside the buffer * @access private */ export function readFixedString (byteArray, position, length) { if (length < 0) { - throw 'dicomParser.readFixedString - length cannot be less than 0'; + throw new RangeError(underflowErrorMessage); } if (position + length > byteArray.length) { - throw 'dicomParser.readFixedString: attempt to read past end of buffer'; + throw new RangeError(overflowErrorMessage); } var result = ''; diff --git a/src/byteStream.js b/src/byteStream.js index 9b7a0b6b..99664276 100644 --- a/src/byteStream.js +++ b/src/byteStream.js @@ -1,5 +1,6 @@ import sharedCopy from './sharedCopy.js'; import { readFixedString } from './byteArrayParser.js'; +import { invalidParameter, missingParameter } from './util/errors.js'; /** * @@ -11,63 +12,67 @@ import { readFixedString } from './byteArrayParser.js'; * * */ -/** - * Constructor for ByteStream objects. - * @param byteArrayParser a parser for parsing the byte array - * @param byteArray a Uint8Array containing the byte stream - * @param position (optional) the position to start reading from. 0 if not specified - * @constructor - * @throws will throw an error if the byteArrayParser parameter is not present - * @throws will throw an error if the byteArray parameter is not present or invalid - * @throws will throw an error if the position parameter is not inside the byte array - */ export default class ByteStream { - constructor (byteArrayParser, byteArray, position) { + /** + * Constructor for ByteStream objects. + * @param byteArrayParser a parser for parsing the byte array + * @param {Uint8Array | Buffer} byteArray a Uint8Array containing the byte stream + * @param {number} position (optional) the position to start reading from. 0 if not specified + * @constructor + * @throws {TypeError} missing or invalid `byteArrayParser` or `byteArray` parameter + * @throws {RangeError} incorrect start `position` parameter + */ + constructor (byteArrayParser, byteArray, position = 0) { if (byteArrayParser === undefined) { - throw 'dicomParser.ByteStream: missing required parameter \'byteArrayParser\''; + throw new TypeError(missingParameter('byteArrayParser')); } if (byteArray === undefined) { - throw 'dicomParser.ByteStream: missing required parameter \'byteArray\''; + throw new TypeError(missingParameter('byteArray')); } if ((byteArray instanceof Uint8Array) === false && ((typeof Buffer === 'undefined') || (byteArray instanceof Buffer) === false)) { - throw 'dicomParser.ByteStream: parameter byteArray is not of type Uint8Array or Buffer'; + throw new TypeError(invalidParameter('byteArray', 'should be of type Uint8Array or Buffer')); } if (position < 0) { - throw 'dicomParser.ByteStream: parameter \'position\' cannot be less than 0'; + throw new RangeError(`ByteStream start position should be a positive integer or 0, got ${position.toString(10)}`); } if (position >= byteArray.length) { - throw 'dicomParser.ByteStream: parameter \'position\' cannot be greater than or equal to \'byteArray\' length'; + throw new RangeError('ByteStream start position should be a positive integer strictly smaller than byteArray' + + `length (${byteArray.length}), got ${position.toString(10)}`); } this.byteArrayParser = byteArrayParser; this.byteArray = byteArray; - this.position = position ? position : 0; + + this.position = position; this.warnings = []; // array of string warnings encountered while parsing } /** - * Safely seeks through the byte stream. Will throw an exception if an attempt - * is made to seek outside of the byte array. - * @param offset the number of bytes to add to the position - * @throws error if seek would cause position to be outside of the byteArray + * Safely seeks through the byte stream. + * @param {number} offset the number of bytes to add to the position + * @throws {RangeError} seek would cause position to be outside the byteArray */ seek (offset) { - if (this.position + offset < 0) { - throw 'dicomParser.ByteStream.prototype.seek: cannot seek to position < 0'; + const newPosition = this.position + offset; + + if (newPosition < 0) { + throw new RangeError( + `ByteStream cannot seek outside the byteArray tried to read position ${newPosition.toString(10)}` + ); } - this.position += offset; + this.position = newPosition; } /** * Returns a new ByteStream object from the current position and of the requested number of bytes * @param numBytes the length of the byte array for the ByteStream to contain * @returns {dicomParser.ByteStream} - * @throws error if buffer overread would occur + * @throws {RangeError} error if buffer overflow would occur */ readByteStream (numBytes) { if (this.position + numBytes > this.byteArray.length) { - throw 'dicomParser.ByteStream.prototype.readByteStream: readByteStream - buffer overread'; + throw new RangeError('Attempted to read stream past buffer end'); } var byteArrayView = sharedCopy(this.byteArray, this.position, numBytes); @@ -76,7 +81,7 @@ export default class ByteStream { return new ByteStream(this.byteArrayParser, byteArrayView); } - getSize() { + getSize () { return this.byteArray.length; } @@ -86,7 +91,7 @@ export default class ByteStream { * the position by 2 bytes * * @returns {*} the parsed unsigned int 16 - * @throws error if buffer overread would occur + * @throws {RangeError} tried to read outside the buffer */ readUint16 () { var result = this.byteArrayParser.readUint16(this.byteArray, this.position); @@ -101,7 +106,7 @@ export default class ByteStream { * the position by 2 bytes * * @returns {*} the parse unsigned int 32 - * @throws error if buffer overread would occur + * @throws {RangeError} tried to read outside the buffer */ readUint32 () { var result = this.byteArrayParser.readUint32(this.byteArray, this.position); @@ -117,7 +122,7 @@ export default class ByteStream { * but will not effect advancement of the position. * @param length the maximum number of bytes to parse * @returns {string} the parsed string - * @throws error if buffer overread would occur + * @throws {RangeError} tried to read outside the buffer */ readFixedString (length) { var result = readFixedString(this.byteArray, this.position, length); diff --git a/src/findAndSetUNElementLength.js b/src/findAndSetUNElementLength.js index 8d915ecd..7cf78e43 100644 --- a/src/findAndSetUNElementLength.js +++ b/src/findAndSetUNElementLength.js @@ -2,15 +2,18 @@ * Internal helper functions for parsing DICOM elements */ +import { missingParameter } from './util/errors.js'; + /** * reads from the byte stream until it finds the magic number for the Sequence Delimitation * Item item and then sets the length of the element * @param byteStream * @param element + * @throws {TypeError} missing required parameter `byteStream` */ export default function findAndSetUNElementLength (byteStream, element) { if (byteStream === undefined) { - throw 'dicomParser.findAndSetUNElementLength: missing required parameter \'byteStream\''; + throw new TypeError(missingParameter('byteStream')); } // group, element, length diff --git a/src/findEndOfEncapsulatedPixelData.js b/src/findEndOfEncapsulatedPixelData.js index caeec743..fd674860 100644 --- a/src/findEndOfEncapsulatedPixelData.js +++ b/src/findEndOfEncapsulatedPixelData.js @@ -1,4 +1,5 @@ import readTag from './readTag.js'; +import { missingParameter } from './util/errors.js'; /** * Internal helper functions for parsing DICOM elements @@ -13,11 +14,11 @@ import readTag from './readTag.js'; */ export default function findEndOfEncapsulatedElement (byteStream, element, warnings) { if (byteStream === undefined) { - throw 'dicomParser.findEndOfEncapsulatedElement: missing required parameter \'byteStream\''; + throw new TypeError(missingParameter('byteStream')); } if (element === undefined) { - throw 'dicomParser.findEndOfEncapsulatedElement: missing required parameter \'element\''; + throw new TypeError(missingParameter('element')); } element.encapsulatedPixelData = true; @@ -27,7 +28,7 @@ export default function findEndOfEncapsulatedElement (byteStream, element, warni const basicOffsetTableItemTag = readTag(byteStream); if (basicOffsetTableItemTag !== 'xfffee000') { - throw 'dicomParser.findEndOfEncapsulatedElement: basic offset table not found'; + throw new Error('Failed to find end of encapsulated element, basic offset table not found in byteStream'); } const basicOffsetTableItemlength = byteStream.readUint32(); @@ -45,6 +46,7 @@ export default function findEndOfEncapsulatedElement (byteStream, element, warni while (byteStream.position < byteStream.byteArray.length) { const tag = readTag(byteStream); + let length = byteStream.readUint32(); if (tag === 'xfffee0dd') { diff --git a/src/findItemDelimitationItem.js b/src/findItemDelimitationItem.js index 514ffb26..15f796d3 100644 --- a/src/findItemDelimitationItem.js +++ b/src/findItemDelimitationItem.js @@ -2,6 +2,8 @@ * Internal helper functions for parsing DICOM elements */ +import { missingParameter } from './util/errors.js'; + /** * reads from the byte stream until it finds the magic numbers for the item delimitation item * and then sets the length of the element @@ -10,7 +12,7 @@ */ export default function findItemDelimitationItemAndSetElementLength (byteStream, element) { if (byteStream === undefined) { - throw 'dicomParser.readDicomElementImplicit: missing required parameter \'byteStream\''; + throw new TypeError(missingParameter('byteStream')); } const itemDelimitationItemLength = 8; // group, element, length diff --git a/src/littleEndianByteArrayParser.js b/src/littleEndianByteArrayParser.js index 08608e49..26d4bc7e 100644 --- a/src/littleEndianByteArrayParser.js +++ b/src/littleEndianByteArrayParser.js @@ -1,6 +1,7 @@ /** * Internal helper functions for parsing different types from a little-endian byte array */ +import { overflowErrorMessage, underflowErrorMessage } from './util/errors.js'; export default { @@ -9,18 +10,18 @@ export default { * Parses an unsigned int 16 from a little-endian byte array * * @param byteArray the byte array to read from - * @param position the position in the byte array to read from - * @returns {*} the parsed unsigned int 16 - * @throws error if buffer overread would occur + * @param {number} position the position in the byte array to read from + * @returns {number} the parsed unsigned int 16 + * @throws {RangeError} tried to read outside of buffer * @access private */ readUint16 (byteArray, position) { if (position < 0) { - throw 'littleEndianByteArrayParser.readUint16: position cannot be less than 0'; + throw new RangeError(underflowErrorMessage); } if (position + 2 > byteArray.length) { - throw 'littleEndianByteArrayParser.readUint16: attempt to read past end of buffer'; + throw new RangeError(overflowErrorMessage); } return byteArray[position] + (byteArray[position + 1] * 256); @@ -32,16 +33,16 @@ export default { * * @param byteArray the byte array to read from * @param position the position in the byte array to read from - * @returns {*} the parsed signed int 16 - * @throws error if buffer overread would occur + * @returns {number} the parsed signed int 16 + * @throws {RangeError} tried to read outside of buffer * @access private */ readInt16 (byteArray, position) { if (position < 0) { - throw 'littleEndianByteArrayParser.readInt16: position cannot be less than 0'; + throw new RangeError(underflowErrorMessage); } if (position + 2 > byteArray.length) { - throw 'littleEndianByteArrayParser.readInt16: attempt to read past end of buffer'; + throw new RangeError(overflowErrorMessage); } let int16 = byteArray[position] + (byteArray[position + 1] << 8); @@ -60,17 +61,17 @@ export default { * * @param byteArray the byte array to read from * @param position the position in the byte array to read from - * @returns {*} the parsed unsigned int 32 - * @throws error if buffer overread would occur + * @returns {number} the parsed unsigned int 32 + * @throws {RangeError} tried to read outside of buffer * @access private */ readUint32 (byteArray, position) { if (position < 0) { - throw 'littleEndianByteArrayParser.readUint32: position cannot be less than 0'; + throw new RangeError(underflowErrorMessage); } if (position + 4 > byteArray.length) { - throw 'littleEndianByteArrayParser.readUint32: attempt to read past end of buffer'; + throw new RangeError(overflowErrorMessage); } return (byteArray[position] + @@ -84,17 +85,17 @@ export default { * * @param byteArray the byte array to read from * @param position the position in the byte array to read from - * @returns {*} the parsed unsigned int 32 - * @throws error if buffer overread would occur + * @returns {number} the parsed unsigned int 32 + * @throws {RangeError} tried to read outside of buffer * @access private */ readInt32 (byteArray, position) { if (position < 0) { - throw 'littleEndianByteArrayParser.readInt32: position cannot be less than 0'; + throw new RangeError(underflowErrorMessage); } if (position + 4 > byteArray.length) { - throw 'littleEndianByteArrayParser.readInt32: attempt to read past end of buffer'; + throw new RangeError(overflowErrorMessage); } return (byteArray[position] + @@ -108,17 +109,17 @@ export default { * * @param byteArray the byte array to read from * @param position the position in the byte array to read from - * @returns {*} the parsed 32-bit float - * @throws error if buffer overread would occur + * @returns {number} the parsed 32-bit float + * @throws {RangeError} tried to read outside of buffer * @access private */ readFloat (byteArray, position) { if (position < 0) { - throw 'littleEndianByteArrayParser.readFloat: position cannot be less than 0'; + throw new RangeError(underflowErrorMessage); } if (position + 4 > byteArray.length) { - throw 'littleEndianByteArrayParser.readFloat: attempt to read past end of buffer'; + throw new RangeError(overflowErrorMessage); } // I am sure there is a better way than this but this should be safe @@ -139,17 +140,17 @@ export default { * * @param byteArray the byte array to read from * @param position the position in the byte array to read from - * @returns {*} the parsed 64-bit float - * @throws error if buffer overread would occur + * @returns {number} the parsed 64-bit float + * @throws {RangeError} tried to read outside of buffer * @access private */ readDouble (byteArray, position) { if (position < 0) { - throw 'littleEndianByteArrayParser.readDouble: position cannot be less than 0'; + throw new RangeError(underflowErrorMessage); } if (position + 8 > byteArray.length) { - throw 'littleEndianByteArrayParser.readDouble: attempt to read past end of buffer'; + throw new RangeError(overflowErrorMessage); } // I am sure there is a better way than this but this should be safe diff --git a/src/parseDicom.js b/src/parseDicom.js index c265ca5f..b26ba2d9 100644 --- a/src/parseDicom.js +++ b/src/parseDicom.js @@ -7,7 +7,7 @@ import readPart10Header from './readPart10Header.js'; import sharedCopy from './sharedCopy.js'; import * as byteArrayParser from './byteArrayParser.js'; import * as parseDicomDataSet from './parseDicomDataSet.js'; -import {DicomParserError} from "./util/errors"; +import { DicomParserError, missingParameter } from './util/errors.js'; // LEE (Little Endian Explicit) is the transfer syntax used in dimse operations when there is a split // between the header and data. @@ -30,23 +30,25 @@ const BEI = '1.2.840.10008.1.2.2'; * @throws {DicomParserError} an error occurs while parsing, check the `clause` property to get the original error. */ -export default function parseDicom(byteArray, options = {}) { +export default function parseDicom (byteArray, options = {}) { if (byteArray === undefined) { - throw new Error('dicomParser.parseDicom: missing required parameter \'byteArray\''); + throw new TypeError(missingParameter('byteArray')); } - + const readTransferSyntax = (metaHeaderDataSet) => { if (metaHeaderDataSet.elements.x00020010 === undefined) { - throw new Error('dicomParser.parseDicom: missing required meta header attribute 0002,0010'); + throw new Error('Missing required meta header attribute (0002,0010)'); } const transferSyntaxElement = metaHeaderDataSet.elements.x00020010; + + return transferSyntaxElement && transferSyntaxElement.Value || byteArrayParser.readFixedString(byteArray, transferSyntaxElement.dataOffset, transferSyntaxElement.length); - } + }; - function isExplicit(transferSyntax) { + function isExplicit (transferSyntax) { // implicit little endian if (transferSyntax === '1.2.840.10008.1.2') { return false; @@ -56,7 +58,7 @@ export default function parseDicom(byteArray, options = {}) { return true; } - function getDataSetByteStream(transferSyntax, position) { + function getDataSetByteStream (transferSyntax, position) { // Detect whether we are inside a browser or Node.js const isNode = (Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]'); @@ -100,7 +102,7 @@ export default function parseDicom(byteArray, options = {}) { } // throw exception since no inflater is available - throw 'dicomParser.parseDicom: no inflater available to handle deflate transfer syntax'; + throw new Error('Failed to parse deflated dataset, no inflater available. Try providing one via options.inflater.'); } // explicit big endian @@ -113,7 +115,7 @@ export default function parseDicom(byteArray, options = {}) { return new ByteStream(littleEndianByteArrayParser, byteArray, position); } - function mergeDataSets(metaHeaderDataSet, instanceDataSet) { + function mergeDataSets (metaHeaderDataSet, instanceDataSet) { for (const propertyName in metaHeaderDataSet.elements) { if (metaHeaderDataSet.elements.hasOwnProperty(propertyName)) { instanceDataSet.elements[propertyName] = metaHeaderDataSet.elements[propertyName]; @@ -127,7 +129,7 @@ export default function parseDicom(byteArray, options = {}) { return instanceDataSet; } - function readDataSet(metaHeaderDataSet) { + function readDataSet (metaHeaderDataSet) { const transferSyntax = readTransferSyntax(metaHeaderDataSet); const explicit = isExplicit(transferSyntax); const dataSetByteStream = getDataSetByteStream(transferSyntax, metaHeaderDataSet.position); @@ -151,7 +153,7 @@ export default function parseDicom(byteArray, options = {}) { } // main function here - function parseTheByteStream() { + function parseTheByteStream () { const metaHeaderDataSet = readPart10Header(byteArray, options); const dataSet = readDataSet(metaHeaderDataSet); @@ -162,4 +164,4 @@ export default function parseDicom(byteArray, options = {}) { return parseTheByteStream(); } -export { LEI, LEE, BEI }; \ No newline at end of file +export { LEI, LEE, BEI }; diff --git a/src/parseDicomDataSet.js b/src/parseDicomDataSet.js index 0dd4a264..3a9dce19 100644 --- a/src/parseDicomDataSet.js +++ b/src/parseDicomDataSet.js @@ -1,5 +1,6 @@ import readDicomElementExplicit from './readDicomElementExplicit.js'; import readDicomElementImplicit from './readDicomElementImplicit.js'; +import { invalidParameter, missingParameter, overflowErrorMessage } from './util/errors.js'; /** * Internal helper functions for parsing implicit and explicit DICOM data sets @@ -7,18 +8,23 @@ import readDicomElementImplicit from './readDicomElementImplicit.js'; /** * reads an explicit data set + * @param {DataSet} dataSet to read from * @param byteStream the byte stream to read from - * @param maxPosition the maximum position to read up to (optional - only needed when reading sequence items) + * @param {number} maxPosition the maximum position to read up to (optional - only needed when reading sequence items) + * @param {object} options + * @param {string} options.untilTag stop the parsing when encountering this tag + * */ export function parseDicomDataSetExplicit (dataSet, byteStream, maxPosition, options = {}) { maxPosition = (maxPosition === undefined) ? byteStream.byteArray.length : maxPosition; if (byteStream === undefined) { - throw 'dicomParser.parseDicomDataSetExplicit: missing required parameter \'byteStream\''; + throw new TypeError(missingParameter('byteStream')); } if (maxPosition < byteStream.position || maxPosition > byteStream.byteArray.length) { - throw 'dicomParser.parseDicomDataSetExplicit: invalid value for parameter \'maxP osition\''; + throw new RangeError(invalidParameter('maxPosition', `should be greater than current position (${byteStream.position.toString(10) + } and smaller than buffer length (${byteStream.byteArray.length.toString(10)}), got ${maxPosition.toString(10)}`)); } const elements = dataSet.elements; @@ -33,24 +39,34 @@ export function parseDicomDataSetExplicit (dataSet, byteStream, maxPosition, opt } if (byteStream.position > maxPosition) { - throw 'dicomParser:parseDicomDataSetExplicit: buffer overrun'; + throw new RangeError(overflowErrorMessage); } } /** * reads an implicit data set + * @param {DataSet} dataSet to read from * @param byteStream the byte stream to read from - * @param maxPosition the maximum position to read up to (optional - only needed when reading sequence items) + * @param {number} maxPosition the maximum position to read up to (optional - only needed when reading sequence items) + * @param {object} options + * @param {string} options.untilTag stop the parsing when encountering this tag */ -export function parseDicomDataSetImplicit (dataSet, byteStream, maxPosition, options = {}) { +export function parseDicomDataSetImplicit ( + dataSet, + byteStream, + maxPosition, + options = {} +) { maxPosition = (maxPosition === undefined) ? dataSet.byteArray.length : maxPosition; if (byteStream === undefined) { - throw 'dicomParser.parseDicomDataSetImplicit: missing required parameter \'byteStream\''; + throw new TypeError(missingParameter('byteStream')); } if (maxPosition < byteStream.position || maxPosition > byteStream.byteArray.length) { - throw 'dicomParser.parseDicomDataSetImplicit: invalid value for parameter \'maxPosition\''; + throw new RangeError(invalidParameter('maxPosition', `should be greater than current position (${ + byteStream.position.toString(10)} and smaller than buffer length (${ + byteStream.byteArray.length.toString(10)}), got ${maxPosition.toString(10)}`)); } const elements = dataSet.elements; diff --git a/src/readDicomElementExplicit.js b/src/readDicomElementExplicit.js index 2455e8ac..c8650ec2 100644 --- a/src/readDicomElementExplicit.js +++ b/src/readDicomElementExplicit.js @@ -1,9 +1,9 @@ import findEndOfEncapsulatedElement from './findEndOfEncapsulatedPixelData.js'; -import findAndSetUNElementLength from './findAndSetUNElementLength.js'; -import readSequenceItemsImplicit from './readSequenceElementImplicit.js'; +import readSequenceItemsImplicit from './readSequenceElementImplicit.js'; import readTag from './readTag.js'; import findItemDelimitationItemAndSetElementLength from './findItemDelimitationItem.js'; import readSequenceItemsExplicit from './readSequenceElementExplicit.js'; +import { missingParameter } from './util/errors.js'; /** * Internal helper functions for for parsing DICOM elements @@ -28,7 +28,7 @@ const getDataLengthSizeInBytesForVR = (vr) => { export default function readDicomElementExplicit (byteStream, warnings, untilTag) { if (byteStream === undefined) { - throw 'dicomParser.readDicomElementExplicit: missing required parameter \'byteStream\''; + throw new TypeError(missingParameter('byteStream')); } const element = { diff --git a/src/readDicomElementImplicit.js b/src/readDicomElementImplicit.js index f74c42b0..daee369f 100644 --- a/src/readDicomElementImplicit.js +++ b/src/readDicomElementImplicit.js @@ -2,6 +2,7 @@ import findItemDelimitationItemAndSetElementLength from './findItemDelimitationI import readSequenceItemsImplicit from './readSequenceElementImplicit.js'; import readTag from './readTag.js'; import { isPrivateTag } from './util/util.js'; +import { missingParameter } from './util/errors.js'; /** * Internal helper functions for for parsing DICOM elements @@ -31,7 +32,7 @@ const isSequence = (element, byteStream) => { export default function readDicomElementImplicit (byteStream, untilTag, vrCallback) { if (byteStream === undefined) { - throw 'dicomParser.readDicomElementImplicit: missing required parameter \'byteStream\''; + throw new TypeError(missingParameter('byteStream')); } const tag = readTag(byteStream); diff --git a/src/readEncapsulatedImageFrame.js b/src/readEncapsulatedImageFrame.js index 93a85e2e..89211475 100644 --- a/src/readEncapsulatedImageFrame.js +++ b/src/readEncapsulatedImageFrame.js @@ -1,4 +1,5 @@ import readEncapsulatedPixelDataFromFragments from './readEncapsulatedPixelDataFromFragments.js'; +import { invalidParameter, missingParameter } from './util/errors.js'; /** * Functionality for extracting encapsulated pixel data @@ -27,7 +28,7 @@ const calculateNumberOfFragmentsForFrame = (frameIndex, basicOffsetTable, fragme } } - throw 'dicomParser.calculateNumberOfFragmentsForFrame: could not find fragment with offset matching basic offset table'; + throw new Error('Failed to compute number of fragments for frame: could not find fragment with offset matching basic offset table.'); }; /** @@ -39,7 +40,7 @@ const calculateNumberOfFragmentsForFrame = (frameIndex, basicOffsetTable, fragme * * @param dataSet - the dataSet containing the encapsulated pixel data * @param pixelDataElement - the pixel data element (x7fe00010) to extract the frame from - * @param frameIndex - the zero based frame index + * @param {number} frameIndex - the zero based frame index * @param [basicOffsetTable] - optional array of starting offsets for frames * @param [fragments] - optional array of objects describing each fragment (offset, position, length) * @returns {object} with the encapsulated pixel data @@ -51,37 +52,42 @@ export default function readEncapsulatedImageFrame (dataSet, pixelDataElement, f // Validate parameters if (dataSet === undefined) { - throw 'dicomParser.readEncapsulatedImageFrame: missing required parameter \'dataSet\''; + throw new TypeError(missingParameter('dataSet')); } if (pixelDataElement === undefined) { - throw 'dicomParser.readEncapsulatedImageFrame: missing required parameter \'pixelDataElement\''; + throw new TypeError(missingParameter('pixelDataElement')); } if (frameIndex === undefined) { - throw 'dicomParser.readEncapsulatedImageFrame: missing required parameter \'frameIndex\''; + throw new TypeError(missingParameter('frameIndex')); } if (basicOffsetTable === undefined) { - throw 'dicomParser.readEncapsulatedImageFrame: parameter \'pixelDataElement\' does not have basicOffsetTable'; + throw new TypeError(invalidParameter('pixelDataElement', 'missing required property \'basicOffsetTable\'')); } if (pixelDataElement.tag !== 'x7fe00010') { - throw 'dicomParser.readEncapsulatedImageFrame: parameter \'pixelDataElement\' refers to non pixel data tag (expected tag = x7fe00010)'; + // PR question : should this be a warning, and we fall back to x7fe00010 anyway ? + throw new TypeError(invalidParameter( + 'pixelDataElement', + `'tag' property refers to non pixel data tag, should be 'x7fe00010', got '${pixelDataElement.tag}'` + )); } - if (pixelDataElement.encapsulatedPixelData !== true) { - throw 'dicomParser.readEncapsulatedImageFrame: parameter \'pixelDataElement\' refers to pixel data element that does not have encapsulated pixel data'; - } - if (pixelDataElement.hadUndefinedLength !== true) { - throw 'dicomParser.readEncapsulatedImageFrame: parameter \'pixelDataElement\' refers to pixel data element that does not have undefined length'; - } - if (pixelDataElement.fragments === undefined) { - throw 'dicomParser.readEncapsulatedImageFrame: parameter \'pixelDataElement\' refers to pixel data element that does not have fragments'; + if ( + pixelDataElement.encapsulatedPixelData !== true || + pixelDataElement.hadUndefinedLength !== true || + pixelDataElement.fragments === undefined + ) { + // PR question : this can only originate from a defect in dicomParser, should it be signified in the error message + // and should we take any kind of action ? + throw new Error('Tried to read non-encapsulated pixel data as if it was encapsulated.'); } if (basicOffsetTable.length === 0) { - throw 'dicomParser.readEncapsulatedImageFrame: basicOffsetTable has zero entries'; - } - if (frameIndex < 0) { - throw 'dicomParser.readEncapsulatedImageFrame: parameter \'frameIndex\' must be >= 0'; + throw new RangeError(invalidParameter('basicOffsetTable', 'table has no entry.')); } - if (frameIndex >= basicOffsetTable.length) { - throw 'dicomParser.readEncapsulatedImageFrame: parameter \'frameIndex\' must be < basicOffsetTable.length'; + if (frameIndex < 0 || basicOffsetTable.length <= frameIndex) { + throw new RangeError(invalidParameter( + 'frameIndex', + `should map into the basic offset table [0..${basicOffsetTable.length - 1}], got ${ + frameIndex.toString(10)}.` + )); } // find starting fragment based on the offset for the frame in the basic offset table @@ -89,7 +95,7 @@ export default function readEncapsulatedImageFrame (dataSet, pixelDataElement, f const startFragmentIndex = findFragmentIndexWithOffset(fragments, offset); if (startFragmentIndex === undefined) { - throw 'dicomParser.readEncapsulatedImageFrame: unable to find fragment that matches basic offset table entry'; + throw new RangeError('Failed to find fragment matching basic offset table entry.'); } // calculate the number of fragments for this frame diff --git a/src/readEncapsulatedPixelData.js b/src/readEncapsulatedPixelData.js index 8b8e216e..1fc5f5a6 100644 --- a/src/readEncapsulatedPixelData.js +++ b/src/readEncapsulatedPixelData.js @@ -1,5 +1,6 @@ import readEncapsulatedImageFrame from './readEncapsulatedImageFrame.js'; import readEncapsulatedPixelDataFromFragments from './readEncapsulatedPixelDataFromFragments.js'; +import { invalidParameter, missingParameter } from './util/errors.js'; /** * Functionality for extracting encapsulated pixel data @@ -16,10 +17,10 @@ let deprecatedNoticeLogged = false; * @deprecated since version 1.6 - use readEncapsulatedPixelDataFromFragments() or readEncapsulatedImageFrame() * @param dataSet - the dataSet containing the encapsulated pixel data * @param pixelDataElement - the pixel data element (x7fe00010) to extract the frame from - * @param frame - the zero based frame index + * @param {number} frameIndex - the zero based frame index * @returns {object} with the encapsulated pixel data */ -export default function readEncapsulatedPixelData (dataSet, pixelDataElement, frame) { +export default function readEncapsulatedPixelData (dataSet, pixelDataElement, frameIndex) { if (!deprecatedNoticeLogged) { deprecatedNoticeLogged = true; @@ -29,36 +30,38 @@ export default function readEncapsulatedPixelData (dataSet, pixelDataElement, fr } if (dataSet === undefined) { - throw 'dicomParser.readEncapsulatedPixelData: missing required parameter \'dataSet\''; + throw new TypeError(missingParameter('dataSet')); } if (pixelDataElement === undefined) { - throw 'dicomParser.readEncapsulatedPixelData: missing required parameter \'element\''; + throw new TypeError(missingParameter('pixelDataElement')); } - if (frame === undefined) { - throw 'dicomParser.readEncapsulatedPixelData: missing required parameter \'frame\''; + if (frameIndex === undefined) { + throw new TypeError(missingParameter('frameIndex')); } if (pixelDataElement.tag !== 'x7fe00010') { - throw 'dicomParser.readEncapsulatedPixelData: parameter \'element\' refers to non pixel data tag (expected tag = x7fe00010)'; + throw new TypeError(invalidParameter( + 'pixelDataElement', + `'tag' property refers to non pixel data tag, should be 'x7fe00010', got '${pixelDataElement.tag}'` + )); } - if (pixelDataElement.encapsulatedPixelData !== true) { - throw 'dicomParser.readEncapsulatedPixelData: parameter \'element\' refers to pixel data element that does not have encapsulated pixel data'; + if ( + pixelDataElement.encapsulatedPixelData !== true || + pixelDataElement.hadUndefinedLength !== true || + pixelDataElement.basicOffsetTable === undefined || + pixelDataElement.fragments === undefined + ) { + throw new Error('Tried to read non-encapsulated pixel data as if it was encapsulated.'); } - if (pixelDataElement.hadUndefinedLength !== true) { - throw 'dicomParser.readEncapsulatedPixelData: parameter \'element\' refers to pixel data element that does not have encapsulated pixel data'; - } - if (pixelDataElement.basicOffsetTable === undefined) { - throw 'dicomParser.readEncapsulatedPixelData: parameter \'element\' refers to pixel data element that does not have encapsulated pixel data'; - } - if (pixelDataElement.fragments === undefined) { - throw 'dicomParser.readEncapsulatedPixelData: parameter \'element\' refers to pixel data element that does not have encapsulated pixel data'; - } - if (frame < 0) { - throw 'dicomParser.readEncapsulatedPixelData: parameter \'frame\' must be >= 0'; + if (frameIndex < 0) { + throw new RangeError(invalidParameter( + 'frameIndex', + `should be a positive integer, got ${frameIndex.toString(10)}` + )); } // If the basic offset table is not empty, we can extract the frame if (pixelDataElement.basicOffsetTable.length !== 0) { - return readEncapsulatedImageFrame(dataSet, pixelDataElement, frame); + return readEncapsulatedImageFrame(dataSet, pixelDataElement, frameIndex); } // No basic offset table, assume all fragments are for one frame - NOTE that this is NOT a valid diff --git a/src/readEncapsulatedPixelDataFromFragments.js b/src/readEncapsulatedPixelDataFromFragments.js index 4b6e001b..579b371c 100644 --- a/src/readEncapsulatedPixelDataFromFragments.js +++ b/src/readEncapsulatedPixelDataFromFragments.js @@ -2,6 +2,7 @@ import alloc from './alloc.js'; import ByteStream from './byteStream.js'; import readSequenceItem from './readSequenceItem.js'; import sharedCopy from './sharedCopy.js'; +import { invalidParameter, missingParameter } from './util/errors.js'; /** * Functionality for extracting encapsulated pixel data @@ -23,7 +24,7 @@ const calculateBufferSize = (fragments, startFragment, numFragments) => { * * @param dataSet - the dataSet containing the encapsulated pixel data * @param pixelDataElement - the pixel data element (x7fe00010) to extract the fragment data from - * @param startFragmentIndex - zero based index of the first fragment to extract from + * @param {number} startFragmentIndex - zero based index of the first fragment to extract from * @param [numFragments] - the number of fragments to extract from, default is 1 * @param [fragments] - optional array of objects describing each fragment (offset, position, length) * @returns {object} byte array with the encapsulated pixel data @@ -35,46 +36,45 @@ export default function readEncapsulatedPixelDataFromFragments (dataSet, pixelDa // check parameters if (dataSet === undefined) { - throw 'dicomParser.readEncapsulatedPixelDataFromFragments: missing required parameter \'dataSet\''; + throw new TypeError(missingParameter('dataSet')); } if (pixelDataElement === undefined) { - throw 'dicomParser.readEncapsulatedPixelDataFromFragments: missing required parameter \'pixelDataElement\''; + throw new TypeError(missingParameter('pixelDataElement')); } if (startFragmentIndex === undefined) { - throw 'dicomParser.readEncapsulatedPixelDataFromFragments: missing required parameter \'startFragmentIndex\''; + throw new TypeError(missingParameter('startFragmentIndex')); } if (numFragments === undefined) { - throw 'dicomParser.readEncapsulatedPixelDataFromFragments: missing required parameter \'numFragments\''; + throw new TypeError(missingParameter('numFragments')); } if (pixelDataElement.tag !== 'x7fe00010') { - throw 'dicomParser.readEncapsulatedPixelDataFromFragments: parameter \'pixelDataElement\' refers to non pixel data tag (expected tag = x7fe00010'; - } - if (pixelDataElement.encapsulatedPixelData !== true) { - throw 'dicomParser.readEncapsulatedPixelDataFromFragments: parameter \'pixelDataElement\' refers to pixel data element that does not have encapsulated pixel data'; - } - if (pixelDataElement.hadUndefinedLength !== true) { - throw 'dicomParser.readEncapsulatedPixelDataFromFragments: parameter \'pixelDataElement\' refers to pixel data element that does not have encapsulated pixel data'; - } - if (pixelDataElement.basicOffsetTable === undefined) { - throw 'dicomParser.readEncapsulatedPixelDataFromFragments: parameter \'pixelDataElement\' refers to pixel data element that does not have encapsulated pixel data'; - } - if (pixelDataElement.fragments === undefined) { - throw 'dicomParser.readEncapsulatedPixelDataFromFragments: parameter \'pixelDataElement\' refers to pixel data element that does not have encapsulated pixel data'; - } - if (pixelDataElement.fragments.length <= 0) { - throw 'dicomParser.readEncapsulatedPixelDataFromFragments: parameter \'pixelDataElement\' refers to pixel data element that does not have encapsulated pixel data'; - } - if (startFragmentIndex < 0) { - throw 'dicomParser.readEncapsulatedPixelDataFromFragments: parameter \'startFragmentIndex\' must be >= 0'; - } - if (startFragmentIndex >= pixelDataElement.fragments.length) { - throw 'dicomParser.readEncapsulatedPixelDataFromFragments: parameter \'startFragmentIndex\' must be < number of fragments'; + throw new TypeError(invalidParameter('pixelDataElement', `'tag' property refers to non pixel data tag, should be 'x7fe00010', got '${pixelDataElement.tag}'`)); + } + if ( + pixelDataElement.encapsulatedPixelData !== true || + pixelDataElement.hadUndefinedLength !== true || + pixelDataElement.basicOffsetTable === undefined || + pixelDataElement.fragments === undefined || + pixelDataElement.fragments.length <= 0 + ) { + throw new Error('Tried to read non-encapsulated pixel data as if it was encapsulated.'); + } + if (startFragmentIndex < 0 || pixelDataElement.fragments.length <= startFragmentIndex) { + throw new RangeError(invalidParameter( + 'startFragmentIndex', + `should map to one of the element's fragments [0..${pixelDataElement.fragments.length - 1 + }], got ${startFragmentIndex.toString(10)}` + )); } if (numFragments < 1) { - throw 'dicomParser.readEncapsulatedPixelDataFromFragments: parameter \'numFragments\' must be > 0'; + throw new RangeError(invalidParameter( + 'numFragments', + `should be a positive integer, got ${numFragments.toString(10)}` + )); } if (startFragmentIndex + numFragments > pixelDataElement.fragments.length) { - throw 'dicomParser.readEncapsulatedPixelDataFromFragments: parameter \'startFragment\' + \'numFragments\' < number of fragments'; + throw new RangeError(`Tried to read more fragments (${(startFragmentIndex + numFragments).toString(10) + } than available (${pixelDataElement.fragments.length})`); } // create byte stream on the data for this pixel data element @@ -84,7 +84,7 @@ export default function readEncapsulatedPixelDataFromFragments (dataSet, pixelDa const basicOffsetTable = readSequenceItem(byteStream); if (basicOffsetTable.tag !== 'xfffee000') { - throw 'dicomParser.readEncapsulatedPixelData: missing basic offset table xfffee000'; + throw new TypeError('Failed to find basic offset table starting item tag (FFFE,E000).'); } byteStream.seek(basicOffsetTable.length); @@ -102,6 +102,7 @@ export default function readEncapsulatedPixelDataFromFragments (dataSet, pixelDa // more than one fragment, combine all of the fragments into one buffer const bufferSize = calculateBufferSize(fragments, startFragmentIndex, numFragments); const pixelData = alloc(byteStream.byteArray, bufferSize); + let pixelDataIndex = 0; for (let i = startFragmentIndex; i < startFragmentIndex + numFragments; i++) { diff --git a/src/readPart10Header.js b/src/readPart10Header.js index adf219b2..517277bc 100644 --- a/src/readPart10Header.js +++ b/src/readPart10Header.js @@ -2,6 +2,7 @@ import ByteStream from './byteStream.js'; import DataSet from './dataSet.js'; import littleEndianByteArrayParser from './littleEndianByteArrayParser.js'; import readDicomElementExplicit from './readDicomElementExplicit.js'; +import { missingParameter } from './util/errors.js'; /** * Parses a DICOM P10 byte array and returns a DataSet object with the parsed elements. If the options @@ -19,13 +20,13 @@ import readDicomElementExplicit from './readDicomElementExplicit.js'; export default function readPart10Header (byteArray, options = {}) { if (byteArray === undefined) { - throw 'dicomParser.readPart10Header: missing required parameter \'byteArray\''; + throw new TypeError(missingParameter('byteArray')); } const { TransferSyntaxUID } = options; const littleEndianByteStream = new ByteStream(littleEndianByteArrayParser, byteArray); - function readPrefix() { + function readPrefix () { if (littleEndianByteStream.getSize() <= 132 && TransferSyntaxUID) { return false; } @@ -34,17 +35,20 @@ export default function readPart10Header (byteArray, options = {}) { if (prefix !== 'DICM') { const { TransferSyntaxUID } = options || {}; + if (!TransferSyntaxUID) { - throw 'dicomParser.readPart10Header: DICM prefix not found at location 132 - this is not a valid DICOM P10 file.'; + throw new Error('Failed to find DICM prefix in preamble (index 132), buffer is not a valid DICOM P10 file.'); } littleEndianByteStream.seek(0); + return false; } + return true; } // main function here - function readTheHeader() { + function readTheHeader () { // Per the DICOM standard, the header is always encoded in Explicit VR Little Endian (see PS3.10, section 7.1) // so use littleEndianByteStream throughout this method regardless of the transfer syntax const isPart10 = readPrefix(); @@ -55,10 +59,13 @@ export default function readPart10Header (byteArray, options = {}) { if (!isPart10) { littleEndianByteStream.position = 0; const metaHeaderDataSet = { - elements: { x00020010: { tag: 'x00020010', vr: 'UI', Value: TransferSyntaxUID } }, - warnings, + elements: { x00020010: { tag: 'x00020010', + vr: 'UI', + Value: TransferSyntaxUID } }, + warnings }; // console.log('Returning metaHeaderDataSet', metaHeaderDataSet); + return metaHeaderDataSet; } diff --git a/src/readSequenceElementExplicit.js b/src/readSequenceElementExplicit.js index 2e46ab7c..d73238b6 100644 --- a/src/readSequenceElementExplicit.js +++ b/src/readSequenceElementExplicit.js @@ -3,6 +3,7 @@ import readDicomElementExplicit from './readDicomElementExplicit.js'; import readSequenceItem from './readSequenceItem.js'; import readTag from './readTag.js'; import * as parseDicomDataSet from './parseDicomDataSet.js'; +import {missingParameter} from "./util/errors.js"; /** * Internal helper functions for parsing DICOM elements @@ -79,11 +80,11 @@ function readSQElementKnownLengthExplicit (byteStream, element, warnings) { export default function readSequenceItemsExplicit (byteStream, element, warnings) { if (byteStream === undefined) { - throw 'dicomParser.readSequenceItemsExplicit: missing required parameter \'byteStream\''; + throw new TypeError(missingParameter('byteStream')); } if (element === undefined) { - throw 'dicomParser.readSequenceItemsExplicit: missing required parameter \'element\''; + throw new TypeError(missingParameter('element')); } element.items = []; diff --git a/src/readSequenceElementImplicit.js b/src/readSequenceElementImplicit.js index e89b56d0..a47e9fde 100644 --- a/src/readSequenceElementImplicit.js +++ b/src/readSequenceElementImplicit.js @@ -3,6 +3,7 @@ import readDicomElementImplicit from './readDicomElementImplicit.js'; import readSequenceItem from './readSequenceItem.js'; import readTag from './readTag.js'; import * as parseDicomDataSet from './parseDicomDataSet.js'; +import { missingParameter } from './util/errors.js'; /** * Internal helper functions for parsing DICOM elements @@ -86,11 +87,11 @@ function readSQElementKnownLengthImplicit (byteStream, element, vrCallback) { */ export default function readSequenceItemsImplicit (byteStream, element, vrCallback) { if (byteStream === undefined) { - throw 'dicomParser.readSequenceItemsImplicit: missing required parameter \'byteStream\''; + throw new TypeError(missingParameter('byteStream')); } if (element === undefined) { - throw 'dicomParser.readSequenceItemsImplicit: missing required parameter \'element\''; + throw new TypeError(missingParameter('element')); } element.items = []; diff --git a/src/readSequenceItem.js b/src/readSequenceItem.js index 04d23084..94e045c3 100644 --- a/src/readSequenceItem.js +++ b/src/readSequenceItem.js @@ -1,4 +1,5 @@ import readTag from './readTag.js'; +import { missingParameter } from './util/errors.js'; /** * Internal helper functions for parsing DICOM elements @@ -14,7 +15,7 @@ import readTag from './readTag.js'; */ export default function readSequenceItem (byteStream) { if (byteStream === undefined) { - throw 'dicomParser.readSequenceItem: missing required parameter \'byteStream\''; + throw new TypeError(missingParameter('byteStream')); } const element = { @@ -24,7 +25,9 @@ export default function readSequenceItem (byteStream) { }; if (element.tag !== 'xfffee000') { - throw `dicomParser.readSequenceItem: item tag (FFFE,E000) not found at offset ${byteStream.position}`; + throw new Error( + `Failed to find sequence item tag (FFFE,E000) at offset ${byteStream.position}, found ${element.tag} instead.` + ); } return element; diff --git a/src/readTag.js b/src/readTag.js index a8a1a9f4..3474271e 100644 --- a/src/readTag.js +++ b/src/readTag.js @@ -2,6 +2,8 @@ * Internal helper functions for parsing DICOM elements */ +import { missingParameter } from './util/errors.js'; + /** * Reads a tag (group number and element number) from a byteStream * @param byteStream the byte stream to read from @@ -10,7 +12,7 @@ */ export default function readTag (byteStream) { if (byteStream === undefined) { - throw 'dicomParser.readTag: missing required parameter \'byteStream\''; + throw new TypeError(missingParameter('byteStream')); } const groupNumber = byteStream.readUint16() * 256 * 256; diff --git a/src/sharedCopy.js b/src/sharedCopy.js index 57135388..22d60115 100644 --- a/src/sharedCopy.js +++ b/src/sharedCopy.js @@ -4,6 +4,8 @@ * */ +import { invalidParameter } from './util/errors.js'; + /** * Creates a view of the underlying byteArray. The view is of the same type as the byteArray (e.g. * Uint8Array or Buffer) and shares the same underlying memory (changing one changes the other) @@ -18,5 +20,5 @@ export default function sharedCopy (byteArray, byteOffset, length) { } else if (byteArray instanceof Uint8Array) { return new Uint8Array(byteArray.buffer, byteArray.byteOffset + byteOffset, length); } - throw 'dicomParser.from: unknown type for byteArray'; + throw new TypeError(invalidParameter('byteArray', 'should be of type Uint8Array or Buffer')); } diff --git a/src/util/createJPEGBasicOffsetTable.js b/src/util/createJPEGBasicOffsetTable.js index 652d374b..154291c6 100644 --- a/src/util/createJPEGBasicOffsetTable.js +++ b/src/util/createJPEGBasicOffsetTable.js @@ -1,4 +1,6 @@ // Each JPEG image has an end of image marker 0xFFD9 +import { invalidParameter, missingParameter } from './errors'; + function isEndOfImageMarker (dataSet, position) { return (dataSet.byteArray[position] === 0xFF && dataSet.byteArray[position + 1] === 0xD9); @@ -35,31 +37,28 @@ function findLastImageFrameFragmentIndex (dataSet, pixelDataElement, startFragme export default function createJPEGBasicOffsetTable (dataSet, pixelDataElement, fragments) { // Validate parameters if (dataSet === undefined) { - throw 'dicomParser.createJPEGBasicOffsetTable: missing required parameter dataSet'; + throw new TypeError(missingParameter('dataSet')); } if (pixelDataElement === undefined) { - throw 'dicomParser.createJPEGBasicOffsetTable: missing required parameter pixelDataElement'; + throw new TypeError(missingParameter('pixelDataElement')); } if (pixelDataElement.tag !== 'x7fe00010') { - throw 'dicomParser.createJPEGBasicOffsetTable: parameter \'pixelDataElement\' refers to non pixel data tag (expected tag = x7fe00010\''; - } - if (pixelDataElement.encapsulatedPixelData !== true) { - throw 'dicomParser.createJPEGBasicOffsetTable: parameter \'pixelDataElement\' refers to pixel data element that does not have encapsulated pixel data'; - } - if (pixelDataElement.hadUndefinedLength !== true) { - throw 'dicomParser.createJPEGBasicOffsetTable: parameter \'pixelDataElement\' refers to pixel data element that does not have encapsulated pixel data'; - } - if (pixelDataElement.basicOffsetTable === undefined) { - throw 'dicomParser.createJPEGBasicOffsetTable: parameter \'pixelDataElement\' refers to pixel data element that does not have encapsulated pixel data'; - } - if (pixelDataElement.fragments === undefined) { - throw 'dicomParser.createJPEGBasicOffsetTable: parameter \'pixelDataElement\' refers to pixel data element that does not have encapsulated pixel data'; + throw new TypeError(invalidParameter( + 'pixelDataElement', + `'tag' property refers to non pixel data tag, should be 'x7fe00010', got '${pixelDataElement.tag}'`) + ); } - if (pixelDataElement.fragments.length <= 0) { - throw 'dicomParser.createJPEGBasicOffsetTable: parameter \'pixelDataElement\' refers to pixel data element that does not have encapsulated pixel data'; + if ( + pixelDataElement.encapsulatedPixelData !== true || + pixelDataElement.hadUndefinedLength !== true || + pixelDataElement.basicOffsetTable === undefined || + pixelDataElement.fragments === undefined || + pixelDataElement.fragments.length <= 0 + ) { + throw new Error('Tried to read non-encapsulated pixel data as if it was encapsulated.'); } if (fragments && fragments.length <= 0) { - throw 'dicomParser.createJPEGBasicOffsetTable: parameter \'fragments\' must not be zero length'; + throw new RangeError(invalidParameter('fragments', 'should be an array of at least one fragment object')); } // Default values diff --git a/src/util/dataSetToJS.js b/src/util/dataSetToJS.js index 7e916dbb..5758d05a 100644 --- a/src/util/dataSetToJS.js +++ b/src/util/dataSetToJS.js @@ -1,5 +1,6 @@ import explicitElementToString from './elementToString.js'; import * as util from './util.js'; +import { missingParameter } from './errors.js'; /** * converts an explicit dataSet to a javascript object @@ -8,7 +9,7 @@ import * as util from './util.js'; */ export default function explicitDataSetToJS (dataSet, options) { if (dataSet === undefined) { - throw 'dicomParser.explicitDataSetToJS: missing required parameter dataSet'; + throw new TypeError(missingParameter('dataSet')); } options = options || { diff --git a/src/util/elementToString.js b/src/util/elementToString.js index cf5a8b7d..7789f939 100644 --- a/src/util/elementToString.js +++ b/src/util/elementToString.js @@ -1,18 +1,26 @@ import * as util from './util.js'; +import { invalidParameter, missingParameter } from './errors.js'; /** * Converts an explicit VR element to a string or undefined if it is not possible to convert. * Throws an error if an implicit element is supplied * @param dataSet * @param element - * @returns {*} + * @returns {string | undefined} + * @throws {TypeError} missing parameter or implicit element provided */ export default function explicitElementToString (dataSet, element) { - if (dataSet === undefined || element === undefined) { - throw 'dicomParser.explicitElementToString: missing required parameters'; + if (dataSet === undefined) { + throw new TypeError(missingParameter('dataSet')); + } + if (element === undefined) { + throw new TypeError(missingParameter('element')); } if (element.vr === undefined) { - throw 'dicomParser.explicitElementToString: cannot convert implicit element to string'; + throw new TypeError(invalidParameter( + 'element', + 'should have a defined \'vr\' property, conversion of implicit element to string is not possible.' + )); } var vr = element.vr; var tag = element.tag; diff --git a/src/util/errors.js b/src/util/errors.js new file mode 100644 index 00000000..f6fbcb25 --- /dev/null +++ b/src/util/errors.js @@ -0,0 +1,32 @@ +export const underflowErrorMessage = 'Attempted to read before buffer start'; +export const overflowErrorMessage = 'Attempted to read past buffer end'; + +/** + * + * @param {string} param the name of the missing parameter + * @returns {`Missing required parameter '${string}'`} + */ +export const missingParameter = (param) => `Missing required parameter '${param}'`; + +/** + * + * @param {string} param the name of the invalid parameter + * @param {string} explanation why the parameter is invalid and, what conditions should the parameter's value meet to + * be valid and if possible the received value. + * @returns {`Invalid parameter '${string}': ${string}`} + */ +export const invalidParameter = (param, explanation) => `Invalid parameter '${param}': ${explanation}`; + +/** + * Encapsulate errors thrown during a parsing operation. + * + * @property {DataSet} dataSet the dataset the error relates to, might be only partially constructed + * depending on where the error occurred. + * @property {Error} cause the original parsing error + */ +export class DicomParserError extends Error { + constructor (dataSet, cause) { + super('Failed to parse dataSet.', { cause }); + this.dataSet = dataSet; + } +} diff --git a/src/util/parseDA.js b/src/util/parseDA.js index e3e6cecf..0da768a8 100644 --- a/src/util/parseDA.js +++ b/src/util/parseDA.js @@ -23,18 +23,23 @@ function isValidDate (d, m, y) { /** * Parses a DA formatted string into a Javascript object * @param {string} date a string in the DA VR format - * @param {boolean} [validate] - true if an exception should be thrown if the date is invalid - * @returns {*} Javascript object with properties year, month and day or undefined if not present or not 8 bytes long + * @param {boolean} [validate = false] - should an exception be thrown when the data is invalid + * @returns {{year: number, month: number, day: number} | undefined} Javascript object with properties year, month and + * day or undefined if not present or not 8 bytes long */ export default function parseDA (date, validate) { + const invalidDate = `Failed to parse string as DA, format or value is not valid, got ${date}`; + if (date && date.length === 8) { var yyyy = parseInt(date.substring(0, 4), 10); var mm = parseInt(date.substring(4, 6), 10); var dd = parseInt(date.substring(6, 8), 10); + + // PR question this will return an invalid date object if validate is false, should it be adressed in this PR ? if (validate) { if (isValidDate(dd, mm, yyyy) !== true) { - throw `invalid DA '${date}'`; + throw new Error(invalidDate); } } @@ -45,7 +50,7 @@ export default function parseDA (date, validate) { }; } if (validate) { - throw `invalid DA '${date}'`; + throw new Error(invalidDate); } return undefined; diff --git a/src/util/parseTM.js b/src/util/parseTM.js index 0c987dea..ba4a36e7 100644 --- a/src/util/parseTM.js +++ b/src/util/parseTM.js @@ -1,10 +1,14 @@ /** - * Parses a TM formatted string into a javascript object with properties for hours, minutes, seconds and fractionalSeconds + * Parses a TM formatted string into a javascript object with properties for hours, minutes, seconds and + * fractionalSeconds * @param {string} time - a string in the TM VR format - * @param {boolean} [validate] - true if an exception should be thrown if the date is invalid - * @returns {*} javascript object with properties for hours, minutes, seconds and fractionalSeconds or undefined if no element or data. Missing fields are set to undefined + * @param {boolean} [validate = false] - should an exception be thrown when the data is invalid + * @returns {*} javascript object with properties for hours, minutes, seconds and fractionalSeconds or undefined if no + * element or data. Missing fields are set to undefined */ export default function parseTM (time, validate) { + const parsingError = `Failed to parse string as TM, format or value is not valid, got ${time}`; + if (time.length >= 2) { // must at least have HH // 0123456789 // HHMMSS.FFFFFF @@ -24,7 +28,7 @@ export default function parseTM (time, validate) { (mm && (mm < 0 || mm > 59)) || (ss && (ss < 0 || ss > 59)) || (ffffff && (ffffff < 0 || ffffff > 999999))) { - throw `invalid TM '${time}'`; + throw new Error(parsingError); } } @@ -37,7 +41,7 @@ export default function parseTM (time, validate) { } if (validate) { - throw `invalid TM '${time}'`; + throw new Error(parsingError); } return undefined; diff --git a/src/util/util.js b/src/util/util.js index 4428a63c..1cd25140 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -41,12 +41,13 @@ const isStringVr = (vr) => stringVrs[vr]; * Tests to see if a given tag in the format xggggeeee is a private tag or not * @param tag * @returns {boolean} - * @throws error if fourth character cannot be parsed + * @throws {Error} fourth character of tag group cannot be parsed */ const isPrivateTag = (tag) => { const lastGroupDigit = parseInt(tag[4], 16); + if (isNaN(lastGroupDigit)) { - throw 'dicomParser.isPrivateTag: cannot parse last character of group'; + throw new Error('Failed to parse last character of tag group as hex value'); } const groupIsOdd = (lastGroupDigit % 2) === 1; @@ -54,10 +55,11 @@ const isPrivateTag = (tag) => { }; /** - * Parses a PN formatted string into a javascript object with properties for givenName, familyName, middleName, prefix and suffix + * Parses a PN formatted string into a javascript object with properties for givenName, familyName, middleName, prefix + * and suffix * @param personName a string in the PN VR format - * @param index - * @returns {*} javascript object with properties for givenName, familyName, middleName, prefix and suffix or undefined if no element or data + * @returns {*} javascript object with properties for givenName, familyName, middleName, prefix and suffix or undefined + * if no element or data */ const parsePN = (personName) => { if (personName === undefined) {