diff --git a/README.md b/README.md index ab82947..c9f4511 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ If you need a feature, you're very welcome to [open an issue](https://github.com Should be straightforward to implement if needed. Maybe `client-zip` should allow extending by third-party code so those extra fields can be plug-ins instead of built into the library. -The UNIX permissions in external attributes (ignored by many readers, though) are hardcoded to 664, could be made configurable. +The UNIX permissions in external attributes (ignored by many readers, though) are hardcoded to 664, could be made configurable. The UNIX permissions are now configurable via the `mode` field, set by default to 664 for files, 775 for folders. ### ZIP64 diff --git a/index.d.ts b/index.d.ts index d9dc917..0e96f72 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,19 +3,19 @@ type StreamLike = ReadableStream | AsyncIterable /** The file name, modification date and size will be read from the input; * extra arguments can be given to override the input’s metadata. */ - type InputWithMeta = File | Response | { input: File | Response, name?: any, lastModified?: any, size?: number | bigint } + type InputWithMeta = File | Response | { input: File | Response, name?: any, lastModified?: any, size?: number | bigint, mode?: number } /** Intrinsic size, but the file name must be provided and modification date can’t be guessed. */ - type InputWithSizeMeta = { input: BufferLike, name: any, lastModified?: any, size?: number | bigint } + type InputWithSizeMeta = { input: BufferLike, name: any, lastModified?: any, size?: number | bigint, mode?: number } /** The file name must be provided ; modification date and content length can’t be guessed. */ - type InputWithoutMeta = { input: StreamLike, name: any, lastModified?: any, size?: number | bigint } + type InputWithoutMeta = { input: StreamLike, name: any, lastModified?: any, size?: number | bigint, mode?: number } /** The folder name must be provided ; modification date can’t be guessed. */ -type InputFolder = { name: any, lastModified?: any, input?: never, size?: never } +type InputFolder = { name: any, lastModified?: any, input?: never, size?: never, mode?: number } /** Both filename and size must be provided ; input is not helpful here. */ - type JustMeta = { input?: StreamLike | undefined, name: any, lastModified?: any, size: number | bigint } + type JustMeta = { input?: StreamLike | undefined, name: any, lastModified?: any, size: number | bigint, mode?: number } type ForAwaitable = AsyncIterable | Iterable diff --git a/src/index.ts b/src/index.ts index faf184a..33afc0c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,19 +5,19 @@ import { loadFiles, contentLength, ForAwaitable } from "./zip.ts" /** The file name, modification date and size will be read from the input; * extra arguments can be given to override the input’s metadata. */ -type InputWithMeta = File | Response | { input: File | Response, name?: any, lastModified?: any, size?: number | bigint } +type InputWithMeta = File | Response | { input: File | Response, name?: any, lastModified?: any, size?: number | bigint, mode?: number } /** Intrinsic size, but the file name must be provided and modification date can’t be guessed. */ -type InputWithSizeMeta = { input: BufferLike, name: any, lastModified?: any, size?: number | bigint } +type InputWithSizeMeta = { input: BufferLike, name: any, lastModified?: any, size?: number | bigint, mode?: number } /** The file name must be provided ; modification date and content length can’t be guessed. */ -type InputWithoutMeta = { input: StreamLike, name: any, lastModified?: any, size?: number | bigint } +type InputWithoutMeta = { input: StreamLike, name: any, lastModified?: any, size?: number | bigint, mode?: number } /** The folder name must be provided ; modification date can’t be guessed. */ -type InputFolder = { name: any, lastModified?: any, input?: never, size?: never } +type InputFolder = { name: any, lastModified?: any, input?: never, size?: never, mode?: number } /** Both filename and size must be provided ; input is not helpful here. */ -type JustMeta = { input?: StreamLike | undefined, name: any, lastModified?: any, size: number | bigint } +type JustMeta = { input?: StreamLike | undefined, name: any, lastModified?: any, size: number | bigint, mode?: number } export type Options = { /** If provided, the returned Response will have its `Content-Length` header set to this value. @@ -37,7 +37,7 @@ export type Options = { function normalizeArgs(file: InputWithMeta | InputWithSizeMeta | InputWithoutMeta | InputFolder | JustMeta) { return file instanceof File || file instanceof Response ? [[file], [file]] as const - : [[file.input, file.name, file.size], [file.input, file.lastModified]] as const + : [[file.input, file.name, file.size], [file.input, file.lastModified, file.mode]] as const } function* mapMeta(files: Iterable) { diff --git a/src/input.ts b/src/input.ts index 59f1855..232f9cd 100644 --- a/src/input.ts +++ b/src/input.ts @@ -6,10 +6,12 @@ export type ZipFileDescription = { modDate: Date bytes: ReadableStream | Uint8Array | Promise crc?: number // will be computed later + mode: number // UNIX permissions, 0o664 by default isFile: true } export type ZipFolderDescription = { modDate: Date + mode: number // UNIX permissions, 0o775 by default isFile: false } export type ZipEntryDescription = ZipFileDescription | ZipFolderDescription; @@ -19,30 +21,38 @@ export type ZipEntryDescription = ZipFileDescription | ZipFolderDescription; * For other types of input, the `name` is required and `modDate` will default to *now*. * @param modDate should be a Date or timestamp or anything else that works in `new Date()` */ -export function normalizeInput(input: File | Response | BufferLike | StreamLike, modDate?: any): ZipFileDescription; -export function normalizeInput(input: undefined, modDate?: any): ZipFolderDescription; -export function normalizeInput(input?: File | Response | BufferLike | StreamLike, modDate?: any): ZipEntryDescription { +export function normalizeInput(input: File | Response | BufferLike | StreamLike, modDate?: any, mode?: number): ZipFileDescription; +export function normalizeInput(input: undefined, modDate?: any, mode?: number): ZipFolderDescription; +export function normalizeInput(input?: File | Response | BufferLike | StreamLike, modDate?: any, mode?: number): ZipEntryDescription { if (modDate !== undefined && !(modDate instanceof Date)) modDate = new Date(modDate) + const isFile = input !== undefined + + if(!mode) { + mode = isFile ? 0o664 : 0o775 + } + if (input instanceof File) return { - isFile: true, + isFile, modDate: modDate || new Date(input.lastModified), - bytes: input.stream() + bytes: input.stream(), + mode } if (input instanceof Response) return { - isFile: true, + isFile, modDate: modDate || new Date(input.headers.get("Last-Modified") || Date.now()), - bytes: input.body! + bytes: input.body!, + mode } if (modDate === undefined) modDate = new Date() else if (isNaN(modDate)) throw new Error("Invalid modification date.") - if (input === undefined) return { isFile: false, modDate } - if (typeof input === "string") return { isFile: true, modDate, bytes: encodeString(input) } - if (input instanceof Blob) return { isFile: true, modDate, bytes: input.stream() } - if (input instanceof Uint8Array || input instanceof ReadableStream) return { isFile: true, modDate, bytes: input } - if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) return { isFile: true, modDate, bytes: makeUint8Array(input) } - if (Symbol.asyncIterator in input) return { isFile: true, modDate, bytes: ReadableFromIterator(input[Symbol.asyncIterator]()) } + if (!isFile) return { isFile, modDate, mode } + if (typeof input === "string") return { isFile, modDate, bytes: encodeString(input), mode } + if (input instanceof Blob) return { isFile, modDate, bytes: input.stream(), mode } + if (input instanceof Uint8Array || input instanceof ReadableStream) return { isFile, modDate, bytes: input, mode } + if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) return { isFile, modDate, bytes: makeUint8Array(input), mode } + if (Symbol.asyncIterator in input) return { isFile, modDate, bytes: ReadableFromIterator(input[Symbol.asyncIterator]()), mode } throw new TypeError("Unsupported input format.") } diff --git a/src/zip.ts b/src/zip.ts index cf5109c..e5345e9 100644 --- a/src/zip.ts +++ b/src/zip.ts @@ -176,7 +176,7 @@ export function centralHeader(file: ZipEntryDescription & Metadata, offset: bigi header.setUint16(30, zip64HeaderLength, true) // useless disk fields = zero (4 bytes) // useless attributes = zero (4 bytes) - header.setUint16(40, file.isFile ? 0o100664 : 0o040775, true) // UNIX regular file with permissions 664, or folder with permission 775. + header.setUint16(40, file.mode | (file.isFile ? 0o100000 : 0o040000), true) header.setUint32(42, clampInt32(offset), true) // offset return makeUint8Array(header) } diff --git a/test/zip.test.ts b/test/zip.test.ts index d040179..68ea80c 100644 --- a/test/zip.test.ts +++ b/test/zip.test.ts @@ -12,10 +12,10 @@ const specDate = new Date("2019-04-26T02:00") const invalidUTF8 = BufferFromHex("fe") const baseFile: ZipFileDescription & Metadata = Object.freeze( - { isFile: true, bytes: new Uint8Array(zipSpec), encodedName: specName, nameIsBuffer: false, modDate: specDate }) + { isFile: true, bytes: new Uint8Array(zipSpec), encodedName: specName, nameIsBuffer: false, modDate: specDate, mode: 0o664 }) const baseFolder: ZipFolderDescription & Metadata = Object.freeze( - { isFile: false, encodedName: new TextEncoder().encode("folder"), nameIsBuffer: false, modDate: specDate }) + { isFile: false, encodedName: new TextEncoder().encode("folder"), nameIsBuffer: false, modDate: specDate, mode: 0o775 }) Deno.test("the ZIP fileHeader function makes file headers", () => { const file = {...baseFile}