@lowdep/hashsum 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/bin/hashsum.js +194 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rushabh Shah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# hashsum
|
|
2
|
+
|
|
3
|
+
   
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Compute and verify file checksums with one consistent interface on every OS. Supports MD5, SHA-1, SHA-256, and SHA-512. Zero dependencies.
|
|
7
|
+
|
|
8
|
+
No more remembering `sha256sum` (Linux) vs `shasum -a 256` (macOS) vs `certutil -hashfile` / `Get-FileHash` (Windows) — all of which print different formats.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g hashsum
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or without installing:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx hashsum file.zip
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
hashsum file.zip # SHA-256 (default)
|
|
30
|
+
hashsum *.iso -a sha512 # SHA-512 of several files
|
|
31
|
+
hashsum dist/* --save # Write sha256sums.txt
|
|
32
|
+
hashsum -c sha256sums.txt # Verify files against the list
|
|
33
|
+
hashsum --string "hello" -a md5 # Hash a literal string
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Output Format
|
|
39
|
+
|
|
40
|
+
Matches `sha256sum`, so the files are interoperable:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
$ hashsum archive.tar.gz
|
|
44
|
+
9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 archive.tar.gz
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Verify Downloads
|
|
50
|
+
|
|
51
|
+
Generate a checksum list:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
hashsum *.iso -a sha256 --save # → sha256sums.txt
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Verify later (the algorithm is auto-detected from each hash's length):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
$ hashsum -c sha256sums.txt
|
|
61
|
+
|
|
62
|
+
hashsum verifying 3 file(s) from sha256sums.txt
|
|
63
|
+
|
|
64
|
+
✓ OK ubuntu.iso
|
|
65
|
+
✗ FAILED corrupted.iso
|
|
66
|
+
? MISSING deleted.iso
|
|
67
|
+
|
|
68
|
+
1 OK · 1 failed · 1 missing
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Exit code is `1` if any file fails or is missing — so it works in CI and scripts.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Supported Algorithms
|
|
76
|
+
|
|
77
|
+
| Algorithm | Hash length | Notes |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `md5` | 32 hex | Fast, **not** for security |
|
|
80
|
+
| `sha1` | 40 hex | Legacy |
|
|
81
|
+
| `sha256` | 64 hex | **Default** — the modern standard |
|
|
82
|
+
| `sha512` | 128 hex | Stronger / faster on 64-bit |
|
|
83
|
+
|
|
84
|
+
When verifying with `-c`, the algorithm is inferred from each hash's length, so a mixed file works.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Options
|
|
89
|
+
|
|
90
|
+
| Flag | Description |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `-a, --algo <name>` | `md5` \| `sha1` \| `sha256` \| `sha512` |
|
|
93
|
+
| `-c, --check <file>` | Verify files listed in a checksums file |
|
|
94
|
+
| `--string <text>` | Hash a string instead of a file |
|
|
95
|
+
| `--save` | Write `<algo>sums.txt` |
|
|
96
|
+
| `--json` | JSON output |
|
|
97
|
+
| `--quiet` | Print only the hash / exit code |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Keywords
|
|
108
|
+
|
|
109
|
+
`checksum` · `sha256` · `md5` · `file hash` · `sha256sum windows` · `verify checksum` · `sha512` · `integrity check` · `cross-platform` · `zero dependencies`
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
<div align="center">
|
|
114
|
+
|
|
115
|
+
**Built to solve, shared to help — Rushabh Shah 🛠️✨**
|
|
116
|
+
|
|
117
|
+
<sub>One of 40+ zero-dependency developer CLI tools — no <code>node_modules</code>, ever.</sub>
|
|
118
|
+
|
|
119
|
+
</div>
|
package/bin/hashsum.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
const VERSION = '1.0.0';
|
|
9
|
+
|
|
10
|
+
// ─── ANSI ─────────────────────────────────────────────────────────────────────
|
|
11
|
+
const isTTY = process.stdout.isTTY;
|
|
12
|
+
const c = (code, t) => isTTY ? `\x1b[${code}m${t}\x1b[0m` : t;
|
|
13
|
+
const bold = t => c('1', t);
|
|
14
|
+
const dim = t => c('2', t);
|
|
15
|
+
const red = t => c('31', t);
|
|
16
|
+
const green = t => c('32', t);
|
|
17
|
+
const yellow = t => c('33', t);
|
|
18
|
+
const cyan = t => c('36', t);
|
|
19
|
+
|
|
20
|
+
const ALGOS = ['md5', 'sha1', 'sha256', 'sha512'];
|
|
21
|
+
|
|
22
|
+
// ─── Hash a file (streaming) ──────────────────────────────────────────────────
|
|
23
|
+
function hashFile(filePath, algo) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const hash = crypto.createHash(algo);
|
|
26
|
+
const stream = fs.createReadStream(filePath);
|
|
27
|
+
stream.on('data', d => hash.update(d));
|
|
28
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
29
|
+
stream.on('error', reject);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hashString(str, algo) {
|
|
34
|
+
return crypto.createHash(algo).update(str).digest('hex');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Parse a checksums file ───────────────────────────────────────────────────
|
|
38
|
+
// Lines look like: <hex> <filename> (two spaces, like sha256sum)
|
|
39
|
+
// or: <hex> *<filename> (binary mode marker)
|
|
40
|
+
function parseChecksumFile(content) {
|
|
41
|
+
const entries = [];
|
|
42
|
+
for (const line of content.split(/\r?\n/)) {
|
|
43
|
+
const trimmed = line.trim();
|
|
44
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
45
|
+
const m = trimmed.match(/^([0-9a-fA-F]{32,128})\s+\*?(.+)$/);
|
|
46
|
+
if (m) entries.push({ hash: m[1].toLowerCase(), file: m[2].trim() });
|
|
47
|
+
}
|
|
48
|
+
return entries;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function algoForHashLength(hex) {
|
|
52
|
+
switch (hex.length) {
|
|
53
|
+
case 32: return 'md5';
|
|
54
|
+
case 40: return 'sha1';
|
|
55
|
+
case 64: return 'sha256';
|
|
56
|
+
case 128: return 'sha512';
|
|
57
|
+
default: return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
62
|
+
const args = process.argv.slice(2);
|
|
63
|
+
const VALUE_FLAGS = new Set(['-a', '--algo', '-c', '--check', '--string']);
|
|
64
|
+
|
|
65
|
+
function getFlag(...names) { for (const f of names) { const i = args.indexOf(f); if (i !== -1) return args[i + 1]; } return null; }
|
|
66
|
+
function hasFlag(...names) { return names.some(f => args.includes(f)); }
|
|
67
|
+
|
|
68
|
+
const positional = [];
|
|
69
|
+
for (let i = 0; i < args.length; i++) {
|
|
70
|
+
if (args[i].startsWith('-') && args[i] !== '-') { if (VALUE_FLAGS.has(args[i])) i++; }
|
|
71
|
+
else positional.push(args[i]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (hasFlag('--version', '-v')) { console.log(`hashsum v${VERSION}`); process.exit(0); }
|
|
75
|
+
|
|
76
|
+
if (hasFlag('--help', '-h') || (!positional.length && !getFlag('-c', '--check') && getFlag('--string') === null)) {
|
|
77
|
+
console.log(`
|
|
78
|
+
${bold('hashsum')} — Compute and verify file checksums, cross-platform, zero deps
|
|
79
|
+
|
|
80
|
+
${bold('USAGE')}
|
|
81
|
+
hashsum [file...] [options]
|
|
82
|
+
hashsum -c <checksums-file> Verify files against a checksum list
|
|
83
|
+
hashsum --string "text" -a sha256 Hash a literal string
|
|
84
|
+
|
|
85
|
+
${bold('OPTIONS')}
|
|
86
|
+
-a, --algo <name> md5 | sha1 | sha256 (default) | sha512
|
|
87
|
+
-c, --check <file> Verify files listed in a checksums file
|
|
88
|
+
--string <text> Hash a string instead of a file
|
|
89
|
+
--save Write "<hash> <file>" lines to <algo>sums.txt
|
|
90
|
+
--json JSON output
|
|
91
|
+
--quiet Minimal output (just results / exit code)
|
|
92
|
+
--version Show version
|
|
93
|
+
|
|
94
|
+
${bold('EXAMPLES')}
|
|
95
|
+
hashsum file.zip # sha256 of one file
|
|
96
|
+
hashsum *.iso -a sha512 # sha512 of several files
|
|
97
|
+
hashsum dist/* --save # write sha256sums.txt
|
|
98
|
+
hashsum -c sha256sums.txt # verify against the list
|
|
99
|
+
hashsum --string "hello" -a md5 # hash a string
|
|
100
|
+
|
|
101
|
+
${bold('OUTPUT FORMAT')} matches sha256sum: ${dim('<hex> <filename>')}
|
|
102
|
+
`);
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const algo = (getFlag('-a', '--algo') || 'sha256').toLowerCase();
|
|
107
|
+
const checkFile = getFlag('-c', '--check');
|
|
108
|
+
const strInput = getFlag('--string');
|
|
109
|
+
const save = hasFlag('--save');
|
|
110
|
+
const asJson = hasFlag('--json');
|
|
111
|
+
const quiet = hasFlag('--quiet');
|
|
112
|
+
|
|
113
|
+
if (!checkFile && !ALGOS.includes(algo)) {
|
|
114
|
+
console.error(red(`\nUnknown algorithm: ${algo}. Use: ${ALGOS.join(', ')}\n`)); process.exit(2);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
(async () => {
|
|
118
|
+
// ── Verify mode ──
|
|
119
|
+
if (checkFile) {
|
|
120
|
+
if (!fs.existsSync(checkFile)) { console.error(red(`\nChecksums file not found: ${checkFile}\n`)); process.exit(2); }
|
|
121
|
+
const baseDir = path.dirname(path.resolve(checkFile));
|
|
122
|
+
const entries = parseChecksumFile(fs.readFileSync(checkFile, 'utf8'));
|
|
123
|
+
if (!entries.length) { console.error(yellow('\nNo checksum entries found in file.\n')); process.exit(2); }
|
|
124
|
+
|
|
125
|
+
if (!quiet && !asJson) console.log(`\n${bold('hashsum')} ${dim('verifying ' + entries.length + ' file(s) from ' + path.basename(checkFile))}\n`);
|
|
126
|
+
|
|
127
|
+
let ok = 0, failed = 0, missing = 0;
|
|
128
|
+
const results = [];
|
|
129
|
+
for (const e of entries) {
|
|
130
|
+
const filePath = path.resolve(baseDir, e.file);
|
|
131
|
+
const useAlgo = algoForHashLength(e.hash) || algo;
|
|
132
|
+
let status;
|
|
133
|
+
if (!fs.existsSync(filePath)) { status = 'missing'; missing++; }
|
|
134
|
+
else {
|
|
135
|
+
const actual = await hashFile(filePath, useAlgo);
|
|
136
|
+
status = actual === e.hash ? 'ok' : 'failed';
|
|
137
|
+
if (status === 'ok') ok++; else failed++;
|
|
138
|
+
}
|
|
139
|
+
results.push({ file: e.file, status, algo: useAlgo });
|
|
140
|
+
if (!quiet && !asJson) {
|
|
141
|
+
const sym = status === 'ok' ? green('✓ OK') : status === 'missing' ? yellow('? MISSING') : red('✗ FAILED');
|
|
142
|
+
console.log(` ${sym} ${e.file}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (asJson) {
|
|
147
|
+
console.log(JSON.stringify({ checked: entries.length, ok, failed, missing, results }, null, 2));
|
|
148
|
+
} else if (!quiet) {
|
|
149
|
+
console.log();
|
|
150
|
+
const parts = [green(`${ok} OK`)];
|
|
151
|
+
if (failed) parts.push(red(`${failed} failed`));
|
|
152
|
+
if (missing) parts.push(yellow(`${missing} missing`));
|
|
153
|
+
console.log(` ${parts.join(' · ')}\n`);
|
|
154
|
+
}
|
|
155
|
+
process.exit(failed > 0 || missing > 0 ? 1 : 0);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── String mode ──
|
|
159
|
+
if (strInput !== null) {
|
|
160
|
+
const digest = hashString(strInput, algo);
|
|
161
|
+
if (asJson) console.log(JSON.stringify({ algo, input: strInput, hash: digest }, null, 2));
|
|
162
|
+
else console.log(quiet ? digest : `${digest} ${dim('(' + algo + ' of string)')}`);
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Compute mode ──
|
|
167
|
+
if (!positional.length) { console.error(red('\nNo files given.\n')); process.exit(1); }
|
|
168
|
+
|
|
169
|
+
const lines = [];
|
|
170
|
+
const jsonOut = [];
|
|
171
|
+
let hadError = false;
|
|
172
|
+
|
|
173
|
+
for (const p of positional) {
|
|
174
|
+
const filePath = path.resolve(p);
|
|
175
|
+
if (!fs.existsSync(filePath)) { console.error(red(`hashsum: ${p}: no such file`)); hadError = true; continue; }
|
|
176
|
+
if (fs.statSync(filePath).isDirectory()) { console.error(yellow(`hashsum: ${p}: is a directory (skipped)`)); continue; }
|
|
177
|
+
const digest = await hashFile(filePath, algo);
|
|
178
|
+
lines.push(`${digest} ${p}`);
|
|
179
|
+
jsonOut.push({ file: p, algo, hash: digest });
|
|
180
|
+
if (!save && !asJson) {
|
|
181
|
+
console.log(quiet ? digest : `${cyan(digest)} ${p}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (asJson) { console.log(JSON.stringify(jsonOut, null, 2)); process.exit(hadError ? 1 : 0); }
|
|
186
|
+
|
|
187
|
+
if (save) {
|
|
188
|
+
const outName = `${algo}sums.txt`;
|
|
189
|
+
fs.writeFileSync(outName, lines.join('\n') + '\n');
|
|
190
|
+
console.log(`${green('✓')} Wrote ${lines.length} checksum(s) to ${cyan(outName)}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
process.exit(hadError ? 1 : 0);
|
|
194
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lowdep/hashsum",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Compute and verify file checksums (md5/sha1/sha256/sha512) with a consistent cross-platform interface — zero dependencies",
|
|
5
|
+
"bin": {
|
|
6
|
+
"hashsum": "bin/hashsum.js"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [
|
|
9
|
+
"checksum",
|
|
10
|
+
"hash",
|
|
11
|
+
"sha256",
|
|
12
|
+
"md5",
|
|
13
|
+
"sha512",
|
|
14
|
+
"verify",
|
|
15
|
+
"integrity",
|
|
16
|
+
"cli",
|
|
17
|
+
"cross-platform",
|
|
18
|
+
"zero-dependencies",
|
|
19
|
+
"file hash",
|
|
20
|
+
"sha256sum windows",
|
|
21
|
+
"verify checksum",
|
|
22
|
+
"integrity check",
|
|
23
|
+
"zero dependencies"
|
|
24
|
+
],
|
|
25
|
+
"author": "Rushabh Shah",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=14"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"bin/"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/Rushabh5000/hashsum.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/Rushabh5000/hashsum/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/Rushabh5000/hashsum#readme",
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|