Packages

zip

Pack files into ZIP and tar archives, unpack them safely, and gzip single files or strings.

import "std:zip"

zip makes and reads archives. Use it to bundle a folder for sending, unpack a download, peek inside an archive without extracting it, or shrink a log file or a string.

osl
import "std:zip"

Compress a folder#

zip.compress(path, output) packs a file or a whole folder into a new ZIP archive. zip.entries(path) lists what is inside, with sizes in bytes.

main.osl
import "std:zip"
import "std:fs"

fs.remove("docs", {force: true}).unwrap()
fs.mkdir("docs/img", {recursive: true}).unwrap()
fs.writeFile("docs/readme.txt", "hello hello hello hello").unwrap()
fs.writeFile("docs/img/note.txt", "tiny").unwrap()

zip.compress("docs", "docs.zip").unwrap()
for e in zip.entries("docs.zip").unwrap() (
  log e.name e.isDir e.size
)
Click the code to edit it

Try it: add a third file to the folder and run it again. The archive keeps the folder's name as its top level.

Extract#

zip.decompress(path, folder) unpacks everything into a folder, creating it if needed. zip.extractFile(path, name, output) takes out one entry.

main.osl
import "std:zip"
import "std:fs"

fs.remove("docs", {force: true}).unwrap()
fs.mkdir("docs", {recursive: true}).unwrap()
fs.writeFile("docs/readme.txt", "read me").unwrap()
zip.compress("docs", "docs.zip").unwrap()

zip.decompress("docs.zip", "unpacked").unwrap()
log fs.readFile("unpacked/docs/readme.txt").unwrap()

zip.extractFile("docs.zip", "docs/readme.txt", "just-one.txt").unwrap()
log fs.readFile("just-one.txt").unwrap()
Click the code to edit it

When it goes wrong#

Missing archives are NotFoundError. A file that is not an archive is a ParseError. Both come back as results, and a failed write leaves nothing half done.

main.osl
import "std:zip"
import "std:fs"

fs.writeFile("plain.txt", "I am not a zip").unwrap()
log zip.decompress("missing.zip", "out").unwrapErr().kind
log zip.entries("plain.txt").unwrapErr().kind
Click the code to edit it

Edit an archive#

create makes an empty archive, addFile adds a file or folder (replacing entries with the same name), and removeFile deletes an entry. Removing a name the archive doesn't have does nothing.

main.osl
import "std:zip"
import "std:fs"

fs.writeFile("a.txt", "A").unwrap()
fs.writeFile("b.txt", "B").unwrap()

zip.create("bundle.zip").unwrap()
zip.addFile("bundle.zip", "a.txt").unwrap()
zip.addFile("bundle.zip", "b.txt").unwrap()
zip.removeFile("bundle.zip", "a.txt").unwrap()
for e in zip.entries("bundle.zip").unwrap() ( log e.name )
Click the code to edit it

Safe extraction#

Archives from strangers can be traps: entries that write outside the target folder, or a tiny file that expands to gigabytes. decompress checks every entry before it writes anything and refuses paths that escape the folder. Set limits too:

main.osl
import "std:zip"
import "std:fs"

fs.mkdir("many", {recursive: true}).unwrap()
fs.writeFile("many/1.txt", "1").unwrap()
fs.writeFile("many/2.txt", "2").unwrap()
zip.compress("many", "many.zip").unwrap()

log zip.decompress("many.zip", "out", {maxFiles: 1}).unwrapErr().kind
log zip.decompress("many.zip", "out", {maxBytes: 1}).unwrapErr().kind
log zip.decompress("many.zip", "out", {maxFiles: 10, maxBytes: 1000}).isOk()
Click the code to edit it

gzip a file#

ZIP holds many files. gzip compresses one. zip.gzip(path, output) and zip.gunzip(path, output) are a pair, and they take size limits as well.

main.osl
import "std:zip"
import "std:fs"

string text = "log line\n" * 100
fs.writeFile("app.log", text).unwrap()
zip.gzip("app.log", "app.log.gz").unwrap()
zip.gunzip("app.log.gz", "restored.log").unwrap()

log fs.size("app.log").unwrap()
log fs.size("app.log.gz").unwrap() < fs.size("app.log").unwrap()
log fs.readFile("restored.log").unwrap() == text
Click the code to edit it

Compress a string#

compressString gzips text and returns it as base64, a string you can store in JSON or send in a header. decompressString reverses it.

main.osl
import "std:zip"

string packed = zip.compressString("abc" * 50)
log packed.len < 150
log zip.decompressString(packed).unwrap().len
log zip.decompressString("not base64!").unwrapErr().kind
Click the code to edit it

tar#

zip.tar(path, output) and zip.untar(path, output) work like compress and decompress for tar archives, which store files and folders only. Combine with gzip for a .tar.gz.

main.osl
import "std:zip"
import "std:fs"

fs.mkdir("proj", {recursive: true}).unwrap()
fs.writeFile("proj/main.osl", "log 1").unwrap()
zip.tar("proj", "proj.tar").unwrap()
zip.untar("proj.tar", "copy").unwrap()
log fs.readFile("copy/proj/main.osl").unwrap()
Click the code to edit it
Quick check

You unpack a ZIP someone uploaded to your server. What should you do?

Your turn

Backup and check

Compress the notes folder to notes.zip, then print how many files (not folders) are in the archive.

main.osl
Ctrl Enter runs
Expected output
2

API reference#

20 members, generated from the std:zip source of OSL 9.3.0.

Functions

#zip.addFile(string path, string file) result<unit, error>

addFile adds the file or directory at file to the existing ZIP archive at path, replacing entries with the same names. Fails with NotFoundError when the archive or file doesn't exist and ParseError when path isn't a ZIP archive; on failure the archive is unchanged.

#zip.compress(string path, string output) result<unit, error>

compress writes the file or directory at path to a new ZIP archive at output, named after path's last element, replacing any file there. Fails with NotFoundError when path doesn't exist and PermissionError or IOError when something can't be read or written; a failed write leaves no archive behind.

#zip.compressString(string text) string

compressString gzips text and returns the result as standard base64, for storing or sending compressed text as a string. Undo it with zip.decompressString.

#zip.copyFile(string src, string dst) boolean

Deprecated. Deprecated: use fs.copy(from, to) instead.

copyFile copies the file src to dst, returning false on failure.

#zip.create(string path) result<unit, error>

create writes an empty ZIP archive to path, replacing any file there, so zip.addFile can fill it. Fails with NotFoundError when the directory doesn't exist and PermissionError or IOError when it can't be written.

#zip.decompress(string path, string output, object? options) result<unit, error>

decompress extracts the ZIP archive at path into the directory output, creating it if needed. Every entry is checked before anything is written. Options: maxBytes (most bytes to write in total) and maxFiles (most entries), both unlimited by default. Fails with NotFoundError when path doesn't exist, ParseError for corrupt data or an entry that would land outside output, UnsupportedError for symbolic link entries or an unknown compression method, ValueError when the archive passes a limit and PermissionError when a symbolic link already in output would redirect an entry. A file that fails part way is removed, but files written before it stay. Throws ValueError for a limit below 1.

#zip.decompressLimited(any zipPath, any outputPath, any ...limits) boolean

Deprecated. Deprecated: use zip.decompress(path, output, {maxBytes: n, maxFiles: n}) instead, which reports why it failed.

decompressLimited(zipPath, outputPath, maxBytes, maxFiles) extracts like decompress, returning false when the archive has more than maxFiles entries or expands past maxBytes.

#zip.decompressString(string text, object? options) result<string, error>

decompressString reverses zip.compressString: it decodes base64 text and gunzips it. Option: maxBytes, the most bytes the result may have (unlimited by default). Fails with ParseError when text isn't base64 or the bytes aren't gzip data, and ValueError when the result passes maxBytes. Throws ValueError for a maxBytes below 1.

#zip.entries(string path) result<{comment: string, compressedSize: int64, isDir: boolean, modified: int64, name: string, size: int64}[], error>

entries lists the members of the ZIP archive at path as records with the keys name, isDir, size and compressedSize (bytes), modified (unix milliseconds) and comment. Fails with NotFoundError when path doesn't exist and ParseError when it isn't a ZIP archive.

#zip.extractFile(string path, string name, string output, object? options) result<unit, error>

extractFile writes the archive entry named name to the file output, replacing any file there. Option: maxBytes, the most bytes to write. Fails with NotFoundError when the archive or entry doesn't exist, ValueError when the entry is a directory or larger than maxBytes and ParseError for corrupt data; a failed write leaves no file behind. Throws ValueError for a maxBytes below 1.

#zip.fileInfo(any zipPath, any filePath) any

Deprecated. Deprecated: use zip.entries(path) instead and pick the entry by name.

fileInfo describes one entry, or returns null when the archive or entry can't be found.

#zip.gunzip(string path, string output, object? options) result<unit, error>

gunzip decompresses the gzip file at path into the file output, replacing any file there. Option: maxBytes, the most bytes to write (unlimited by default). Fails with NotFoundError when path doesn't exist, ParseError when it isn't valid gzip data, ValueError when it expands past maxBytes and PermissionError or IOError for other file errors; a failed write leaves no file behind. Throws ValueError for a maxBytes below 1.

#zip.gunzipLimited(any sourcePath, any outputPath, any maxOutputBytes) boolean

Deprecated. Deprecated: use zip.gunzip(path, output, {maxBytes: n}) instead, which reports why it failed.

gunzipLimited gunzips like gunzip, returning false when the output would pass maxOutputBytes.

#zip.gzip(string path, string output, object? options) result<unit, error>

gzip compresses the file at path into a gzip file at output, replacing any file there. Options: maxInputBytes (most bytes to read from path) and maxBytes (most bytes to write), both unlimited by default. Fails with NotFoundError when path doesn't exist, ValueError when a limit is passed and PermissionError or IOError for other file errors; a failed write leaves no file behind. Throws ValueError for a limit below 1.

#zip.gzipLimited(any sourcePath, any outputPath, any ...limits) boolean

Deprecated. Deprecated: use zip.gzip(path, output, {maxInputBytes: n, maxBytes: n}) instead, which reports why it failed.

gzipLimited(sourcePath, outputPath, maxInputBytes, maxOutputBytes) gzips like gzip, returning false when the input or output passes its limit.

#zip.list(any zipPath) any

Deprecated. Deprecated: use zip.entries(path) instead, which reports why it failed.

list returns the archive's entries as objects, or [] when it can't be read.

#zip.removeFile(string path, string name) result<unit, error>

removeFile removes the entry named name from the ZIP archive at path. Removing an entry the archive doesn't have does nothing, like fs.remove. Fails with NotFoundError when the archive doesn't exist and ParseError when path isn't a ZIP archive; on failure the archive is unchanged.

#zip.statistics(any zipPath) any

Deprecated. Deprecated: use zip.entries(path) instead and total the sizes.

statistics totals the archive's entries, or returns null when it can't be read.

#zip.tar(string path, string output) result<unit, error>

tar writes the file or directory at path to a new tar archive at output, named after path's last element, replacing any file there. Only regular files and directories are stored. Fails like zip.compress; a failed write leaves no archive behind.

#zip.untar(string path, string output, object? options) result<unit, error>

untar extracts the tar archive at path into the directory output, like zip.decompress, and takes the same maxBytes and maxFiles options. Only regular files and directories are supported; other entry types such as links fail with UnsupportedError before anything is written.