Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type table loader. #98

Merged
merged 8 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ jobs:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm install
Expand All @@ -25,9 +25,9 @@ jobs:
matrix:
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm install
Expand All @@ -41,9 +41,9 @@ jobs:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm install
Expand All @@ -57,16 +57,17 @@ jobs:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- name: Generate coverage report
run: npm run coverage-ci
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v2
# with:
# file: ./coverage/lcov.info
# fail_ci_if_error: true
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage/lcov.info
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# @digitalbazaar/cborld ChangeLog

## 7.2.0 - 2024-10-xx

### Added
- Add `async function typeTableLoader({registryEntryId})` option to look up the
`typeTable` to use by id for both `encode` and `decode`.

### Changed
- **NOTE**: The handling of `typeTable` and `typeTableLoader` is more strict
than before and requies one option be used when appropriate. This could cause
issues with code that was depending on undefined behavior.
- Refactor `registryEntryId` encoding and decoding logic. Trying to be more
readable and handle more error and edge cases. This is a work in progress.

## 7.1.3 - 2024-10-16

### Fixed
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# JavaScript CBOR-LD Processor

[![Build Status](https://img.shields.io/github/actions/workflow/status/digitalbazaar/cborld/main.yml)](https://github.com/digitalbazaar/cborld/actions/workflows/main.yml)
[![Coverage Status](https://img.shields.io/codecov/c/github/digitalbazaar/cborld)](https://codecov.io/gh/digitalbazaar/cborld)

> A JavaScript CBOR-LD Process for Web browsers and Node.js apps.

## Table of Contents
Expand Down Expand Up @@ -76,6 +79,8 @@ const jsonldDocument = await cborld.decode({cborldBytes, documentLoader});

## API

**NOTE**: Please check `encode.js` and `decode.js` for the latest API options.

### Functions

<dl>
Expand Down
104 changes: 83 additions & 21 deletions lib/decode.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {CborldError} from './CborldError.js';
import {Converter} from './Converter.js';
import {Decompressor} from './Decompressor.js';
import {inspect} from './util.js';
import {default as varint} from 'varint';

// 0xd9 == 11011001
// 110 = CBOR major type 6
Expand All @@ -23,23 +24,27 @@ const CBORLD_TAG_SECOND_BYTE_LEGACY = 0x05;
* @param {object} options - The options to use when decoding CBOR-LD.
* @param {Uint8Array} options.cborldBytes - The encoded CBOR-LD bytes to
* decode.
* @param {Function} options.documentLoader -The document loader to use when
* resolving JSON-LD Context URLs.
* @param {diagnosticFunction} options.diagnose - A function that, if
* provided, is called with diagnostic information.
* @param {Map} options.typeTable - A map of possible value types, including
* @param {documentLoaderFunction} options.documentLoader - The document loader
* to use when resolving JSON-LD Context URLs.
* @param {Map} [options.typeTable] - A map of possible value types, including
* `context`, `url`, `none`, and any JSON-LD type, each of which maps to
* another map of values of that type to their associated CBOR-LD integer
* values.
* @param {Map} options.appContextMap - A map of context string values
* @param {Function} [options.typeTableLoader] - The typeTable loader to use to
* resolve a registryEntryId to a typeTable.
* @param {diagnosticFunction} [options.diagnose] - A function that, if
* provided, is called with diagnostic information.
* @param {Map} [options.appContextMap] - A map of context string values
* to their associated CBOR-LD integer values. For use with legacy
* cborldBytes.
*
* @returns {Promise<object>} - The decoded JSON-LD Document.
*/
export async function decode({
cborldBytes, documentLoader,
cborldBytes,
documentLoader,
typeTable,
typeTableLoader,
diagnose,
appContextMap = new Map(),
}) {
Expand All @@ -55,7 +60,7 @@ export async function decode({
'ERR_NOT_CBORLD',
'CBOR-LD must start with a CBOR major type "Tag" header of `0xd9`.');
}
const {suffix, isLegacy} = _getSuffix({cborldBytes});
const {suffix, isLegacy, registryEntryId} = _getSuffix({cborldBytes});
const isCompressed = _checkCompressionMode({cborldBytes, isLegacy});
if(!isCompressed) {
return cborg.decode(suffix, {useMaps: false});
Expand All @@ -68,6 +73,22 @@ export async function decode({
diagnose(inspect(input, {depth: null, colors: true}));
}

// lookup typeTable by id if needed
if(!isLegacy) {
if(typeTable && typeTableLoader) {
throw new TypeError('Use either "typeTable" or "typeTableLoader".');
}
if(!typeTable && typeTableLoader) {
typeTable = await typeTableLoader({registryEntryId});
}
if(!typeTable) {
throw new CborldError(
'ERR_NO_TYPETABLE',
'"typeTable" not provided or found for registryEntryId ' +
`"${registryEntryId}".`);
}
}

const converter = _createConverter({
isLegacy,
typeTable,
Expand Down Expand Up @@ -126,27 +147,59 @@ function _checkCompressionMode({cborldBytes, isLegacy}) {
}

function _getSuffix({cborldBytes}) {
const isModern = cborldBytes[1] === CBORLD_TAG_SECOND_BYTE;
const isLegacy = cborldBytes[1] === CBORLD_TAG_SECOND_BYTE_LEGACY;
let index = 1; // start after 0xd9
const isModern = cborldBytes[index] === CBORLD_TAG_SECOND_BYTE;
const isLegacy = cborldBytes[index] === CBORLD_TAG_SECOND_BYTE_LEGACY;
if(!(isModern || isLegacy)) {
throw new CborldError(
'ERR_NOT_CBORLD',
'CBOR-LD must either have a second byte of 0x06 or 0x05 (legacy).');
}

const tagValue = cborldBytes[2];
let index = 3;
if(isModern && tagValue >= 128) {
// FIXME: this assumes tag length <= 31 bytes; throw error if not
// cborldBytes[index + 1] is the header byte for the varint bytestring
const varintArrayLength = cborldBytes[index + 1] % 32;
// This sets `index` to the index of the first byte of the second
// array element in `cborldBytes`
index += varintArrayLength + 2;
}
index++; // advance to tag value
const {buffer, byteOffset, length} = cborldBytes;
const tagValue = cborldBytes[index];
let registryEntryId;
if(isModern) {
if(tagValue < 128) {
registryEntryId = tagValue;
// advance to encoded data
index++;
} else {
index++; // advance to array
// check for 2 element array
if(cborldBytes[index] !== 0x82) {
throw new CborldError(
'ERR_NOT_CBORLD',
'CBOR-LD large varint encoding error.');
}
index++; // advance to byte string tag
// first element is tail of varint encoded as byte string
// low 5 bits are byte string length (or exceptions for large values)
const varintArrayLength = cborldBytes[index] % 32;
// don't support unbounded lengths here
if(varintArrayLength >= 24) {
throw new CborldError(
'ERR_NOT_CBORLD',
'CBOR-LD encoded registryEntryId too large.');
}
// FIXME: check for bad 0 length
index++; // advance to byte string data
// create single buffer for id varint initial byte and tail bytes
const varintBytes = new Uint8Array(varintArrayLength + 1);
varintBytes[0] = tagValue;
const varintTailBytes = new Uint8Array(buffer, index, varintArrayLength);
varintBytes.set(varintTailBytes, 1);
// decode id from varint
registryEntryId = varint.decode(varintBytes);
// advance to second array element
index += varintArrayLength;
}
} else {
index++; // advance to tag value
}
const suffix = new Uint8Array(buffer, byteOffset + index, length - index);
return {suffix, isLegacy};
return {suffix, isLegacy, registryEntryId};
}

/**
Expand All @@ -156,3 +209,12 @@ function _getSuffix({cborldBytes}) {
* @callback diagnosticFunction
* @param {string} message - The diagnostic message.
*/

/**
* Fetches a resource given a URL and returns it as a string.
*
* @callback documentLoaderFunction
* @param {string} url - The URL to retrieve.

* @returns {Promise<string>} The resource associated with the URL as a string.
*/
Loading