Skip to content
This repository was archived by the owner on Feb 12, 2024. It is now read-only.

Commit 04e3cf3

Browse files
authoredSep 29, 2021
fix: do not accept single items for ipfs.add (#3900)
The types allow passing single items to `ipfs.addAll` and multiple items to `ipfs.add`. Instead, only accept single items to `ipfs.add` and streams of item to `ipfs.addAll` and fail with a more helpful error message if you do not do this. BREAKING CHANGE: errors will now be thrown if multiple items are passed to `ipfs.add` or single items to `ipfs.addAll` (n.b. you can still pass a list of a single item to `ipfs.addAll`)
1 parent 5ddd0c5 commit 04e3cf3

36 files changed

+688
-146
lines changed
 

‎packages/interface-ipfs-core/src/add-all.js

+20
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,26 @@ export function testAddAll (factory, options) {
301301
await expect(all(ipfs.addAll(nonValid))).to.eventually.be.rejected()
302302
})
303303

304+
it('should fail when passed single file objects', async () => {
305+
const nonValid = { content: 'hello world' }
306+
307+
// @ts-expect-error nonValid is non valid
308+
await expect(all(ipfs.addAll(nonValid))).to.eventually.be.rejectedWith(/single item passed/)
309+
})
310+
311+
it('should fail when passed single strings', async () => {
312+
const nonValid = 'hello world'
313+
314+
await expect(all(ipfs.addAll(nonValid))).to.eventually.be.rejectedWith(/single item passed/)
315+
})
316+
317+
it('should fail when passed single buffers', async () => {
318+
const nonValid = uint8ArrayFromString('hello world')
319+
320+
// @ts-expect-error nonValid is non valid
321+
await expect(all(ipfs.addAll(nonValid))).to.eventually.be.rejectedWith(/single item passed/)
322+
})
323+
304324
it('should wrap content in a directory', async () => {
305325
const data = { path: 'testfile.txt', content: fixtures.smallFile.data }
306326

‎packages/interface-ipfs-core/src/add.js

+7
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,13 @@ export function testAdd (factory, options) {
244244
await expect(ipfs.add(null)).to.eventually.be.rejected()
245245
})
246246

247+
it('should fail when passed multiple file objects', async () => {
248+
const nonValid = [{ content: 'hello' }, { content: 'world' }]
249+
250+
// @ts-expect-error nonValid is non valid
251+
await expect(ipfs.add(nonValid)).to.eventually.be.rejectedWith(/multiple items passed/)
252+
})
253+
247254
it('should wrap content in a directory', async () => {
248255
const data = { path: 'testfile.txt', content: fixtures.smallFile.data }
249256

‎packages/ipfs-cli/src/parser.js

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import yargs from 'yargs'
32
import { ipfsPathHelp, disablePrinting } from './utils.js'
43
import { commandList } from './commands/index.js'

‎packages/ipfs-core-utils/package.json

+10-4
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,17 @@
3838
".": {
3939
"import": "./src/index.js"
4040
},
41-
"./files/normalise-input": {
42-
"import": "./src/files/normalise-input.js"
41+
"./files/normalise-input-single": {
42+
"import": "./src/files/normalise-input-single.js"
4343
},
44-
"./files/normalise-input.browser": {
45-
"import": "./src/files/normalise-input.browser.js"
44+
"./files/normalise-input-single.browser": {
45+
"import": "./src/files/normalise-input-single.browser.js"
46+
},
47+
"./files/normalise-input-multiple": {
48+
"import": "./src/files/normalise-input-multiple.js"
49+
},
50+
"./files/normalise-input-multiple.browser": {
51+
"import": "./src/files/normalise-input-multiple.browser.js"
4652
},
4753
"./files/normalise-content": {
4854
"import": "./src/files/normalise-content.js"

‎packages/ipfs-core-utils/src/files/normalise.js renamed to ‎packages/ipfs-core-utils/src/files/normalise-candidate-multiple.js

+16-29
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,25 @@ import {
1414
} from 'ipfs-unixfs'
1515

1616
/**
17+
* @typedef {import('ipfs-core-types/src/utils').ImportCandidate} ImportCandidate
1718
* @typedef {import('ipfs-core-types/src/utils').ToContent} ToContent
1819
* @typedef {import('ipfs-unixfs-importer').ImportCandidate} ImporterImportCandidate
19-
* @typedef {import('ipfs-core-types/src/utils').ImportCandidate} ImportCandidate
2020
* @typedef {import('ipfs-core-types/src/utils').ImportCandidateStream} ImportCandidateStream
2121
*/
2222

2323
/**
24-
* @param {ImportCandidate | ImportCandidateStream} input
24+
* @param {ImportCandidateStream} input
2525
* @param {(content:ToContent) => Promise<AsyncIterable<Uint8Array>>} normaliseContent
2626
*/
2727
// eslint-disable-next-line complexity
28-
export async function * normalise (input, normaliseContent) {
29-
if (input === null || input === undefined) {
30-
throw errCode(new Error(`Unexpected input: ${input}`), 'ERR_UNEXPECTED_INPUT')
31-
}
32-
28+
export async function * normaliseCandidateMultiple (input, normaliseContent) {
3329
// String
34-
if (typeof input === 'string' || input instanceof String) {
35-
yield toFileObject(input.toString(), normaliseContent)
36-
return
37-
}
38-
3930
// Uint8Array|ArrayBuffer|TypedArray
4031
// Blob|File
41-
if (isBytes(input) || isBlob(input)) {
42-
yield toFileObject(input, normaliseContent)
43-
return
32+
// fs.ReadStream
33+
// @ts-expect-error _readableState is a property of a node fs.ReadStream
34+
if (typeof input === 'string' || input instanceof String || isBytes(input) || isBlob(input) || input._readableState) {
35+
throw errCode(new Error('Unexpected input: single item passed - if you are using ipfs.allAll, please use ipfs.add instead'), 'ERR_UNEXPECTED_INPUT')
4436
}
4537

4638
// Browser ReadableStream
@@ -67,42 +59,37 @@ export async function * normalise (input, normaliseContent) {
6759

6860
// (Async)Iterable<Number>
6961
// (Async)Iterable<Bytes>
70-
if (Number.isInteger(value) || isBytes(value)) {
71-
yield toFileObject(peekable, normaliseContent)
72-
return
62+
if (Number.isInteger(value)) {
63+
throw errCode(new Error('Unexpected input: single item passed - if you are using ipfs.allAll, please use ipfs.add instead'), 'ERR_UNEXPECTED_INPUT')
7364
}
7465

75-
// fs.ReadStream<Bytes>
66+
// (Async)Iterable<fs.ReadStream>
7667
if (value._readableState) {
77-
// @ts-ignore Node readable streams have a `.path` property so we need to pass it as the content
68+
// @ts-ignore Node fs.ReadStreams have a `.path` property so we need to pass it as the content
7869
yield * map(peekable, (/** @type {ImportCandidate} */ value) => toFileObject({ content: value }, normaliseContent))
7970
return
8071
}
8172

82-
// (Async)Iterable<Blob>
83-
// (Async)Iterable<String>
84-
// (Async)Iterable<{ path, content }>
85-
if (isFileObject(value) || isBlob(value) || typeof value === 'string' || value instanceof String) {
86-
yield * map(peekable, (/** @type {ImportCandidate} */ value) => toFileObject(value, normaliseContent))
73+
if (isBytes(value)) {
74+
yield toFileObject({ content: peekable }, normaliseContent)
8775
return
8876
}
8977

9078
// (Async)Iterable<(Async)Iterable<?>>
9179
// (Async)Iterable<ReadableStream<?>>
9280
// ReadableStream<(Async)Iterable<?>>
9381
// ReadableStream<ReadableStream<?>>
94-
if (value[Symbol.iterator] || value[Symbol.asyncIterator] || isReadableStream(value)) {
82+
if (isFileObject(value) || value[Symbol.iterator] || value[Symbol.asyncIterator] || isReadableStream(value) || isBlob(value)) {
9583
yield * map(peekable, (/** @type {ImportCandidate} */ value) => toFileObject(value, normaliseContent))
9684
return
9785
}
9886
}
9987

10088
// { path, content: ? }
101-
// Note: Detected _after_ (Async)Iterable<?> because Node.js streams have a
89+
// Note: Detected _after_ (Async)Iterable<?> because Node.js fs.ReadStreams have a
10290
// `path` property that passes this check.
10391
if (isFileObject(input)) {
104-
yield toFileObject(input, normaliseContent)
105-
return
92+
throw errCode(new Error('Unexpected input: single item passed - if you are using ipfs.allAll, please use ipfs.add instead'), 'ERR_UNEXPECTED_INPUT')
10693
}
10794

10895
throw errCode(new Error('Unexpected input: ' + typeof input), 'ERR_UNEXPECTED_INPUT')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import errCode from 'err-code'
2+
import browserStreamToIt from 'browser-readablestream-to-it'
3+
import itPeekable from 'it-peekable'
4+
import {
5+
isBytes,
6+
isBlob,
7+
isReadableStream,
8+
isFileObject
9+
} from './utils.js'
10+
import {
11+
parseMtime,
12+
parseMode
13+
} from 'ipfs-unixfs'
14+
15+
/**
16+
* @typedef {import('ipfs-core-types/src/utils').ToContent} ToContent
17+
* @typedef {import('ipfs-unixfs-importer').ImportCandidate} ImporterImportCandidate
18+
* @typedef {import('ipfs-core-types/src/utils').ImportCandidate} ImportCandidate
19+
* @typedef {import('ipfs-core-types/src/utils').ImportCandidateStream} ImportCandidateStream
20+
*/
21+
22+
/**
23+
* @param {ImportCandidate} input
24+
* @param {(content:ToContent) => Promise<AsyncIterable<Uint8Array>>} normaliseContent
25+
*/
26+
// eslint-disable-next-line complexity
27+
export async function * normaliseCandidateSingle (input, normaliseContent) {
28+
if (input === null || input === undefined) {
29+
throw errCode(new Error(`Unexpected input: ${input}`), 'ERR_UNEXPECTED_INPUT')
30+
}
31+
32+
// String
33+
if (typeof input === 'string' || input instanceof String) {
34+
yield toFileObject(input.toString(), normaliseContent)
35+
return
36+
}
37+
38+
// Uint8Array|ArrayBuffer|TypedArray
39+
// Blob|File
40+
if (isBytes(input) || isBlob(input)) {
41+
yield toFileObject(input, normaliseContent)
42+
return
43+
}
44+
45+
// Browser ReadableStream
46+
if (isReadableStream(input)) {
47+
input = browserStreamToIt(input)
48+
}
49+
50+
// Iterable<?>
51+
if (Symbol.iterator in input || Symbol.asyncIterator in input) {
52+
// @ts-ignore it's (async)iterable
53+
const peekable = itPeekable(input)
54+
55+
/** @type {any} value **/
56+
const { value, done } = await peekable.peek()
57+
58+
if (done) {
59+
// make sure empty iterators result in empty files
60+
yield { content: [] }
61+
return
62+
}
63+
64+
peekable.push(value)
65+
66+
// (Async)Iterable<Number>
67+
// (Async)Iterable<Bytes>
68+
// (Async)Iterable<String>
69+
if (Number.isInteger(value) || isBytes(value) || typeof value === 'string' || value instanceof String) {
70+
yield toFileObject(peekable, normaliseContent)
71+
return
72+
}
73+
74+
throw errCode(new Error('Unexpected input: multiple items passed - if you are using ipfs.add, please use ipfs.addAll instead'), 'ERR_UNEXPECTED_INPUT')
75+
}
76+
77+
// { path, content: ? }
78+
// Note: Detected _after_ (Async)Iterable<?> because Node.js fs.ReadStreams have a
79+
// `path` property that passes this check.
80+
if (isFileObject(input)) {
81+
yield toFileObject(input, normaliseContent)
82+
return
83+
}
84+
85+
throw errCode(new Error('Unexpected input: cannot convert "' + typeof input + '" into ImportCandidate'), 'ERR_UNEXPECTED_INPUT')
86+
}
87+
88+
/**
89+
* @param {ImportCandidate} input
90+
* @param {(content:ToContent) => Promise<AsyncIterable<Uint8Array>>} normaliseContent
91+
*/
92+
async function toFileObject (input, normaliseContent) {
93+
// @ts-ignore - Those properties don't exist on most input types
94+
const { path, mode, mtime, content } = input
95+
96+
/** @type {ImporterImportCandidate} */
97+
const file = {
98+
path: path || '',
99+
mode: parseMode(mode),
100+
mtime: parseMtime(mtime)
101+
}
102+
103+
if (content) {
104+
file.content = await normaliseContent(content)
105+
} else if (!path) { // Not already a file object with path or content prop
106+
// @ts-ignore - input still can be different ToContent
107+
file.content = await normaliseContent(input)
108+
}
109+
110+
return file
111+
}

‎packages/ipfs-core-utils/src/files/normalise-content.browser.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from './utils.js'
1010

1111
/**
12-
* @param {import('./normalise').ToContent} input
12+
* @param {import('ipfs-core-types/src/utils').ToContent} input
1313
*/
1414
export async function normaliseContent (input) {
1515
// Bytes

‎packages/ipfs-core-utils/src/files/normalise-content.js

+12-17
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,29 @@ import {
1212
} from './utils.js'
1313

1414
/**
15-
* @param {import('./normalise').ToContent} input
15+
* @template T
16+
* @param {T} thing
1617
*/
17-
export async function normaliseContent (input) {
18-
return toAsyncGenerator(input)
18+
async function * toAsyncIterable (thing) {
19+
yield thing
1920
}
2021

2122
/**
22-
* @param {import('./normalise').ToContent} input
23+
* @param {import('ipfs-core-types/src/utils').ToContent} input
2324
*/
24-
async function * toAsyncGenerator (input) {
25+
export async function normaliseContent (input) {
2526
// Bytes | String
2627
if (isBytes(input)) {
27-
yield toBytes(input)
28-
return
28+
return toAsyncIterable(toBytes(input))
2929
}
3030

3131
if (typeof input === 'string' || input instanceof String) {
32-
yield toBytes(input.toString())
33-
return
32+
return toAsyncIterable(toBytes(input.toString()))
3433
}
3534

3635
// Blob
3736
if (isBlob(input)) {
38-
yield * blobToIt(input)
39-
return
37+
return blobToIt(input)
4038
}
4139

4240
// Browser stream
@@ -54,22 +52,19 @@ async function * toAsyncGenerator (input) {
5452

5553
if (done) {
5654
// make sure empty iterators result in empty files
57-
yield * []
58-
return
55+
return toAsyncIterable(new Uint8Array(0))
5956
}
6057

6158
peekable.push(value)
6259

6360
// (Async)Iterable<Number>
6461
if (Number.isInteger(value)) {
65-
yield Uint8Array.from((await all(peekable)))
66-
return
62+
return toAsyncIterable(Uint8Array.from(await all(peekable)))
6763
}
6864

6965
// (Async)Iterable<Bytes|String>
7066
if (isBytes(value) || typeof value === 'string' || value instanceof String) {
71-
yield * map(peekable, toBytes)
72-
return
67+
return map(peekable, toBytes)
7368
}
7469
}
7570

Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
11
import { normaliseContent } from './normalise-content.browser.js'
2-
import { normalise } from './normalise.js'
2+
import { normaliseCandidateMultiple } from './normalise-candidate-multiple.js'
33

44
/**
55
* @typedef {import('ipfs-core-types/src/utils').ImportCandidateStream} ImportCandidateStream
6-
* @typedef {import('ipfs-core-types/src/utils').ImportCandidate} ImportCandidate
76
* @typedef {import('ipfs-core-types/src/utils').BrowserImportCandidate} BrowserImportCandidate
87
*/
98

109
/**
11-
* Transforms any of the `ipfs.add` input types into
10+
* Transforms any of the `ipfs.addAll` input types into
1211
*
1312
* ```
1413
* AsyncIterable<{ path, mode, mtime, content: Blob }>
1514
* ```
1615
*
1716
* See https://github.com/ipfs/js-ipfs/blob/master/docs/core-api/FILES.md#ipfsadddata-options
1817
*
19-
* @param {ImportCandidate | ImportCandidateStream} input
18+
* @param {ImportCandidateStream} input
2019
* @returns {AsyncGenerator<BrowserImportCandidate, void, undefined>}
2120
*/
2221
export function normaliseInput (input) {
23-
// @ts-ignore normaliseContent returns Blob and not AsyncIterator
24-
return normalise(input, normaliseContent)
22+
// @ts-expect-error browser normaliseContent returns a Blob not an AsyncIterable<Uint8Array>
23+
return normaliseCandidateMultiple(input, normaliseContent, true)
2524
}

0 commit comments

Comments
 (0)