Skip to content

Commit

Permalink
use unix format by default for file metadata and add option executable
Browse files Browse the repository at this point in the history
  • Loading branch information
gildas-lormeau committed Jan 30, 2025
1 parent 50938de commit 6e0fd98
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 19 deletions.
8 changes: 7 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,12 @@ export interface ZipWriterAddDataOptions
* @defaultValue false
*/
directory?: boolean;
/**
* `true` if the entry is an executable file.
*
* @defaultValue false
*/
executable?: boolean;
/**
* The comment of the entry.
*/
Expand Down Expand Up @@ -1357,7 +1363,7 @@ export interface ZipWriterConstructorOptions {
/**
* `true` to write {@link EntryMetaData#externalFileAttributes} in MS-DOS format for folder entries.
*
* @defaultValue true
* @defaultValue false
*/
msDosCompatible?: boolean;
/**
Expand Down
2 changes: 2 additions & 0 deletions lib/core/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const BITFLAG_LANG_ENCODING_FLAG = 0x0800;
const FILE_ATTR_MSDOS_DIR_MASK = 0x10;
const FILE_ATTR_UNIX_DIR_MASK = 0x4000;
const FILE_ATTR_UNIX_EXECUTABLE_MASK = 0o111;
const FILE_ATTR_UNIX_DEFAULT_MASK = 0o644;

const VERSION_DEFLATE = 0x14;
const VERSION_ZIP64 = 0x2D;
Expand Down Expand Up @@ -106,6 +107,7 @@ export {
FILE_ATTR_MSDOS_DIR_MASK,
FILE_ATTR_UNIX_DIR_MASK,
FILE_ATTR_UNIX_EXECUTABLE_MASK,
FILE_ATTR_UNIX_DEFAULT_MASK,
VERSION_DEFLATE,
VERSION_ZIP64,
VERSION_AES,
Expand Down
6 changes: 5 additions & 1 deletion lib/core/zip-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ const PROPERTY_NAME_ENCRYPTED = "encrypted";
const PROPERTY_NAME_VERSION = "version";
const PROPERTY_NAME_VERSION_MADE_BY = "versionMadeBy";
const PROPERTY_NAME_ZIPCRYPTO = "zipCrypto";
const PROPERTY_NAME_DIRECTORY = "directory";
const PROPERTY_NAME_EXECUTABLE = "executable";

const PROPERTY_NAMES = [
PROPERTY_NAME_FILENAME, PROPERTY_NAME_RAW_FILENAME, PROPERTY_NAME_COMPPRESSED_SIZE, PROPERTY_NAME_UNCOMPPRESSED_SIZE,
PROPERTY_NAME_LAST_MODIFICATION_DATE, PROPERTY_NAME_RAW_LAST_MODIFICATION_DATE, PROPERTY_NAME_COMMENT, PROPERTY_NAME_RAW_COMMENT,
PROPERTY_NAME_LAST_ACCESS_DATE, PROPERTY_NAME_CREATION_DATE, PROPERTY_NAME_OFFSET, PROPERTY_NAME_DISK_NUMBER_START,
PROPERTY_NAME_DISK_NUMBER_START, PROPERTY_NAME_INTERNAL_FILE_ATTRIBUTE, PROPERTY_NAME_INTERNAL_FILE_ATTRIBUTES, PROPERTY_NAME_EXTERNAL_FILE_ATTRIBUTE, PROPERTY_NAME_EXTERNAL_FILE_ATTRIBUTES, PROPERTY_NAME_MS_DOS_COMPATIBLE, PROPERTY_NAME_ZIP64, PROPERTY_NAME_ENCRYPTED, PROPERTY_NAME_VERSION, PROPERTY_NAME_VERSION_MADE_BY, PROPERTY_NAME_ZIPCRYPTO, "directory", "executable", "bitFlag", "signature", "filenameUTF8", "commentUTF8", "compressionMethod", "extraField", "rawExtraField", "extraFieldZip64", "extraFieldUnicodePath", "extraFieldUnicodeComment", "extraFieldAES", "extraFieldNTFS", "extraFieldExtendedTimestamp"];
PROPERTY_NAME_DISK_NUMBER_START, PROPERTY_NAME_INTERNAL_FILE_ATTRIBUTE, PROPERTY_NAME_INTERNAL_FILE_ATTRIBUTES, PROPERTY_NAME_EXTERNAL_FILE_ATTRIBUTE, PROPERTY_NAME_EXTERNAL_FILE_ATTRIBUTES, PROPERTY_NAME_MS_DOS_COMPATIBLE, PROPERTY_NAME_ZIP64, PROPERTY_NAME_ENCRYPTED, PROPERTY_NAME_VERSION, PROPERTY_NAME_VERSION_MADE_BY, PROPERTY_NAME_ZIPCRYPTO, PROPERTY_NAME_DIRECTORY, PROPERTY_NAME_EXECUTABLE, "bitFlag", "signature", "filenameUTF8", "commentUTF8", "compressionMethod", "extraField", "rawExtraField", "extraFieldZip64", "extraFieldUnicodePath", "extraFieldUnicodeComment", "extraFieldAES", "extraFieldNTFS", "extraFieldExtendedTimestamp"];

class Entry {

Expand Down Expand Up @@ -90,5 +92,7 @@ export {
PROPERTY_NAME_VERSION,
PROPERTY_NAME_VERSION_MADE_BY,
PROPERTY_NAME_ZIPCRYPTO,
PROPERTY_NAME_DIRECTORY,
PROPERTY_NAME_EXECUTABLE,
Entry
};
55 changes: 38 additions & 17 deletions lib/core/zip-writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ import {
BITFLAG_DATA_DESCRIPTOR,
BITFLAG_LANG_ENCODING_FLAG,
FILE_ATTR_MSDOS_DIR_MASK,
FILE_ATTR_UNIX_DIR_MASK,
FILE_ATTR_UNIX_EXECUTABLE_MASK,
FILE_ATTR_UNIX_DEFAULT_MASK,
VERSION_DEFLATE,
VERSION_ZIP64,
VERSION_AES,
Expand Down Expand Up @@ -92,6 +95,8 @@ import {
PROPERTY_NAME_VERSION,
PROPERTY_NAME_VERSION_MADE_BY,
PROPERTY_NAME_ZIPCRYPTO,
PROPERTY_NAME_DIRECTORY,
PROPERTY_NAME_EXECUTABLE,
Entry
} from "./zip-entry.js";

Expand Down Expand Up @@ -227,10 +232,37 @@ export {

async function addFile(zipWriter, name, reader, options) {
name = name.trim();
if (options.directory && (!name.endsWith(DIRECTORY_SIGNATURE))) {
name += DIRECTORY_SIGNATURE;
} else {
options.directory = name.endsWith(DIRECTORY_SIGNATURE);
const msDosCompatible = getOptionValue(zipWriter, options, PROPERTY_NAME_MS_DOS_COMPATIBLE);
const versionMadeBy = getOptionValue(zipWriter, options, PROPERTY_NAME_VERSION_MADE_BY, msDosCompatible ? 20 : 768);
const executable = getOptionValue(zipWriter, options, PROPERTY_NAME_EXECUTABLE);
if (versionMadeBy > MAX_16_BITS) {
throw new Error(ERR_INVALID_VERSION);
}
let externalFileAttributes = getOptionValue(zipWriter, options, PROPERTY_NAME_EXTERNAL_FILE_ATTRIBUTES, 0);
if (externalFileAttributes === 0) {
externalFileAttributes = getOptionValue(zipWriter, options, PROPERTY_NAME_EXTERNAL_FILE_ATTRIBUTE, 0);
}
if (!options.directory && name.endsWith(DIRECTORY_SIGNATURE)) {
options.directory = true;
}
const directory = getOptionValue(zipWriter, options, PROPERTY_NAME_DIRECTORY);
if (directory) {
if (!name.endsWith(DIRECTORY_SIGNATURE)) {
name += DIRECTORY_SIGNATURE;
}
if (externalFileAttributes === 0) {
if (msDosCompatible) {
externalFileAttributes = FILE_ATTR_MSDOS_DIR_MASK;
} else {
externalFileAttributes = FILE_ATTR_UNIX_DIR_MASK << 16;
}
}
} else if (!msDosCompatible && externalFileAttributes === 0) {
if (executable) {
externalFileAttributes = (FILE_ATTR_UNIX_EXECUTABLE_MASK | FILE_ATTR_UNIX_DEFAULT_MASK) << 16;
} else {
externalFileAttributes = FILE_ATTR_UNIX_DEFAULT_MASK << 16;
}
}
const encode = getOptionValue(zipWriter, options, "encodeText", encodeText);
let rawFilename = encode(name);
Expand All @@ -252,22 +284,13 @@ async function addFile(zipWriter, name, reader, options) {
if (version > MAX_16_BITS) {
throw new Error(ERR_INVALID_VERSION);
}
const versionMadeBy = getOptionValue(zipWriter, options, PROPERTY_NAME_VERSION_MADE_BY, 20);
if (versionMadeBy > MAX_16_BITS) {
throw new Error(ERR_INVALID_VERSION);
}
const lastModDate = getOptionValue(zipWriter, options, PROPERTY_NAME_LAST_MODIFICATION_DATE, new Date());
const lastAccessDate = getOptionValue(zipWriter, options, PROPERTY_NAME_LAST_ACCESS_DATE);
const creationDate = getOptionValue(zipWriter, options, PROPERTY_NAME_CREATION_DATE);
const msDosCompatible = getOptionValue(zipWriter, options, PROPERTY_NAME_MS_DOS_COMPATIBLE, true);
let internalFileAttributes = getOptionValue(zipWriter, options, PROPERTY_NAME_INTERNAL_FILE_ATTRIBUTES, 0);
if (internalFileAttributes === 0) {
internalFileAttributes = getOptionValue(zipWriter, options, PROPERTY_NAME_INTERNAL_FILE_ATTRIBUTE, 0);
}
let externalFileAttributes = getOptionValue(zipWriter, options, PROPERTY_NAME_EXTERNAL_FILE_ATTRIBUTES, 0);
if (externalFileAttributes === 0) {
externalFileAttributes = getOptionValue(zipWriter, options, PROPERTY_NAME_EXTERNAL_FILE_ATTRIBUTE, 0);
}
const passThrough = getOptionValue(zipWriter, options, "passThrough");
let password, rawPassword;
if (!passThrough) {
Expand Down Expand Up @@ -590,6 +613,7 @@ async function createFileEntry(reader, writer, { diskNumberStart, lock }, entryI
zipCrypto,
dataDescriptor,
directory,
executable,
versionMadeBy,
rawComment,
rawExtraField,
Expand All @@ -611,6 +635,7 @@ async function createFileEntry(reader, writer, { diskNumberStart, lock }, entryI
versionMadeBy,
zip64,
directory: Boolean(directory),
executable: Boolean(executable),
filenameUTF8: true,
rawFilename,
commentUTF8: true,
Expand Down Expand Up @@ -1071,13 +1096,11 @@ async function closeFile(zipWriter, comment, options) {
rawComment,
versionMadeBy,
headerArray,
directory,
zip64,
zip64UncompressedSize,
zip64CompressedSize,
zip64DiskNumberStart,
zip64Offset,
msDosCompatible,
internalFileAttributes,
externalFileAttributes,
diskNumberStart,
Expand All @@ -1101,8 +1124,6 @@ async function closeFile(zipWriter, comment, options) {
setUint16(directoryView, offset + 36, internalFileAttributes);
if (externalFileAttributes) {
setUint32(directoryView, offset + 38, externalFileAttributes);
} else if (directory && msDosCompatible) {
setUint8(directoryView, offset + 38, FILE_ATTR_MSDOS_DIR_MASK);
}
setUint32(directoryView, offset + 42, zip64 && zip64Offset ? MAX_32_BITS : fileEntryOffset);
arraySet(directoryArray, rawFilename, offset + 46);
Expand Down
31 changes: 31 additions & 0 deletions tests/all/test-executable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* global Blob */

import * as zip from "../../index.js";
// bash
const TEXT_CONTENT = `#!/bin/bash
echo "Hello, world!"
`;
const FILENAME = "hello.sh";
const BLOB = new Blob([TEXT_CONTENT], { type: zip.getMimeType(FILENAME) });

export { test };

async function test() {
zip.configure({ chunkSize: 128, useWebWorkers: true });
const blobWriter = new zip.BlobWriter("application/zip");
const zipWriter = new zip.ZipWriter(blobWriter);
await zipWriter.add(FILENAME, new zip.BlobReader(BLOB), { executable: true });
await zipWriter.close();
const zipReader = new zip.ZipReader(new zip.BlobReader(await blobWriter.getData()));
const entries = await zipReader.getEntries();
if (entries[0].executable && entries[0].filename == FILENAME) {
const text = await entries[0].getData(new zip.TextWriter());
await zipReader.close();
await zip.terminateWorkers();
if (TEXT_CONTENT != text) {
throw new Error();
}
} else {
throw new Error();
}
}
1 change: 1 addition & 0 deletions tests/tests-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default ([
{ title: "Data descriptor signature", script: "./test-data-descriptor-signature.js" },
{ title: "Data descriptor", script: "./test-data-descriptor.js" },
{ title: "Directory", script: "./test-directory.js" },
{ title: "Executable", script: "./test-executable.js" },
{ title: "Duplicated Filename", script: "./test-duplicated-filename.js" },
{ title: "Empty zip file", script: "./test-empty.js" },
{ title: "Extended timestamp", script: "./test-extended-timestamp.js" },
Expand Down

0 comments on commit 6e0fd98

Please sign in to comment.