Skip to content

Commit

Permalink
Merge pull request #73 from shgysk8zer0/patch/hashing
Browse files Browse the repository at this point in the history
Update hashing
  • Loading branch information
shgysk8zer0 authored Sep 17, 2024
2 parents b4707ae + 6292852 commit e169be7
Show file tree
Hide file tree
Showing 15 changed files with 1,529 additions and 399 deletions.
5 changes: 0 additions & 5 deletions .eslintignore

This file was deleted.

52 changes: 0 additions & 52 deletions .eslintrc.json

This file was deleted.

66 changes: 48 additions & 18 deletions .github/workflows/super-linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,33 @@ on:
branches:
- master
- release/*
paths-ignore:
- '**/*.js' # Disable since ESLint is used and v9 config is incompatible
- '**/*.mjs'
- '**/*.cjs'
- '**/*.min.js'

###############
# Set the Job #
###############
jobs:
build:
strategy:
fail-fast: false
# matrix:
# language:
# Name the Job
name: Lint Code Base
# Set the agent to run on
runs-on: ubuntu-latest

permissions:
contents: read
packages: read
# To report GitHub Actions status checks
statuses: write


##################
# Load all steps #
##################
Expand All @@ -43,39 +59,53 @@ jobs:
##########################
- name: Checkout Code
uses: actions/checkout@v4
with:
# super-linter needs the full git history to get the
# list of files that changed across commits
fetch-depth: 0

################################
# Run Linter against code base #
################################
- name: Lint Code Base
uses: docker://github/super-linter:v4
uses: super-linter/super-linter@v7.1.0
env:
LINTER_RULES_PATH: /
JAVASCRIPT_ES_CONFIG_FILE: .eslintrc.json
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VALIDATE_ALL_CODEBASE: true
DEFAULT_BRANCH: master
VALIDATE_YAML: true
# VALIDATE_JSON: false
# DEFAULT_BRANCH: master
# DEFAULT_WORKSPACE:
# ANSIBLE_DIRECTORY:
# ACTIONS_RUNNER_DEBUG: true
VALIDATE_ALL_CODEBASE: false
IGNORE_GENERATED_FILES: true
IGNORE_GITIGNORED_FILES: true
SUPPRESS_POSSUM: true
LOG_LEVEL: ERROR
LINTER_RULES_PATH: './'
EDITORCONFIG_FILE_NAME: '.editorconfig'
# JAVASCRIPT_ES_CONFIG_FILE: 'eslint.config.js'
CSS_FILE_NAME: '.stylelintrc.json'

# Valate Languages - Uncomment to Enable
# JS/TS/JSON Disabled until ESLint in super-linter is updated
VALIDATE_HTML: true
VALIDATE_CSS: true
# VALIDATE_JAVASCRIPT_ES: true
# VALIDATE_JAVASCRIPT_STANDARD: true
# VALIDATE_JSON: true
# VALIDATE_XML: true
VALIDATE_MD: true
# VALIDATE_HTML: true
VALIDATE_MARKDOWN: true
VALIDATE_YAML: true
# VALIDATE_TYPESCRIPT_ES: true
# VALIDATE_TYPESCRIPT_STANDARD: true
# VALIDATE_JSX: true
# VALIDATE_TSX: true
# VALIDATE_BASH: true
# VALIDATE_PERL: true
# VALIDATE_PHP_BUILTIN: true
# VALIDATE_PYTHON: true
# VALIDATE_RUBY: true
# VALIDATE_COFFEE: true
# VALIDATE_ANSIBLE: true
# VALIDATE_JAVASCRIPT_ES: true
# VALIDATE_JAVASCRIPT_STANDARD: true
# VALIDATE_TYPESCRIPT_ES: true
# VALIDATE_TYPESCRIPT_STANDARD: true
# VALIDATE_DOCKER: true
# VALIDATE_GO: true
# VALIDATE_TERRAFORM: true
# VALIDATE_CSS: true
# ANSIBLE_DIRECTORY:
# ACTIONS_RUNNER_DEBUG: true
# DEFAULT_WORKSPACE:

1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ test/
.nvmrc
*.tgz
*.log
*.test.js
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20.3.1
20.9.0
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- markdownlint-disable -->
# Changelog
All notable changes to this project will be documented in this file.

Expand All @@ -6,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [v1.0.8] - 2024-09-17

### Added
- Add `exports` to package
- Add `.test.js` / `node --test` testing

### Changed
- Update hashing output to use `Uint8Array.prototype.(toHex|toBase64)`
- Update `md5()` to use `ArrayBuffer`-based techniquess and be consistent with `crypto.subtle.digest`
- The `hash()` function now supports a wider variety if inputs/data

## [v1.0.7] - 2024-07-16

### Added
Expand Down
4 changes: 2 additions & 2 deletions base64.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export async function base64Encode(thing) {
} else if (thing instanceof Blob || thing instanceof Request || thing instanceof Response) {
return await thing.bytes().then(bytes => bytes.toBase64());
} else if (thing instanceof URL) {
return await btoa(thing.href);
return btoa(thing.href);
} else if (thing instanceof Uint8Array) {
return thing.toBase64();
} else {
return await btoa(thing.toString());
return btoa(thing.toString());
}

case 'undefined':
Expand Down
4 changes: 4 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { ignoreFile } from '@shgysk8zer0/eslint-config/ignoreFile.js';
import browser from '@shgysk8zer0/eslint-config/browser.js';

export default [ignoreFile, browser()];
99 changes: 68 additions & 31 deletions hash.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
/**
* @copyright 2021-2023 Chris Zuber <[email protected]>
* @copyright 2021-2024 Chris Zuber <[email protected]>
*/
export const MD5 = 'md5';
export const SHA_1 = 'SHA-1';
export const SHA_256 = 'SHA-256';
export const SHA_384 = 'SHA-384';
export const SHA_512 = 'SHA-512';
export const SHA = SHA_1; // Alias of SHA_1
export const HEX = 'hex';
export const BASE_64 = 'base64';
export const ARRAY_BUFFER = 'ArrayBuffer';
export const BASE_64_URL = 'base64url';
export const ARRAY_BUFFER = 'arraybuffer';
export const UINT8_ARRAY = 'uint8array';
export const SRI = 'sri';
export { md5 } from './md5.js';
export const DEFAULT_OUTPUT = HEX;
import { md5 as md5Hash } from './md5.js';
export const DEFAULT_OUTPUT = HEX;

export function bufferToHex(buffer) {
if (! (buffer instanceof ArrayBuffer)) {
throw new TypeError('`bufferToHex()` requires an ArrayBuffer');
} else {
return [...new Uint8Array(buffer)].map(value => value.toString(16).padStart(2, '0')).join('');
return new Uint8Array(buffer).toHex();
}
}

Expand All @@ -29,55 +32,89 @@ export function hexToBuffer(hex) {
} else if (hex.length % 2 !== 0) {
throw new TypeError('Strings must be an even length');
} else {
return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))).buffer;
return Uint8Array.fromHex(hex).buffer;
}
}

export function sha1(data, { output = DEFAULT_OUTPUT } = {}) {
return hash(data, { algorithm: SHA_1, output });
export async function sha1(data, { output = DEFAULT_OUTPUT, signal } = {}) {
return hash(data, { algorithm: SHA_1, output, signal });
}

export function sha256(data, { output = DEFAULT_OUTPUT } = {}) {
return hash(data, { algorithm: SHA_256, output });
export async function sha256(data, { output = DEFAULT_OUTPUT, signal } = {}) {
return hash(data, { algorithm: SHA_256, output, signal });
}

export function sha384(data, { output = DEFAULT_OUTPUT } = {}) {
return hash(data, { algorithm: SHA_384, output });
export async function sha384(data, { output = DEFAULT_OUTPUT, signal } = {}) {
return hash(data, { algorithm: SHA_384, output, signal });
}

export function sha512(data, { output = DEFAULT_OUTPUT } = {}) {
return hash(data, { algorithm: SHA_512, output });
export async function sha512(data, { output = DEFAULT_OUTPUT, signal } = {}) {
return hash(data, { algorithm: SHA_512, output, signal });
}

export async function hash(data, { algorithm = SHA_256, output = DEFAULT_OUTPUT } = {}) {
if (data instanceof File) {
return hash(await data.arrayBuffer(), { algorithm, output });
} else if (typeof data === 'string') {
return hash(new TextEncoder().encode(data), { algorithm, output });
} else if (typeof data === 'undefined') {
throw new TypeError('Cannot hash `undefined`');
} else if (data instanceof ArrayBuffer || data.buffer instanceof ArrayBuffer) {
const buffer = await crypto.subtle.digest(algorithm.toUpperCase(), data);
export async function md5(data, { output = DEFAULT_OUTPUT, signal } = {}) {
return hash(data, { algorithm: MD5, output, signal });
}

/**
* Calculates the hash of the provided data using the specified algorithm.
*
* @param {string|Blob|Response|Request|ArrayBuffer|ArrayBufferView|Promise<string|ArrayBuffer|Blob|ArrayBufferView|Response>} data The data to hash.
* @param {object} options (optional)
* @param {string} [options.algorithm='SHA-256'] The hashing algorithm (defaults to `SHA_256`).
* @param {string} [options.output='hex'] The desired output format (defaults to `DEFAULT_OUTPUT`).
* @param {AbortSignal} [options.signal] An optional `AbortSignal` to cancel the operation.
* @returns {Promise<string|ArrayBuffer|Uint8Array>} The hash in the specified format.
* @throws {TypeError} If the data type is not supported, the output format is invalid, or the algorithm is not supported.
* @throws {Error} If the request fails (for `Request` type).
*/
export async function hash(data, { algorithm = SHA_256, output = DEFAULT_OUTPUT, signal } = {}) {
if (signal instanceof AbortSignal && signal.aborted) {
throw signal.reason;
} else if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
const buffer = algorithm.toLowerCase() === MD5 ? await md5Hash(data) : await crypto.subtle.digest(algorithm.toUpperCase(), data);

switch (output.toLowerCase()) {
case HEX:
return bufferToHex(buffer);
return new Uint8Array(buffer).toHex();

case BASE_64:
return btoa(buffer);
case BASE_64_URL:
return new Uint8Array(buffer).toBase64({ alphabet: output });

case SRI: {
const codeUnits = new Uint16Array(buffer);
const charCodes = new Uint8Array(codeUnits.buffer);
const result = btoa(String.fromCharCode(...charCodes));
return `${algorithm.replace('-', '').toLowerCase()}-${result}`;
}
case SRI:
return `${algorithm.replace('-', '').toLowerCase()}-${new Uint8Array(buffer).toBase64({ alphabet: BASE_64 })}`;

case ARRAY_BUFFER:
return buffer;

case UINT8_ARRAY:
return new Uint8Array(buffer);

default:
throw new TypeError(`Unsupported output format: '${output}'`);
}
} else if (data instanceof Blob) {
return hash(await data.arrayBuffer(), { algorithm, output, signal });
} else if (typeof data === 'string') {
return hash(new TextEncoder().encode(data), { algorithm, output, signal });
} else if (data instanceof Response) {
return hash(await data.clone().arrayBuffer(), { algorithm, output, signal });
} else if (data instanceof Request) {
const resp = await fetch(data);

if (resp.ok) {
return await hash(await resp.arrayBuffer(), { algorithm, output, signal: signal ?? data.signal });
} else {
throw new DOMException(`${resp.url} [${resp.status} ${resp.statusText}]`, 'NetworkError');
}
} else if (data instanceof Promise) {
return await hash(await data, { algorithm, output, signal });
} else {
throw new TypeError('Invalid data to hash.');
}
}

export async function getIntegrity(data, { algorithm = SHA_256, signal } = {}) {
return await hash(data, { algorithm, output: SRI, signal });
}
2 changes: 2 additions & 0 deletions immutable.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* global Record Tuple */
export function deepFreeze(thing) {
switch(typeof thing) {
case 'function':
Expand Down Expand Up @@ -35,6 +36,7 @@ export function getImmutable(thing) {
} else if (Array.isArray(thing)) {
return Tuple.from(thing.map(getImmutable));
} else {

return Record.fromEntries(
Object.entries(thing)
.map(([key, val]) => [key, getImmutable(val)])
Expand Down
Loading

0 comments on commit e169be7

Please sign in to comment.