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

fix: do not accept single items for ipfs.add #3900

Merged
merged 5 commits into from
Sep 29, 2021
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
20 changes: 20 additions & 0 deletions packages/interface-ipfs-core/src/add-all.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,26 @@ export function testAddAll (factory, options) {
await expect(all(ipfs.addAll(nonValid))).to.eventually.be.rejected()
})

it('should fail when passed single file objects', async () => {
const nonValid = { content: 'hello world' }

// @ts-expect-error nonValid is non valid
await expect(all(ipfs.addAll(nonValid))).to.eventually.be.rejectedWith(/single item passed/)
})

it('should fail when passed single strings', async () => {
const nonValid = 'hello world'

await expect(all(ipfs.addAll(nonValid))).to.eventually.be.rejectedWith(/single item passed/)
})

it('should fail when passed single buffers', async () => {
const nonValid = uint8ArrayFromString('hello world')

// @ts-expect-error nonValid is non valid
await expect(all(ipfs.addAll(nonValid))).to.eventually.be.rejectedWith(/single item passed/)
})

it('should wrap content in a directory', async () => {
const data = { path: 'testfile.txt', content: fixtures.smallFile.data }

Expand Down
7 changes: 7 additions & 0 deletions packages/interface-ipfs-core/src/add.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,13 @@ export function testAdd (factory, options) {
await expect(ipfs.add(null)).to.eventually.be.rejected()
})

it('should fail when passed multiple file objects', async () => {
const nonValid = [{ content: 'hello' }, { content: 'world' }]

// @ts-expect-error nonValid is non valid
await expect(ipfs.add(nonValid)).to.eventually.be.rejectedWith(/multiple items passed/)
})

it('should wrap content in a directory', async () => {
const data = { path: 'testfile.txt', content: fixtures.smallFile.data }

Expand Down
1 change: 0 additions & 1 deletion packages/ipfs-cli/src/parser.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import yargs from 'yargs'
import { ipfsPathHelp, disablePrinting } from './utils.js'
import { commandList } from './commands/index.js'
Expand Down
14 changes: 10 additions & 4 deletions packages/ipfs-core-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,17 @@
".": {
"import": "./src/index.js"
},
"./files/normalise-input": {
"import": "./src/files/normalise-input.js"
"./files/normalise-input-single": {
"import": "./src/files/normalise-input-single.js"
},
"./files/normalise-input.browser": {
"import": "./src/files/normalise-input.browser.js"
"./files/normalise-input-single.browser": {
"import": "./src/files/normalise-input-single.browser.js"
},
"./files/normalise-input-multiple": {
"import": "./src/files/normalise-input-multiple.js"
},
"./files/normalise-input-multiple.browser": {
"import": "./src/files/normalise-input-multiple.browser.js"
},
"./files/normalise-content": {
"import": "./src/files/normalise-content.js"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,25 @@ import {
} from 'ipfs-unixfs'

/**
* @typedef {import('ipfs-core-types/src/utils').ImportCandidate} ImportCandidate
* @typedef {import('ipfs-core-types/src/utils').ToContent} ToContent
* @typedef {import('ipfs-unixfs-importer').ImportCandidate} ImporterImportCandidate
* @typedef {import('ipfs-core-types/src/utils').ImportCandidate} ImportCandidate
* @typedef {import('ipfs-core-types/src/utils').ImportCandidateStream} ImportCandidateStream
*/

/**
* @param {ImportCandidate | ImportCandidateStream} input
* @param {ImportCandidateStream} input
* @param {(content:ToContent) => Promise<AsyncIterable<Uint8Array>>} normaliseContent
*/
// eslint-disable-next-line complexity
export async function * normalise (input, normaliseContent) {
if (input === null || input === undefined) {
throw errCode(new Error(`Unexpected input: ${input}`), 'ERR_UNEXPECTED_INPUT')
}

export async function * normaliseCandidateMultiple (input, normaliseContent) {
// String
if (typeof input === 'string' || input instanceof String) {
yield toFileObject(input.toString(), normaliseContent)
return
}

// Uint8Array|ArrayBuffer|TypedArray
// Blob|File
if (isBytes(input) || isBlob(input)) {
yield toFileObject(input, normaliseContent)
return
// fs.ReadStream
// @ts-expect-error _readableState is a property of a node fs.ReadStream
if (typeof input === 'string' || input instanceof String || isBytes(input) || isBlob(input) || input._readableState) {
throw errCode(new Error('Unexpected input: single item passed - if you are using ipfs.allAll, please use ipfs.add instead'), 'ERR_UNEXPECTED_INPUT')
}

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

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

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

// (Async)Iterable<Blob>
// (Async)Iterable<String>
// (Async)Iterable<{ path, content }>
if (isFileObject(value) || isBlob(value) || typeof value === 'string' || value instanceof String) {
yield * map(peekable, (/** @type {ImportCandidate} */ value) => toFileObject(value, normaliseContent))
if (isBytes(value)) {
yield toFileObject({ content: peekable }, normaliseContent)
return
}

// (Async)Iterable<(Async)Iterable<?>>
// (Async)Iterable<ReadableStream<?>>
// ReadableStream<(Async)Iterable<?>>
// ReadableStream<ReadableStream<?>>
if (value[Symbol.iterator] || value[Symbol.asyncIterator] || isReadableStream(value)) {
if (isFileObject(value) || value[Symbol.iterator] || value[Symbol.asyncIterator] || isReadableStream(value) || isBlob(value)) {
yield * map(peekable, (/** @type {ImportCandidate} */ value) => toFileObject(value, normaliseContent))
return
}
}

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

throw errCode(new Error('Unexpected input: ' + typeof input), 'ERR_UNEXPECTED_INPUT')
Expand Down
111 changes: 111 additions & 0 deletions packages/ipfs-core-utils/src/files/normalise-candidate-single.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import errCode from 'err-code'
import browserStreamToIt from 'browser-readablestream-to-it'
import itPeekable from 'it-peekable'
import {
isBytes,
isBlob,
isReadableStream,
isFileObject
} from './utils.js'
import {
parseMtime,
parseMode
} from 'ipfs-unixfs'

/**
* @typedef {import('ipfs-core-types/src/utils').ToContent} ToContent
* @typedef {import('ipfs-unixfs-importer').ImportCandidate} ImporterImportCandidate
* @typedef {import('ipfs-core-types/src/utils').ImportCandidate} ImportCandidate
* @typedef {import('ipfs-core-types/src/utils').ImportCandidateStream} ImportCandidateStream
*/

/**
* @param {ImportCandidate} input
* @param {(content:ToContent) => Promise<AsyncIterable<Uint8Array>>} normaliseContent
*/
// eslint-disable-next-line complexity
export async function * normaliseCandidateSingle (input, normaliseContent) {
if (input === null || input === undefined) {
throw errCode(new Error(`Unexpected input: ${input}`), 'ERR_UNEXPECTED_INPUT')
}

// String
if (typeof input === 'string' || input instanceof String) {
yield toFileObject(input.toString(), normaliseContent)
return
}

// Uint8Array|ArrayBuffer|TypedArray
// Blob|File
if (isBytes(input) || isBlob(input)) {
yield toFileObject(input, normaliseContent)
return
}

// Browser ReadableStream
if (isReadableStream(input)) {
input = browserStreamToIt(input)
}

// Iterable<?>
if (Symbol.iterator in input || Symbol.asyncIterator in input) {
// @ts-ignore it's (async)iterable
const peekable = itPeekable(input)

/** @type {any} value **/
const { value, done } = await peekable.peek()

if (done) {
// make sure empty iterators result in empty files
yield { content: [] }
return
}

peekable.push(value)

// (Async)Iterable<Number>
// (Async)Iterable<Bytes>
// (Async)Iterable<String>
if (Number.isInteger(value) || isBytes(value) || typeof value === 'string' || value instanceof String) {
yield toFileObject(peekable, normaliseContent)
return
}

throw errCode(new Error('Unexpected input: multiple items passed - if you are using ipfs.add, please use ipfs.addAll instead'), 'ERR_UNEXPECTED_INPUT')
}

// { path, content: ? }
// Note: Detected _after_ (Async)Iterable<?> because Node.js fs.ReadStreams have a
// `path` property that passes this check.
if (isFileObject(input)) {
yield toFileObject(input, normaliseContent)
return
}

throw errCode(new Error('Unexpected input: cannot convert "' + typeof input + '" into ImportCandidate'), 'ERR_UNEXPECTED_INPUT')
}

/**
* @param {ImportCandidate} input
* @param {(content:ToContent) => Promise<AsyncIterable<Uint8Array>>} normaliseContent
*/
async function toFileObject (input, normaliseContent) {
// @ts-ignore - Those properties don't exist on most input types
const { path, mode, mtime, content } = input

/** @type {ImporterImportCandidate} */
const file = {
path: path || '',
mode: parseMode(mode),
mtime: parseMtime(mtime)
}

if (content) {
file.content = await normaliseContent(content)
} else if (!path) { // Not already a file object with path or content prop
// @ts-ignore - input still can be different ToContent
file.content = await normaliseContent(input)
}

return file
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from './utils.js'

/**
* @param {import('./normalise').ToContent} input
* @param {import('ipfs-core-types/src/utils').ToContent} input
*/
export async function normaliseContent (input) {
// Bytes
Expand Down
29 changes: 12 additions & 17 deletions packages/ipfs-core-utils/src/files/normalise-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,29 @@ import {
} from './utils.js'

/**
* @param {import('./normalise').ToContent} input
* @template T
* @param {T} thing
*/
export async function normaliseContent (input) {
return toAsyncGenerator(input)
async function * toAsyncIterable (thing) {
yield thing
}

/**
* @param {import('./normalise').ToContent} input
* @param {import('ipfs-core-types/src/utils').ToContent} input
*/
async function * toAsyncGenerator (input) {
export async function normaliseContent (input) {
// Bytes | String
if (isBytes(input)) {
yield toBytes(input)
return
return toAsyncIterable(toBytes(input))
}

if (typeof input === 'string' || input instanceof String) {
yield toBytes(input.toString())
return
return toAsyncIterable(toBytes(input.toString()))
}

// Blob
if (isBlob(input)) {
yield * blobToIt(input)
return
return blobToIt(input)
}

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

if (done) {
// make sure empty iterators result in empty files
yield * []
return
return toAsyncIterable(new Uint8Array(0))
}

peekable.push(value)

// (Async)Iterable<Number>
if (Number.isInteger(value)) {
yield Uint8Array.from((await all(peekable)))
return
return toAsyncIterable(Uint8Array.from(await all(peekable)))
}

// (Async)Iterable<Bytes|String>
if (isBytes(value) || typeof value === 'string' || value instanceof String) {
yield * map(peekable, toBytes)
return
return map(peekable, toBytes)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import { normaliseContent } from './normalise-content.browser.js'
import { normalise } from './normalise.js'
import { normaliseCandidateMultiple } from './normalise-candidate-multiple.js'

/**
* @typedef {import('ipfs-core-types/src/utils').ImportCandidateStream} ImportCandidateStream
* @typedef {import('ipfs-core-types/src/utils').ImportCandidate} ImportCandidate
* @typedef {import('ipfs-core-types/src/utils').BrowserImportCandidate} BrowserImportCandidate
*/

/**
* Transforms any of the `ipfs.add` input types into
* Transforms any of the `ipfs.addAll` input types into
*
* ```
* AsyncIterable<{ path, mode, mtime, content: Blob }>
* ```
*
* See https://github.com/ipfs/js-ipfs/blob/master/docs/core-api/FILES.md#ipfsadddata-options
*
* @param {ImportCandidate | ImportCandidateStream} input
* @param {ImportCandidateStream} input
* @returns {AsyncGenerator<BrowserImportCandidate, void, undefined>}
*/
export function normaliseInput (input) {
// @ts-ignore normaliseContent returns Blob and not AsyncIterator
return normalise(input, normaliseContent)
// @ts-expect-error browser normaliseContent returns a Blob not an AsyncIterable<Uint8Array>
return normaliseCandidateMultiple(input, normaliseContent, true)
}
Loading