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}