diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 0000000..6f2d942 --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1,9 @@ +# .JuliaFormatter.toml + +margin = 92 # The maximum line length for code +short_to_long_function_def = true # Whether to use short or long function definitions +always_use_return = true # Whether to always use the return keyword in functions +separate_kwargs_with_semicolon = false # Whether to separate keyword arguments with semicolons instead of commas +join_lines_based_on_source = false # Whether to join lines based on source code instead of formatting rules +whitespace_in_kwargs = true # Whether to include whitespace around keyword argument separators +remove_extra_newlines = true # Whether to remove extra newlines in code diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..7cab782 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +/.github/ @gryumov +/src/ @gryumov +/test/ @gryumov +/.JuliaFormatter.toml @gryumov +/.gitignore @gryumov +/LICENSE @gryumov +/Project.toml @gryumov diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..9398e23 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,41 @@ +--- +name: 'Bug report' +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +Describe the bug +--- +> A clear and concise description of what the bug is. + + + +To Reproduce +--- +> Add a Minimal, Complete, and Verifiable example (for more details, see e.g. https://stackoverflow.com/help/mcve) +> +> If the code is too long, feel free to put it in a public gist and link +it in the issue: https://gist.github.com + +```julia +# Past your code here. +``` + + + +Expected behavior +--- +> A clear and concise description of what you expected to happen. + + + +Additional context +--- +> Add any other context about the problem here.\ +> Please include the output of\ +> `julia> versioninfo()`\ +> and\ +> `pkg> st` \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..730a65b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,32 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'feature' +assignees: '' + +--- + +Is your feature request related to a problem? Please describe. +--- +> A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + + + +Describe the solution you'd like +--- +> A clear and concise description of what you want to happen. + + + +Describe alternatives you've considered +--- +> A clear and concise description of any alternative solutions or features you've considered. + + + +Additional context +--- +> Add any other context or screenshots about the feature request here. + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..fd44682 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,6 @@ +### Pull request checklist + +- [ ] Did you bump the project version? +- [ ] Did you add a description to the Pull Request? +- [ ] Did you add new tests? +- [ ] Did you add reviewers? diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d4109f8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + \ No newline at end of file diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..e962c65 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,81 @@ +name: CI + +on: + push: + branches: + - master + tags: ['*'] + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} + +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + permissions: + actions: write + contents: read + strategy: + fail-fast: false + matrix: + version: + - '1' + - '1.8' + - '1.9' + - '1.10' + os: + - ubuntu-latest + arch: + - x64 + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + files: lcov.info + + docs: + permissions: + contents: write + statuses: write + actions: write + pull-requests: write + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@latest + - uses: julia-actions/cache@v2 + - name: Install dependencies + run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(; path = pwd())); Pkg.instantiate();' + - name: Build and deploy + env: + GITHUB_TOKEN: ${{ github.token }} + run: julia --project=docs/ docs/make.jl + - name: Make comment with preview link + if: ${{ github.event_name == 'pull_request' && github.event.action == 'opened'}} + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: ':blue_book: A preview of the documentation will be [here](${{env.preview_url}}) soon' + }) + env: + preview_url: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/previews/PR${{ github.event.number }}/ diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml new file mode 100644 index 0000000..062291a --- /dev/null +++ b/.github/workflows/CompatHelper.yml @@ -0,0 +1,18 @@ +name: CompatHelper + +on: + schedule: + - cron: 0 0 * * * + workflow_dispatch: + +jobs: + CompatHelper: + runs-on: ubuntu-latest + steps: + - name: Pkg.add("CompatHelper") + run: julia -e 'using Pkg; Pkg.add("CompatHelper")' + - name: CompatHelper.main() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} + run: julia -e 'using CompatHelper; CompatHelper.main()' diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml new file mode 100644 index 0000000..2bacdb8 --- /dev/null +++ b/.github/workflows/TagBot.yml @@ -0,0 +1,31 @@ +name: TagBot +on: + issue_comment: + types: + - created + workflow_dispatch: + inputs: + lookback: + default: 3 +permissions: + actions: read + checks: read + contents: write + deployments: read + issues: read + discussions: read + packages: read + pages: read + pull-requests: read + repository-projects: read + security-events: read + statuses: read +jobs: + TagBot: + if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' + runs-on: ubuntu-latest + steps: + - uses: JuliaRegistries/TagBot@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c389b6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# MacOS system files +.DS_Store + +# Visual Studio Code settings +.vscode + +# Files generated by invoking Julia with --code-coverage +*.jl.cov +*.jl.*.cov + +# Files generated by invoking Julia with --track-allocation +*.jl.mem + +# System-specific files and directories generated by the BinaryProvider and BinDeps packages +# They contain absolute paths specific to the host computer, and so should not be committed +deps/deps.jl +deps/build.log +deps/downloads/ +deps/usr/ +deps/src/ + +# Build artifacts for creating documentation generated by the Documenter package +docs/build/ +docs/site/ + +# File generated by Pkg, the package manager, based on a corresponding Project.toml +# It records a fixed state of all packages used by the project. As such, it should not be +# committed for packages, but should be committed for applications that require a static +# environment. +Manifest.toml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6c89d0f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# LibZip.jl Changelog + +The latest version of this file can be found at the master branch of the [LibZip.jl repository](https://github.com/bhftbootcamp/LibZip.jl). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fda1be7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Betterhand AG + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Project.toml b/Project.toml new file mode 100644 index 0000000..e1701c4 --- /dev/null +++ b/Project.toml @@ -0,0 +1,11 @@ +name = "LibZip" +uuid = "89089acc-ee81-44d2-83aa-3aa2951b1031" +version = "1.0.0" + +[deps] +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +libzip_jll = "337d8026-41b4-5cde-a456-74a10e5b31d1" + +[compat] +libzip_jll = "1.11.1" +julia = "1.8" diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e7eb7f --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# LibZip.jl + +[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://bhftbootcamp.github.io/LibZip.jl/stable/) +[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://bhftbootcamp.github.io/LibZip.jl/dev/) +[![Build Status](https://github.com/bhftbootcamp/LibZip.jl/actions/workflows/CI.yml/badge.svg?branch=master)](https://github.com/bhftbootcamp/LibZip.jl/actions/workflows/CI.yml?query=branch%3Amaster) +[![Coverage](https://codecov.io/gh/bhftbootcamp/LibZip.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/bhftbootcamp/LibZip.jl) +[![Registry](https://img.shields.io/badge/registry-General-4063d8)](https://github.com/JuliaRegistries/General) + +LibZip is a convenient wrapper around [libzip](https://github.com/nih-at/libzip) for reading, creating, and modifying ZIP and ZIP64 archives. It also supports file encryption and decryption. + +## Installation + +To install LibZip, simply use the Julia package manager: + +```julia +] add LibZip +``` + +## Usage + +### 🤐 Create a zip archive, compress a file, and encrypt it: + +```julia +using LibZip + +# Create a new zip archive +archive = ZipArchive(; flags = LIBZIP_CREATE) + +# Add some text data to a file inside the archive and compress it +write(archive, "greetings.txt", b"Hello, from LibZip!") +zip_compress_file!(archive, "greetings.txt", LIBZIP_CM_DEFLATE; compression_level = 1) + +# Encrypt the file inside the archive +zip_encrypt_file!(archive, "greetings.txt", "P@ssw0rd123!"; method = LIBZIP_EM_AES_128) + +# Write the zip archive to a specified file path +write("secure_archive.zip", archive) + +# Close the archive to finalize changes +close(archive) +``` + +### 🔓 Extract and decrypt files: + +```julia +using LibZip + +# Read the zip archive from disk +zip_data = read("secure_archive.zip") + +# Open the archive for reading +archive = ZipArchive(zip_data; flags = LIBZIP_RDONLY) + +# Extract and decrypt the file, providing the password +read(archive, "greetings.txt"; password = "P@ssw0rd123!") + +# Set a default password for all files +julia> zip_default_password!(archive, "P@ssw0rd123!") +true + +# Iterate through the archive and read files with the default password +# The file contents are decoded from bytes to a string for display +julia> for item in archive + println((item.name, String(item.body))) + end +("greetings.txt", "Hello, from LibZip!") +``` + +### 📂 Reading and Modifying an Existing Archive: + +```julia +using LibZip + +# Open an existing zip archive for reading and modification +archive = zip_open("secure_archive.zip"; flags = LIBZIP_CREATE) + +# Add or modify files inside the archive +write(archive, "new_file.txt", b"More content!") +zip_compress_file!(archive, "new_file.txt", LIBZIP_CM_DEFLATE; compression_level = 1) + +# Close the archive after modification +close(archive) +``` + +## Contributing + +Contributions to LibZip are welcome! If you encounter a bug, have a feature request, or would like to contribute code, please open an issue or a pull request on GitHub. diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..1f35072 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,3 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +LibZip = "89089acc-ee81-44d2-83aa-3aa2951b1031" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..13fdff4 --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,28 @@ +using LibZip +using Documenter + +DocMeta.setdocmeta!(LibZip, :DocTestSetup, :(using LibZip); recursive = true) + +makedocs(; + modules = [LibZip], + sitename = "LibZip.jl", + format = Documenter.HTML(; + repolink = "https://github.com/bhftbootcamp/LibZip.jl", + canonical = "https://bhftbootcamp.github.io/LibZip.jl", + edit_link = "master", + assets = ["assets/favicon.ico"], + sidebar_sitename = true, # Set to 'false' if the package logo already contain its name + ), + pages = [ + "Home" => "index.md", + "pages/api_reference.md", + "pages/constants.md", + ], + warnonly = [:doctest, :missing_docs], +) + +deploydocs(; + repo = "github.com/bhftbootcamp/LibZip.jl", + devbranch = "master", + push_preview = true, +) diff --git a/docs/src/assets/favicon.ico b/docs/src/assets/favicon.ico new file mode 100644 index 0000000..31a47b1 Binary files /dev/null and b/docs/src/assets/favicon.ico differ diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..de37bce --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,77 @@ +# LibZip.jl + +LibZip is a convenient wrapper around [libzip](https://github.com/nih-at/libzip) for reading, creating, and modifying ZIP and ZIP64 archives. It also supports file encryption and decryption. + +## Installation + +To install LibZip, simply use the Julia package manager: + +```julia +] add LibZip +``` + +## Usage + +### 🤐 Create a zip archive, compress a file, and encrypt it: + +```julia +using LibZip + +# Create a new zip archive +archive = ZipArchive(; flags = LIBZIP_CREATE) + +# Add some text data to a file inside the archive and compress it +write(archive, "greetings.txt", b"Hello, from LibZip!") +zip_compress_file!(archive, "greetings.txt", LIBZIP_CM_DEFLATE; compression_level = 1) + +# Encrypt the file inside the archive +zip_encrypt_file!(archive, "greetings.txt", "P@ssw0rd123!"; method = LIBZIP_EM_AES_128) + +# Write the zip archive to a specified file path +write("secure_archive.zip", archive) + +# Close the archive to finalize changes +close(archive) +``` + +### 🔓 Extract and decrypt files: + +```julia +using LibZip + +# Read the zip archive from disk +zip_data = read("secure_archive.zip") + +# Open the archive for reading +archive = ZipArchive(zip_data; flags = LIBZIP_RDONLY) + +# Extract and decrypt the file, providing the password +read(archive, "greetings.txt"; password = "P@ssw0rd123!") + +# Set a default password for all files +julia> zip_default_password!(archive, "P@ssw0rd123!") +true + +# Iterate through the archive and read files with the default password +# The file contents are decoded from bytes to a string for display +julia> for item in archive + println((item.name, String(item.body))) + end +("greetings.txt", "Hello, from LibZip!") +``` + +### 📂 Reading and Modifying an Existing Archive: + +```julia +using LibZip + +# Open an existing zip archive for reading and modification +archive = zip_open("secure_archive.zip"; flags = LIBZIP_CREATE) + +# Add or modify files inside the archive +write(archive, "new_file.txt", b"More content!") +zip_compress_file!(archive, "new_file.txt", LIBZIP_CM_DEFLATE; compression_level = 1) + +# Close the archive after modification +close(archive) +``` diff --git a/docs/src/pages/api_reference.md b/docs/src/pages/api_reference.md new file mode 100644 index 0000000..66cd2ff --- /dev/null +++ b/docs/src/pages/api_reference.md @@ -0,0 +1,34 @@ +# API Reference + +## Types + +```@docs +ZipFile +ZipArchive +``` + +```@docs +ZipArchive() +``` + +## IO + +```@docs +zip_open +Base.close +Base.read +Base.read! +Base.write +``` + +# Tools + +```@docs +zip_discard +zip_compress_file! +zip_encrypt_file! +zip_default_password! +zip_add_dir! +zip_get_file_info +Base.length +``` diff --git a/docs/src/pages/constants.md b/docs/src/pages/constants.md new file mode 100644 index 0000000..73a83cd --- /dev/null +++ b/docs/src/pages/constants.md @@ -0,0 +1,63 @@ +# Constants + +## [Compression methods](@id compression_methods) + +- `LIBZIP_CM_DEFAULT` (`-1`): Similar to `Deflate`. +- `LIBZIP_CM_STORE` (`0`): Store the file uncompressed. +- `LIBZIP_CM_SHRINK` (`1`): Shrunk. +- `LIBZIP_CM_REDUCE_1` (`2`): Reduced with factor `1`. +- `LIBZIP_CM_REDUCE_2` (`3`): Reduced with factor `2`. +- `LIBZIP_CM_REDUCE_3` (`4`): Reduced with factor `3`. +- `LIBZIP_CM_REDUCE_4` (`5`): Reduced with factor `4`. +- `LIBZIP_CM_IMPLODE` (`6`): Imploded. +- `LIBZIP_CM_DEFLATE` (`8`): Deflated. +- `LIBZIP_CM_DEFLATE64` (`9`): `Deflate64`. +- `LIBZIP_CM_PKWARE_IMPLODE` (`10`): `PKWARE` imploding. +- `LIBZIP_CM_BZIP2` (`12`): Compressed using `BZIP2` algorithm +- `LIBZIP_CM_LZMA` (`14`): `LZMA` (`EFS`). +- `LIBZIP_CM_TERSE` (`18`): Compressed using `IBM TERSE` (new). +- `LIBZIP_CM_LZ77` (`19`): IBM `LZ77` z Architecture (`PFS`). +- `LIBZIP_CM_LZMA2` (`33`) +- `LIBZIP_CM_ZSTD` (`93`): `ZSTD` compressed data. +- `LIBZIP_CM_XZ` (`95`): `XZ` compressed data. +- `LIBZIP_CM_JPEG` (`96`): Compressed `Jpeg` data. +- `LIBZIP_CM_WAVPACK` (`97`): `WavPack` compressed data. +- `LIBZIP_CM_PPMD` (`98`): `PPMd` version `I`, Rev `1`. + +## [Encryption methods](@id encryption_methods) + +`LIBZIP_EM_NONE` (`0`): Not encrypted +`LIBZIP_EM_TRAD_PKWARE` (`1`): Traditional `PKWARE` encryption +`LIBZIP_EM_AES_128` (`257`): Winzip `AES-128` encryption. +`LIBZIP_EM_AES_192` (`258`): Winzip `AES-192` encryption. +`LIBZIP_EM_AES_256` (`259`): Winzip `AES-256` encryption. + +## [Open flags](@id open_flags) + +- `LIBZIP_CREATE` (`1`): Create the archive if it does not exist. +- `LIBZIP_EXCL` (`2`): Error if archive already exists. +- `LIBZIP_CHECKCONS` (`4`): Perform additional stricter consistency checks on the archive, and error if they fail. +- `LIBZIP_TRUNCATE` (`8`): If archive exists, ignore its current contents. In other words, handle it the same way as an empty archive. +- `LIBZIP_RDONLY` (`16`): Open archive in read-only mode. + +## [File info flags](@id file_info_flags) + +- `LIBZIP_FL_ENC_GUESS` (`0`): Guess encoding of name (default). (Only `CP-437` and `UTF-8` are recognized). +- `LIBZIP_FL_NOCASE` (`1`): Ignore case distinctions. (Will only work well if the file names are ASCII.) With this flag, zip_name_locate() will be slow for archives with many files. +- `LIBZIP_FL_NODIR` (`2`): Ignore directory part of file name in archive. With this flag, zip_name_locate() will be slow for archives with many files. +- `LIBZIP_FL_ENC_RAW` (`64`): Compare fname against the unmodified names as they are in the ZIP archive, without converting them to `UTF-8`. +- `LIBZIP_FL_ENC_STRICT` (`128`): Follow the ZIP specification and expect `CP-437` encoded names in the ZIP archive (except if they are explicitly marked as `UTF-8`). Convert them to `UTF-8` before comparing. +- `LIBZIP_FL_ENC_UTF_8` (`2048`): Interpret name as `UTF-8`. +- `LIBZIP_FL_ENC_CP437` (`4096`): Interpret name as code page `437` (`CP-437`). + +## [Read file flags](@id read_file_flags) + +- `LIBZIP_FL_COMPRESSED` (`4`): Read compressed data. +- `LIBZIP_FL_UNCHANGED` (`8`): Use original data, ignoring changes. + +## [Add file flags](@id add_file_flags) + +- `LIBZIP_FL_ENC_GUESS` (`0`): Guess encoding of name (default). (Only `CP-437` and `UTF-8` are recognized). +- `LIBZIP_FL_ENC_UTF_8` (`2048`): Interpret name as `UTF-8`. +- `LIBZIP_FL_ENC_CP437` (`4096`): Interpret name as code page `437` (`CP-437`). +- `LIBZIP_FL_OVERWRITE` (`8192`): If file with name exists, overwrite (replace) it. diff --git a/src/LibZip.jl b/src/LibZip.jl new file mode 100644 index 0000000..ceabefe --- /dev/null +++ b/src/LibZip.jl @@ -0,0 +1,585 @@ +module LibZip + +using libzip_jll + +export LIBZIP_CREATE, + LIBZIP_EXCL, + LIBZIP_CHECKCONS, + LIBZIP_TRUNCATE, + LIBZIP_RDONLY + +export LIBZIP_ER_OK, + LIBZIP_ER_MULTIDISK, + LIBZIP_ER_RENAME, + LIBZIP_ER_CLOSE, + LIBZIP_ER_SEEK, + LIBZIP_ER_READ, + LIBZIP_ER_WRITE, + LIBZIP_ER_CRC, + LIBZIP_ER_ZIPCLOSED, + LIBZIP_ER_NOENT, + LIBZIP_ER_EXISTS, + LIBZIP_ER_OPEN, + LIBZIP_ER_TMPOPEN, + LIBZIP_ER_ZLIB, + LIBZIP_ER_MEMORY, + LIBZIP_ER_CHANGED, + LIBZIP_ER_COMPNOTSUPP, + LIBZIP_ER_EOF, + LIBZIP_ER_INVAL, + LIBZIP_ER_NOZIP, + LIBZIP_ER_INTERNAL, + LIBZIP_ER_INCONS, + LIBZIP_ER_REMOVE, + LIBZIP_ER_DELETED, + LIBZIP_ER_ENCRNOTSUPP, + LIBZIP_ER_RDONLY, + LIBZIP_ER_NOPASSWD, + LIBZIP_ER_WRONGPASSWD, + LIBZIP_ER_OPNOTSUPP, + LIBZIP_ER_INUSE, + LIBZIP_ER_TELL, + LIBZIP_ER_COMPRESSED_DATA, + LIBZIP_ER_CANCELLED, + LIBZIP_ER_DATA_LENGTH, + LIBZIP_ER_NOT_ALLOWED, + LIBZIP_ER_TRUNCATED_ZIP + +export LIBZIP_FL_NOCASE, + LIBZIP_FL_NODIR, + LIBZIP_FL_COMPRESSED, + LIBZIP_FL_UNCHANGED, + LIBZIP_FL_ENCRYPTED, + LIBZIP_FL_ENC_GUESS, + LIBZIP_FL_ENC_RAW, + LIBZIP_FL_ENC_STRICT, + LIBZIP_FL_LOCAL, + LIBZIP_FL_CENTRAL, + LIBZIP_FL_ENC_UTF_8, + LIBZIP_FL_ENC_CP437, + LIBZIP_FL_OVERWRITE + +export LIBZIP_CM_DEFAULT, + LIBZIP_CM_STORE, + LIBZIP_CM_SHRINK, + LIBZIP_CM_REDUCE_1, + LIBZIP_CM_REDUCE_2, + LIBZIP_CM_REDUCE_3, + LIBZIP_CM_REDUCE_4, + LIBZIP_CM_IMPLODE, + LIBZIP_CM_DEFLATE, + LIBZIP_CM_DEFLATE64, + LIBZIP_CM_PKWARE_IMPLODE, + LIBZIP_CM_BZIP2, + LIBZIP_CM_LZMA, + LIBZIP_CM_TERSE, + LIBZIP_CM_LZ77, + LIBZIP_CM_LZMA2, + LIBZIP_CM_ZSTD, + LIBZIP_CM_XZ, + LIBZIP_CM_JPEG, + LIBZIP_CM_WAVPACK, + LIBZIP_CM_PPMD + +export LIBZIP_EM_NONE, + LIBZIP_EM_TRAD_PKWARE, + LIBZIP_EM_AES_128, + LIBZIP_EM_AES_192, + LIBZIP_EM_AES_256, + LIBZIP_EM_UNKNOWN + +export LIBZIP_OPSYS_DOS, + LIBZIP_OPSYS_AMIGA, + LIBZIP_OPSYS_OPENVMS, + LIBZIP_OPSYS_UNIX, + LIBZIP_OPSYS_VM_CMS, + LIBZIP_OPSYS_ATARI_ST, + LIBZIP_OPSYS_OS_2, + LIBZIP_OPSYS_MACINTOSH, + LIBZIP_OPSYS_Z_SYSTEM, + LIBZIP_OPSYS_CPM, + LIBZIP_OPSYS_WINDOWS_NTFS, + LIBZIP_OPSYS_MVS, + LIBZIP_OPSYS_VSE, + LIBZIP_OPSYS_ACORN_RISC, + LIBZIP_OPSYS_VFAT, + LIBZIP_OPSYS_ALTERNATE_MVS, + LIBZIP_OPSYS_BEOS, + LIBZIP_OPSYS_TANDEM, + LIBZIP_OPSYS_OS_400, + LIBZIP_OPSYS_OS_X + +export LIBZIP_AFL_RDONLY, + LIBZIP_AFL_IS_TORRENTZIP, + LIBZIP_AFL_WANT_TORRENTZIP, + LIBZIP_AFL_CREATE_OR_KEEP_FILE_FOR_EMPTY_ARCHIVE + +export LIBZIP_LENGTH_TO_END, + LIBZIP_LENGTH_UNCHECKED + +export LibZipT, + LibZipSourceT, + LibZipErrorT, + LibZipFileT, + LibZipStatT + +export libzip_libzip_version + +export libzip_open, + libzip_open_from_source, + libzip_close, + libzip_discard + +export libzip_source_buffer, + libzip_source_buffer_create, + libzip_source_file_create, + libzip_source_keep, + libzip_source_stat, + libzip_source_open, + libzip_source_read, + libzip_source_close, + libzip_source_free + +export libzip_dir_add, + libzip_file_add, + libzip_file_replace, + libzip_set_file_compression, + libzip_file_set_encryption, + libzip_file_set_comment, + libzip_file_get_comment, + libzip_file_set_dostime, + libzip_file_set_mtime, + libzip_file_set_external_attributes, + libzip_file_get_external_attributes, + libzip_rename, + libzip_delete + +export libzip_get_num_entries, + libzip_set_default_password, + libzip_get_name, + libzip_stat_init, + libzip_stat, + libzip_stat_index, + libzip_name_locate, + libzip_set_archive_comment, + libzip_get_archive_comment, + libzip_set_archive_flag, + libzip_get_archive_flag + +export libzip_fopen, + libzip_fopen_index, + libzip_fopen_encrypted, + libzip_fopen_index_encrypted, + libzip_fread, + libzip_fclose + +export libzip_unchange, + libzip_unchange_all, + libzip_unchange_archive + +export libzip_error_init, + libzip_error_init_with_code, + libzip_get_error, + libzip_error_code_zip, + libzip_error_code_system, + libzip_error_strerror, + libzip_strerror, + libzip_source_error, + zip_error_fini + +export ZipArchive, + ZipError, + ZipFile + +export zip_open, + zip_discard, + zip_compress_file!, + zip_encrypt_file!, + zip_default_password!, + zip_get_file_info, + zip_add_dir! + +const LIBZIP_CREATE = 1 +const LIBZIP_EXCL = 2 +const LIBZIP_CHECKCONS = 4 +const LIBZIP_TRUNCATE = 8 +const LIBZIP_RDONLY = 16 + +const LIBZIP_ER_OK = 0 # N No error +const LIBZIP_ER_MULTIDISK = 1 # N Multi-disk zip archives not supported +const LIBZIP_ER_RENAME = 2 # S Renaming temporary file failed +const LIBZIP_ER_CLOSE = 3 # S Closing zip archive failed +const LIBZIP_ER_SEEK = 4 # S Seek error +const LIBZIP_ER_READ = 5 # S Read error +const LIBZIP_ER_WRITE = 6 # S Write error +const LIBZIP_ER_CRC = 7 # N CRC error +const LIBZIP_ER_ZIPCLOSED = 8 # N Containing zip archive was closed +const LIBZIP_ER_NOENT = 9 # N No such file +const LIBZIP_ER_EXISTS = 10 # N File already exists +const LIBZIP_ER_OPEN = 11 # S Can't open file +const LIBZIP_ER_TMPOPEN = 12 # S Failure to create temporary file +const LIBZIP_ER_ZLIB = 13 # Z Zlib error +const LIBZIP_ER_MEMORY = 14 # N Malloc failure +const LIBZIP_ER_CHANGED = 15 # N Entry has been changed +const LIBZIP_ER_COMPNOTSUPP = 16 # N Compression method not supported +const LIBZIP_ER_EOF = 17 # N Premature end of file +const LIBZIP_ER_INVAL = 18 # N Invalid argument +const LIBZIP_ER_NOZIP = 19 # N Not a zip archive +const LIBZIP_ER_INTERNAL = 20 # N Internal error +const LIBZIP_ER_INCONS = 21 # L Zip archive inconsistent +const LIBZIP_ER_REMOVE = 22 # S Can't remove file +const LIBZIP_ER_DELETED = 23 # N Entry has been deleted +const LIBZIP_ER_ENCRNOTSUPP = 24 # N Encryption method not supported +const LIBZIP_ER_RDONLY = 25 # N Read-only archive +const LIBZIP_ER_NOPASSWD = 26 # N No password provided +const LIBZIP_ER_WRONGPASSWD = 27 # N Wrong password provided +const LIBZIP_ER_OPNOTSUPP = 28 # N Operation not supported +const LIBZIP_ER_INUSE = 29 # N Resource still in use +const LIBZIP_ER_TELL = 30 # S Tell error +const LIBZIP_ER_COMPRESSED_DATA = 31 # N Compressed data invalid +const LIBZIP_ER_CANCELLED = 32 # N Operation cancelled +const LIBZIP_ER_DATA_LENGTH = 33 # N Unexpected length of data +const LIBZIP_ER_NOT_ALLOWED = 34 # N Not allowed in torrentzip +const LIBZIP_ER_TRUNCATED_ZIP = 35 # N Possibly truncated or corrupted zip archive + +const LIBZIP_FL_NOCASE = UInt32(1) # ignore case on name lookup +const LIBZIP_FL_NODIR = UInt32(2) # ignore directory component +const LIBZIP_FL_COMPRESSED = UInt32(4) # read compressed data +const LIBZIP_FL_UNCHANGED = UInt32(8) # use original data, ignoring changes +const LIBZIP_FL_ENCRYPTED = UInt32(32) # read encrypted data (implies LIBZIP_FL_COMPRESSED) +const LIBZIP_FL_ENC_GUESS = UInt32(0) # guess string encoding (is default) +const LIBZIP_FL_ENC_RAW = UInt32(64) # get unmodified string +const LIBZIP_FL_ENC_STRICT = UInt32(128) # follow specification strictly +const LIBZIP_FL_LOCAL = UInt32(256) # in local header +const LIBZIP_FL_CENTRAL = UInt32(512) # in central directory +const LIBZIP_FL_ENC_UTF_8 = UInt32(2048) # string is UTF-8 encoded +const LIBZIP_FL_ENC_CP437 = UInt32(4096) # string is CP437 encoded +const LIBZIP_FL_OVERWRITE = UInt32(8192) # zip_file_add: if file with name exists, overwrite (replace) it + +const LIBZIP_CM_DEFAULT = -1 # better of deflate or store +const LIBZIP_CM_STORE = 0 # stored (uncompressed) +const LIBZIP_CM_SHRINK = 1 # shrunk +const LIBZIP_CM_REDUCE_1 = 2 # reduced with factor 1 +const LIBZIP_CM_REDUCE_2 = 3 # reduced with factor 2 +const LIBZIP_CM_REDUCE_3 = 4 # reduced with factor 3 +const LIBZIP_CM_REDUCE_4 = 5 # reduced with factor 4 +const LIBZIP_CM_IMPLODE = 6 # imploded +const LIBZIP_CM_DEFLATE = 8 # deflated +const LIBZIP_CM_DEFLATE64 = 9 # deflate64 +const LIBZIP_CM_PKWARE_IMPLODE = 10 # PKWARE imploding +const LIBZIP_CM_BZIP2 = 12 # compressed using BZIP2 algorithm +const LIBZIP_CM_LZMA = 14 # LZMA (EFS) +const LIBZIP_CM_TERSE = 18 # compressed using IBM TERSE (new) +const LIBZIP_CM_LZ77 = 19 # IBM LZ77 z Architecture (PFS) +const LIBZIP_CM_LZMA2 = 33 +const LIBZIP_CM_ZSTD = 93 # Zstandard compressed data +const LIBZIP_CM_XZ = 95 # XZ compressed data +const LIBZIP_CM_JPEG = 96 # Compressed Jpeg data +const LIBZIP_CM_WAVPACK = 97 # WavPack compressed data +const LIBZIP_CM_PPMD = 98 # PPMd version I, Rev 1 + +const LIBZIP_EM_NONE = UInt16(0) # not encrypted +const LIBZIP_EM_TRAD_PKWARE = UInt16(1) # traditional PKWARE encryption +const LIBZIP_EM_AES_128 = UInt16(257) # Winzip AES encryption +const LIBZIP_EM_AES_192 = UInt16(258) +const LIBZIP_EM_AES_256 = UInt16(259) +const LIBZIP_EM_UNKNOWN = UInt16(65535) # unknown algorithm + +const LIBZIP_OPSYS_DOS = UInt8(0x00) +const LIBZIP_OPSYS_AMIGA = UInt8(0x01) +const LIBZIP_OPSYS_OPENVMS = UInt8(0x02) +const LIBZIP_OPSYS_UNIX = UInt8(0x03) +const LIBZIP_OPSYS_VM_CMS = UInt8(0x04) +const LIBZIP_OPSYS_ATARI_ST = UInt8(0x05) +const LIBZIP_OPSYS_OS_2 = UInt8(0x06) +const LIBZIP_OPSYS_MACINTOSH = UInt8(0x07) +const LIBZIP_OPSYS_Z_SYSTEM = UInt8(0x08) +const LIBZIP_OPSYS_CPM = UInt8(0x09) +const LIBZIP_OPSYS_WINDOWS_NTFS = UInt8(0x0a) +const LIBZIP_OPSYS_MVS = UInt8(0x0b) +const LIBZIP_OPSYS_VSE = UInt8(0x0c) +const LIBZIP_OPSYS_ACORN_RISC = UInt8(0x0d) +const LIBZIP_OPSYS_VFAT = UInt8(0x0e) +const LIBZIP_OPSYS_ALTERNATE_MVS = UInt8(0x0f) +const LIBZIP_OPSYS_BEOS = UInt8(0x10) +const LIBZIP_OPSYS_TANDEM = UInt8(0x11) +const LIBZIP_OPSYS_OS_400 = UInt8(0x12) +const LIBZIP_OPSYS_OS_X = UInt8(0x13) + +const LIBZIP_AFL_RDONLY = UInt32(2) # read only -- cannot be cleared +const LIBZIP_AFL_IS_TORRENTZIP = UInt32(4) # current archive is torrentzipped +const LIBZIP_AFL_WANT_TORRENTZIP = UInt32(8) # write archive in torrentzip format +const LIBZIP_AFL_CREATE_OR_KEEP_FILE_FOR_EMPTY_ARCHIVE = UInt32(16) # don't remove file if archive is empty + +const LIBZIP_LENGTH_TO_END = 0 +const LIBZIP_LENGTH_UNCHECKED = -2 # only supported by zip_source_file and its variants + +struct LibZipT end +struct LibZipSourceT end +struct LibZipFileT end + +mutable struct LibZipErrorT + zip_err::Int64 + sys_err::Int64 + str::Ptr{UInt8} + + LibZipErrorT() = new(0, 0, Ptr{UInt8}()) +end + +mutable struct LibZipStatT + valid::Int64 + name::Ptr{UInt8} + index::Int64 + size::Int64 + comp_size::Int64 + time::Int64 + crc::UInt32 + comp_method::UInt16 + encryption_method::UInt16 + flags::Int32 + + LibZipStatT() = new(0, Ptr{UInt8}(), 0, 0, 0, 0, 0, 0, 0, 0) +end + +function libzip_libzip_version() + return ccall((:zip_libzip_version, libzip), Ptr{UInt8}, ()) +end + +#__ Open/Close Archive + +function libzip_open(path, flags, errorp) + return ccall((:zip_open, libzip), Ptr{LibZipT}, (Ptr{UInt8}, Cint, Ptr{Clonglong}), path, flags, errorp) +end + +function libzip_open_from_source(zs, flags, ze) + return ccall((:zip_open_from_source, libzip), Ptr{LibZipT}, (Ptr{LibZipSourceT}, Cint, Ptr{LibZipErrorT}), zs, flags, ze) +end + +function libzip_close(archive) + return ccall((:zip_close, libzip), Cint, (Ptr{LibZipT},), archive) +end + +function libzip_discard(archive) + return ccall((:zip_discard, libzip), Cvoid, (Ptr{LibZipT},), archive) +end + +#__ Source + +function libzip_source_buffer(archive, data, len, freep) + return ccall((:zip_source_buffer, libzip), Ptr{LibZipSourceT}, (Ptr{LibZipT}, Ptr{Cvoid}, Csize_t, Cint,), archive, data, len, freep) +end + +function libzip_source_buffer_create(data, len, freep, error) + return ccall((:zip_source_buffer_create, libzip), Ptr{LibZipSourceT}, (Ptr{UInt8}, Csize_t, Cint, Ptr{LibZipErrorT}), data, len, freep, error) +end + +function libzip_source_file_create(fname, start, len, error) + return ccall((:zip_source_file_create, libzip), Ptr{LibZipSourceT}, (Ptr{UInt8}, Csize_t, Cssize_t, Ptr{LibZipErrorT}), fname, start, len, error) +end + +function libzip_source_keep(source) + return ccall((:zip_source_keep, libzip), Cvoid, (Ptr{LibZipSourceT},), source) +end + +function libzip_source_stat(source, sb) + return ccall((:zip_source_stat, libzip), Int64, (Ptr{LibZipSourceT}, Ptr{LibZipStatT}), source, sb) +end + +function libzip_source_open(source) + return ccall((:zip_source_open, libzip), Int64, (Ptr{LibZipSourceT},), source) +end + +function libzip_source_read(source, data, len) + return ccall((:zip_source_read, libzip), Int64, (Ptr{LibZipSourceT}, Ptr{UInt8}, Csize_t), source, data, len) +end + +function libzip_source_close(source) + return ccall((:zip_source_close, libzip), Int64, (Ptr{LibZipSourceT},), source) +end + +function libzip_source_free(source) + return ccall((:zip_source_free, libzip), Cvoid, (Ptr{LibZipSourceT},), source) +end + +#__ Add/Change Files and Directories + +function libzip_dir_add(archive, name, flags) + return ccall((:zip_dir_add, libzip), Int64, (Ptr{LibZipT}, Ptr{UInt8}, Cuint), archive, name, flags) +end + +function libzip_file_add(archive, name, source, flags) + return ccall((:zip_file_add, libzip), Int64, (Ptr{LibZipT}, Ptr{UInt8}, Ptr{LibZipSourceT}, Cuint), archive, name, source, flags) +end + +function libzip_file_replace(archive, index, source, flags) + return ccall((:zip_file_replace, libzip), Int64, (Ptr{LibZipT}, Csize_t, Ptr{LibZipSourceT}, Cuint), archive, index, source, flags) +end + +function libzip_set_file_compression(archive, index, comp, comp_flags) + return ccall((:zip_set_file_compression, libzip), Int64, (Ptr{LibZipT}, Csize_t, Cint, Cuint), archive, index, comp, comp_flags) +end + +function libzip_file_set_encryption(archive, index, method, password) + return ccall((:zip_file_set_encryption, libzip), Int64, (Ptr{LibZipT}, Csize_t, Cushort, Ptr{UInt8}), archive, index, method, password) +end + +function libzip_file_set_comment(archive, index, comment, len, flags) + return ccall((:zip_file_set_comment, libzip), Int64, (Ptr{LibZipT}, Csize_t, Ptr{UInt8}, Cushort, Cuint), archive, index, comment, len, flags) +end + +function libzip_file_get_comment(archive, index, lenp, flags) + return ccall((:zip_file_get_comment, libzip), Ptr{UInt8}, (Ptr{LibZipT}, Csize_t, Ptr{Cint}, Int64), archive, index, lenp, flags) +end + +function libzip_file_set_dostime(archive, index, dostime, dosdate, flags) + return ccall((:zip_file_set_dostime, libzip), Int64, (Ptr{LibZipT}, Csize_t, Cushort, Cushort, Cuint), archive, index, dostime, dosdate, flags) +end + +function libzip_file_set_mtime(archive, index, mtime, flags) + return ccall((:zip_file_set_mtime, libzip), Int64, (Ptr{LibZipT}, Csize_t, Clonglong, Cuint), archive, index, mtime, flags) +end + +function libzip_file_set_external_attributes(archive, index, flags, opsys, attributes) + return ccall((:zip_file_set_external_attributes, libzip), Int64, (Ptr{LibZipT}, Csize_t, Cuint, Cuchar, Cuint), archive, index, flags, opsys, attributes) +end + +function libzip_file_get_external_attributes(archive, index, flags, opsys, attributes) + return ccall((:zip_file_get_external_attributes, libzip), Int64, (Ptr{LibZipT}, Csize_t, Cuint, Ptr{Cuchar}, Ptr{Cuint}), archive, index, flags, opsys, attributes) +end + +function libzip_rename(archive, index, name) + return ccall((:zip_rename, libzip), Int64, (Ptr{LibZipT}, Csize_t, Ptr{UInt8}), archive, index, name) +end + +function libzip_delete(archive, index) + return ccall((:zip_delete, libzip), Int64, (Ptr{LibZipT}, Csize_t), archive, index) +end + +#__ Miscellaneous + +function libzip_get_num_entries(archive, flags) + return ccall((:zip_get_num_entries, libzip), Int64, (Ptr{LibZipT}, Cuint), archive, flags) +end + +function libzip_set_default_password(archive, password) + return ccall((:zip_set_default_password, libzip), Int64, (Ptr{LibZipT}, Ptr{UInt8}), archive, password) +end + +function libzip_get_name(archive, index, flags) + return ccall((:zip_get_name, libzip), Ptr{UInt8}, (Ptr{LibZipT}, Csize_t, Cuint), archive, index, flags) +end + +function libzip_stat_init(sb) + return ccall((:zip_stat_init, libzip), Cvoid, (Ptr{LibZipStatT},), sb) +end + +function libzip_stat(archive, fname, flags, sb) + return ccall((:zip_stat, libzip), Int64, (Ptr{LibZipT}, Ptr{UInt8}, Cuint, Ptr{LibZipStatT}), archive, fname, flags, sb) +end + +function libzip_stat_index(archive, index, flags, sb) + return ccall((:zip_stat_index, libzip), Int64, (Ptr{LibZipT}, Csize_t, Cuint, Ptr{LibZipStatT}), archive, index, flags, sb) +end + +function libzip_name_locate(archive, fname, flags) + return ccall((:zip_name_locate, libzip), Int64, (Ptr{LibZipT}, Ptr{UInt8}, Cuint), archive, fname, flags) +end + +function libzip_set_archive_comment(archive, comment, len) + return ccall((:zip_set_archive_comment, libzip), Int64, (Ptr{LibZipT}, Ptr{UInt8}, Cint), archive, comment, len) +end + +function libzip_get_archive_comment(archive, lenp, flags) + return ccall((:zip_get_archive_comment, libzip), Ptr{UInt8}, (Ptr{LibZipT}, Ptr{Cint}, Cuint), archive, lenp, flags) +end + +function libzip_set_archive_flag(archive, flag, value) + return ccall((:zip_set_archive_flag, libzip), Int64, (Ptr{LibZipT}, Cuint, Cint), archive, flag, value) +end + +function libzip_get_archive_flag(archive, flag, flags) + return ccall((:zip_get_archive_flag, libzip), Int64, (Ptr{LibZipT}, Cuint, Cuint), archive, flag, flags) +end + +#__ Read Files + +function libzip_fopen(archive, fname, flags) + return ccall((:zip_fopen, libzip), Ptr{LibZipFileT}, (Ptr{LibZipT}, Ptr{UInt8}, Cuint), archive, fname, flags) +end + +function libzip_fopen_index(archive, index, flags) + return ccall((:zip_fopen_index, libzip), Ptr{LibZipFileT}, (Ptr{LibZipT}, Csize_t, Cuint), archive, index, flags) +end + +function libzip_fopen_encrypted(archive, fname, flags, password) + return ccall((:zip_fopen_encrypted, libzip), Ptr{LibZipFileT}, (Ptr{LibZipT}, Ptr{UInt8}, Cuint, Ptr{UInt8}), archive, fname, flags, password) +end + +function libzip_fopen_index_encrypted(archive, index, flags, password) + return ccall((:zip_fopen_index_encrypted, libzip), Ptr{LibZipFileT}, (Ptr{LibZipT}, Csize_t, Cuint, Ptr{UInt8}), archive, index, flags, password) +end + +function libzip_fread(file, buf, nbytes) + return ccall((:zip_fread, libzip), Int64, (Ptr{LibZipFileT}, Ptr{Cvoid}, Cuint), file, buf, nbytes) +end + +function libzip_fclose(file) + return ccall((:zip_fclose, libzip), Int64, (Ptr{LibZipFileT},), file) +end + +#__ Revert Changes + +function libzip_unchange(archive, index) + return ccall((:zip_unchange, libzip), Int64, (Ptr{LibZipT}, Csize_t), archive, index) +end + +function libzip_unchange_all(archive) + return ccall((:zip_unchange_all, libzip), Int64, (Ptr{LibZipT},), archive) +end + +function libzip_unchange_archive(archive) + return ccall((:zip_unchange_archive, libzip), Int64, (Ptr{LibZipT},), archive) +end + +#__ Error Handling + +function libzip_error_init(error) + return ccall((:zip_error_init, libzip), Cvoid, (Ptr{LibZipErrorT},), error) +end + +function libzip_error_init_with_code(error, ze) + return ccall((:zip_error_init_with_code, libzip), Cvoid, (Ptr{LibZipErrorT}, Clonglong), error, ze) +end + +function libzip_get_error(archive) + return ccall((:zip_get_error, libzip), Ptr{LibZipErrorT}, (Ptr{LibZipT},), archive) +end + +function libzip_error_code_zip(ze) + return ccall((:zip_error_code_zip, libzip), Int64, (Ptr{LibZipErrorT},), ze) +end + +function libzip_error_code_system(ze) + return ccall((:zip_error_code_system, libzip), Int64, (Ptr{LibZipErrorT},), ze) +end + +function libzip_error_strerror(ze) + return ccall((:zip_error_strerror, libzip), Ptr{UInt8}, (Ptr{LibZipErrorT},), ze) +end + +function libzip_strerror(archive) + return ccall((:zip_strerror, libzip), Ptr{UInt8}, (Ptr{LibZipT},), archive) +end + +function libzip_source_error(source) + return ccall((:zip_source_error, libzip), Ptr{LibZipErrorT}, (Ptr{LibZipSourceT},), source) +end + +function zip_error_fini(ze) + return ccall((:zip_error_fini, libzip), Cvoid, (Ptr{LibZipErrorT},), ze) +end + +include("ZipTools.jl") +using .ZipTools + +end diff --git a/src/ZipTools.jl b/src/ZipTools.jl new file mode 100644 index 0000000..81966fe --- /dev/null +++ b/src/ZipTools.jl @@ -0,0 +1,565 @@ +module ZipTools + +export ZipArchive, + ZipError, + ZipFile + +export zip_open, + zip_discard, + zip_compress_file!, + zip_encrypt_file!, + zip_default_password!, + zip_get_file_info, + zip_add_dir! + +using Dates +using ..LibZip + +const COMPRESSION_METHODS = Dict( + LIBZIP_CM_DEFAULT => "DEFAULT", + LIBZIP_CM_STORE => "STORE", + LIBZIP_CM_SHRINK => "SHRINK", + LIBZIP_CM_REDUCE_1 => "REDUCE_1", + LIBZIP_CM_REDUCE_2 => "REDUCE_2", + LIBZIP_CM_REDUCE_3 => "REDUCE_3", + LIBZIP_CM_REDUCE_4 => "REDUCE_4", + LIBZIP_CM_IMPLODE => "IMPLODE", + LIBZIP_CM_DEFLATE => "DEFLATE", + LIBZIP_CM_DEFLATE64 => "DEFLATE64", + LIBZIP_CM_PKWARE_IMPLODE => "PKWARE_IMPLODE", + LIBZIP_CM_BZIP2 => "BZIP2", + LIBZIP_CM_LZMA => "LZMA", + LIBZIP_CM_TERSE => "TERSE", + LIBZIP_CM_LZ77 => "LZ77", + LIBZIP_CM_LZMA2 => "LZMA2", + LIBZIP_CM_ZSTD => "ZSTD", + LIBZIP_CM_XZ => "XZ", + LIBZIP_CM_JPEG => "JPEG", + LIBZIP_CM_WAVPACK => "WAVPACK", + LIBZIP_CM_PPMD => "PPMD", +) + +const ENCRYPTION_METHODS = Dict( + LIBZIP_EM_NONE => "NONE", + LIBZIP_EM_TRAD_PKWARE => "PKWARE", + LIBZIP_EM_AES_128 => "AES_128", + LIBZIP_EM_AES_192 => "AES_192", + LIBZIP_EM_AES_256 => "AES_256", + LIBZIP_EM_UNKNOWN => "UNKNOWN", +) + +struct ZipError <: Exception + code::Int + message::String + + function ZipError(err_ptr::Ptr{LibZipErrorT}) + code = libzip_error_code_zip(err_ptr) + message = unsafe_string(libzip_error_strerror(err_ptr)) + return new(code, message) + end + + function ZipError(code::Int) + err = LibZipErrorT() + err_ptr = Ptr{LibZipErrorT}(pointer_from_objref(err)) + libzip_error_init_with_code(err_ptr, code) + return ZipError(err_ptr) + end +end + +Base.show(io::IO, e::ZipError) = print(io, "Error $(e.code): $(e.message)") + +#__ File + +""" + ZipFile + +Represents a file contained in an [`ZipArchive`](@ref). + +## Fields + +- `body::Vector{UInt8}`: Binary representation of file contents. +- `name::String`: File name. +- `index::Int64`: File index number (zero-based). +- `size::Int64`: File uncompressed size. +- `comp_size::Int64`: File compressed size. +- `time::DateTime`: File last modification time. +- `crc::Int64`: File CRC-32 checksum. +- `comp_method::Int64`: File compression method. +- `encryption_method::Int64`: File encryption method. +""" +struct ZipFile + body::Vector{UInt8} + name::String + index::Int + size::Int + comp_size::Int + time::DateTime + crc::Int + comp_method::Int + encryption_method::Int + + function ZipFile(data::Vector{UInt8}, stat::LibZipStatT) + name = unsafe_string(stat.name) + index = stat.index + size = stat.size + comp_size = stat.comp_size + time = DateTime(1970, 1, 1) + Second(stat.time) + crc = stat.crc + comp_method = stat.comp_method + encryption_method = stat.encryption_method + + return new( + data, + name, + index, + size, + comp_size, + time, + crc, + comp_method, + encryption_method, + ) + end +end + +function compression_str(num::Int) + return get(COMPRESSION_METHODS, num, "UNKNOWN") +end + +function encryption_str(num::Int) + return get(ENCRYPTION_METHODS, num, "UNKNOWN") +end + +function Base.show(io::IO, entry::ZipFile) + println(io, "File: $(entry.name)") + println(io, " Index: $(entry.index)") + println(io, " Size: $(entry.size) bytes") + println(io, " Compressed Size: $(entry.comp_size) bytes") + println(io, " Last Modified: $(entry.time)") + println(io, " CRC32: $(entry.crc)") + println(io, " Compression: $(compression_str(entry.comp_method))") + println(io, " Encryption: $(encryption_str(entry.encryption_method))") +end + +#__ Archive + +""" + ZipArchive + +Type describing zip archive for reading and writing. + +## Fields +- `archive_ptr::Ptr{LibZipT}`: The pointer to the C library zip structure. +- `source_ptr::Ptr{LibZipSourceT}`: The pointer to the C library source structure. +- `comment::String`: The archives's comment. +- `closed::Bool`: Flag indicating whether the archive is closed. +""" +mutable struct ZipArchive + archive_ptr::Ptr{LibZipT} + source_ptr::Ptr{LibZipSourceT} + comment::String + closed::Bool + + function ZipArchive(archive_ptr::Ptr{LibZipT}, source_ptr::Ptr{LibZipSourceT} = C_NULL) + libzip_source_keep(source_ptr) + comment = unsafe_string(libzip_get_archive_comment(archive_ptr, C_NULL, 0)) + zip = new(archive_ptr, source_ptr, comment, false) + finalizer(zip_discard, zip) + return zip + end +end + +function init_source(data::AbstractVector{UInt8}, freep::Int = 0) + err = LibZipErrorT() + err_ptr = Ptr{LibZipErrorT}(pointer_from_objref(err)) + libzip_error_init(err_ptr) + ptr = libzip_source_buffer_create(data, length(data), freep, err_ptr) + ptr == C_NULL && throw(ZipError(err_ptr)) + zip_error_fini(err_ptr) + return ptr +end + +function init_source(path::AbstractString, start::Int = 0, len::Int = -1) + err = LibZipErrorT() + err_ptr = Ptr{LibZipErrorT}(pointer_from_objref(err)) + libzip_error_init(err_ptr) + ptr = libzip_source_file_create(path, start, len, err_ptr) + ptr == C_NULL && throw(ZipError(err_ptr)) + zip_error_fini(err_ptr) + return ptr +end + +""" + ZipArchive(data::Vector{UInt8}; flags::Int = LIBZIP_RDONLY) -> ZipArchive + ZipArchive(; flags::Int = LIBZIP_CREATE) -> ZipArchive + +Construct an empty [`ZipArchive`](@ref) or an existing one from an in-memory byte buffer with specified `flags`. + +See also [`Open flags`](@ref open_flags) section. + +## Examples + +```julia-repl +julia> ZipArchive(; flags = LIBZIP_CREATE | LIBZIP_CHECKCONS) + ZipArchive: + 🔓 Archive is open and ready for use! + 📂 The archive is empty. +``` + +```julia-repl +julia> zip_file = read(".../secrets.zip"); + +julia> ZipArchive(zip_file) + ZipArchive: + 🔓 Archive is open and ready for use! + 📁 Files in archive: + 📄 my_secret_1.txt/: 42 bytes + [...] +``` +""" +ZipArchive() + +function ZipArchive(data::AbstractVector{UInt8}; flags::Int = LIBZIP_RDONLY) + err = LibZipErrorT() + err_ptr = Ptr{LibZipErrorT}(pointer_from_objref(err)) + libzip_error_init(err_ptr) + source_ptr = init_source(data) + archive_ptr = libzip_open_from_source(source_ptr, flags, err_ptr) + archive_ptr == C_NULL && throw(ZipError(err_ptr)) + zip_error_fini(err_ptr) + return ZipArchive(archive_ptr, source_ptr) +end + +function ZipArchive(; flags::Int = LIBZIP_CREATE) + source_ptr = libzip_source_buffer_create(C_NULL, 0, 0, C_NULL) + archive_ptr = libzip_open_from_source(source_ptr, flags, C_NULL) + return ZipArchive(archive_ptr, source_ptr) +end + +#__ Utils + +function check_closed(zip::ZipArchive) + @assert isopen(zip) "zip archive was closed" + return nothing +end + +function locate_file(zip::ZipArchive, filename::AbstractString, flags::UInt32=UInt32(0)) + check_closed(zip) + index = libzip_name_locate(zip.archive_ptr, filename, flags) + index < 0 && throw(ZipError(LIBZIP_ER_NOENT)) + return index +end + +function zip_error_code(zip::ZipArchive) + error_ptr = libzip_get_error(zip.archive_ptr) + return libzip_error_code_zip(error_ptr) +end + +function source_error_code(zip::ZipArchive) + error_ptr = libzip_source_error(zip.source_ptr) + return libzip_error_code_zip(error_ptr) +end + +#__ Tools + +Base.isopen(zip::ZipArchive) = !zip.closed + +""" + zip_open(path::String; flags::Int = LIBZIP_RDONLY) -> ZipArchive + +Open a zip archive file by its `path` with specified `flags`. + +See also [`Open flags`](@ref open_flags) section. +""" +function zip_open(path::AbstractString; flags::Int = LIBZIP_RDONLY) + err = LibZipErrorT() + err_ptr = Ptr{LibZipErrorT}(pointer_from_objref(err)) + libzip_error_init(err_ptr) + source_ptr = init_source(path) + archive_ptr = libzip_open_from_source(source_ptr, flags, err_ptr) + archive_ptr == C_NULL && throw(ZipError(err_ptr)) + zip_error_fini(err_ptr) + return ZipArchive(archive_ptr, source_ptr) +end + +""" + close(zip::ZipArchive) + +Commit cahnges and close a `zip` archive instance. +""" +function Base.close(zip::ZipArchive) + if isopen(zip) + status = libzip_close(zip.archive_ptr) + iszero(status) || throw(ZipError(zip_error_code(zip))) + zip.closed = true + end +end + +""" + zip_discard(zip::ZipArchive) + +Close a `zip` archive instance without saving changes. +""" +function zip_discard(zip::ZipArchive) + if isopen(zip) + libzip_source_free(zip.source_ptr) + libzip_discard(zip.archive_ptr) + zip.closed = true + end +end + +""" + zip_compress_file!(zip::ZipArchive, index::Int, method::Int = LIBZIP_CM_DEFAULT; compression_level::Int = 1) + zip_compress_file!(zip::ZipArchive, filename::String, method::Int = LIBZIP_CM_DEFAULT; compression_level::Int = 1) + +Set the compression `method` for the file at position `index` or by `filename` in the `zip` archive. +The `compression_level` argument defines the compression level. + +See also [`Compression methods`](@ref compression_methods) section. +""" +function zip_compress_file! end + +function zip_compress_file!( + zip::ZipArchive, + index::Int, + method::Int = LIBZIP_CM_DEFAULT; + compression_level::Int = 1, +) + check_closed(zip) + status = libzip_set_file_compression(zip.archive_ptr, index, method, compression_level) + iszero(status) || throw(ZipError(zip_error_code(zip))) + return nothing +end + +function zip_compress_file!( + zip::ZipArchive, + filename::AbstractString, + method::Int = LIBZIP_CM_DEFAULT; + kw..., +) + return zip_compress_file!(zip, locate_file(zip, filename), method; kw...) +end + +""" + zip_encrypt_file!(zip::ZipArchive, index::Int, password::String; method::UInt16 = LIBZIP_EM_AES_128) + zip_encrypt_file!(zip::ZipArchive, filename::String, password::String; method::UInt16 = LIBZIP_EM_AES_128) + +Set the encryption `method` for the file at position `index` or by `filename` in the `zip` archive using the `password`. + +See also [`encryption methods`](@ref encryption_methods) section. +""" +function zip_encrypt_file! end + +function zip_encrypt_file!( + zip::ZipArchive, + index::Int, + password::AbstractString; + method::UInt16 = LIBZIP_EM_AES_128, +) + check_closed(zip) + status = libzip_file_set_encryption(zip.archive_ptr, index, method, password) + iszero(status) || throw(ZipError(zip_error_code(zip))) + return nothing +end + +function zip_encrypt_file!( + zip::ZipArchive, + filename::AbstractString, + password::AbstractString; + kw..., +) + return zip_encrypt_file!(zip, locate_file(zip, filename), password; kw...) +end + +""" + zip_default_password!(zip::ZipArchive, password::String) + +Set the default `password` in the `zip` archive used when accessing encrypted files. +""" +function zip_default_password!(zip::ZipArchive, password::AbstractString) + check_closed(zip) + status = libzip_set_default_password(zip.archive_ptr, password) + status >= 0 || throw(ZipError(zip_error_code(zip))) + return nothing +end + +""" + zip_add_dir!(zip::ZipArchive, dirname::String; flags::UInt32 = LIBZIP_FL_OVERWRITE) + +Add a directory to a `zip` archive by the `dirname`, which will be created if it does not exist yet or overwritten if it does exist. + +See also [`Add file flags`](@ref add_file_flags) section. +""" +function zip_add_dir!(zip::ZipArchive, dirname::AbstractString; flags::UInt32 = LIBZIP_FL_OVERWRITE) + status = libzip_dir_add(zip.archive_ptr, dirname, flags) + status >= 0 || throw(ZipError(zip_error_code(zip))) + return nothing +end + +""" + zip_get_file_info(zip::ZipArchive, filename::String; flags::UInt32 = LIBZIP_FL_ENC_GUESS) + zip_get_file_info(zip::ZipArchive, index::Int; flags::UInt32 = LIBZIP_FL_ENC_GUESS) + +Return information about the `filename` in a `zip` archive. + +See also [`File info flags`](@ref file_info_flags) section. +""" +function zip_get_file_info end + +function zip_get_file_info(zip::ZipArchive, index::Int; flags::UInt32 = LIBZIP_FL_ENC_GUESS) + check_closed(zip) + info = LibZipStatT() + info_ptr = pointer_from_objref(info) + libzip_stat_init(info_ptr) + status = libzip_stat_index(zip.archive_ptr, index, flags, info_ptr) + iszero(status) || throw(ZipError(zip_error_code(zip))) + return info +end + +function zip_get_file_info( + zip::ZipArchive, + filename::AbstractString; + flags::UInt32 = LIBZIP_FL_ENC_GUESS, +) + return zip_get_file_info(zip, locate_file(zip, filename); flags) +end + +#__ Iterate + +Base.IteratorSize(::Type{ZipArchive}) = Base.HasLength() +Base.IteratorEltype(::Type{ZipArchive}) = Base.HasEltype() +Base.eltype(::Type{ZipArchive}) = ZipFile + +""" + length(zip::ZipArchive, flags::UInt32 = LIBZIP_FL_ENC_GUESS) + +Return the number of files in `zip` archive. + +See also [`Read file flags`](@ref read_file_flags) section. +""" +function Base.length(zip::ZipArchive, flags::UInt32 = LIBZIP_FL_ENC_GUESS) + check_closed(zip) + return libzip_get_num_entries(zip.archive_ptr, flags) +end + +function Base.iterate(zip::ZipArchive, state::Int = 0) + state >= length(zip) && return nothing + entry = ZipFile(read(zip, state), zip_get_file_info(zip, state)) + return (entry, state + 1) +end + +#__ IO + +""" + read(zip::ZipArchive, index::Int; kw...) -> Vector{UInt8} + read(zip::ZipArchive, filename::String; kw...) -> Vector{UInt8} + +Read the file contents of a `zip` archive by `index` or `filename`. + +## Keyword arguments +- `flags::UInt32`: Mode for a reading and name lookup (by default `LIBZIP_FL_ENC_GUESS`). +- `password::Union{Nothing,AbstractString}`: Password for an encrypted entry (by default `nothing`). + +See also [`Read file flags`](@ref read_file_flags) section. +""" +function Base.read( + zip::ZipArchive, + index::Int; + flags::UInt32 = LIBZIP_FL_ENC_GUESS, + password::Union{Nothing,AbstractString} = nothing, +) + check_closed(zip) + file_ptr = if password === nothing + libzip_fopen_index(zip.archive_ptr, index, flags) + else + libzip_fopen_index_encrypted(zip.archive_ptr, index, flags, password) + end + file_ptr == C_NULL && throw(ZipError(zip_error_code(zip))) + info = zip_get_file_info(zip, index) + buffer = Vector{UInt8}(undef, info.size) + status = libzip_fread(file_ptr, buffer, info.size) + status >= 0 || throw(ZipError(zip_error_code(zip))) + return buffer +end + +function Base.read(zip::ZipArchive, filename::AbstractString; kw...) + return read(zip, locate_file(zip, filename); kw...) +end + +""" + read!(zip::ZipArchive) -> Vector{UInt8} + +Read binary data and then close the `zip` archive. +""" +function Base.read!(zip::ZipArchive) + check_closed(zip) + close(zip) + info = LibZipStatT() + info_ptr = pointer_from_objref(info) + libzip_stat_init(info_ptr) + libzip_source_stat(zip.source_ptr, info_ptr) < 0 && throw(ZipError(source_error_code(zip))) + libzip_source_open(zip.source_ptr) < 0 && throw(ZipError(source_error_code(zip))) + len = info.size + buffer = Vector{UInt8}(undef, len) + libzip_source_read(zip.source_ptr, buffer, len) < 0 && throw(ZipError(source_error_code(zip))) + libzip_source_close(zip.source_ptr) + return buffer +end + +""" + write(zip::ZipArchive, filename::String, data::Vector{UInt8}; flags::UInt32 = LIBZIP_FL_OVERWRITE) + +Write the binary `data` to a `zip` archive, which will be created if it does not exist yet or overwritten if it does exist. + +See also [`Add file flags`](@ref add_file_flags) section. +""" +function Base.write( + zip::ZipArchive, + filename::AbstractString, + data::AbstractVector{UInt8}; + flags::UInt32 = LIBZIP_FL_OVERWRITE, +) + check_closed(zip) + status = libzip_file_add(zip.archive_ptr, filename, init_source(data), flags) + status >= 0 || throw(ZipError(zip_error_code(zip))) + return nothing +end + +""" + write(path::String, zip::ZipArchive) + +Write the `zip` archive binary data as a file by `path`. +""" +function Base.write(path::AbstractString, zip::ZipArchive) + check_closed(zip) + return write(path, read!(zip)) +end + +#__ Show + +function Base.show(io::IO, zip::ZipArchive) + println(io, " ZipArchive:") + if !isempty(zip.comment) + println(io, " Comment: \"$(zip.comment)\"") + end + if zip.closed + println(io, " 🔒 Archive is closed — access is restricted.") + else + println(io, " 🔓 Archive is open and ready for use!") + end + if !zip.closed + if length(zip) == 0 + println(io, " 📂 The archive is empty.") + else + println(io, " 📁 Files in archive:") + for i in 0:length(zip)-1 + file_info = zip_get_file_info(zip, i) + println(io, " 📄 $(unsafe_string(file_info.name)): $(file_info.size) bytes") + end + end + else + println(io, " Cannot display contents of a closed archive.") + end +end + +end diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 0000000..0c36332 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,2 @@ +[deps] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/data/binance_spot_tickers.json b/test/data/binance_spot_tickers.json new file mode 100644 index 0000000..8fe7c02 --- /dev/null +++ b/test/data/binance_spot_tickers.json @@ -0,0 +1,48 @@ +[ + { + "symbol":"BTCUSDT", + "askPrice":68077.16, + "askQty":12.79483, + "bidPrice":68077.15, + "bidQty":0.48951, + "closeTime":"2024-10-16T11:58:13.872", + "count":6996706, + "firstId":3924216037, + "highPrice":68424.0, + "lastId":3931212742, + "lastPrice":68077.16, + "lastQty":0.0048, + "lowPrice":64800.01, + "openPrice":65340.0, + "openTime":"2024-10-15T11:58:13.872", + "prevClosePrice":65340.0, + "priceChange":2737.16, + "priceChangePercent":4.189, + "quoteVolume":3.2214732188681374e9, + "volume":48248.82197, + "weightedAvgPrice":66767.91447615 + }, + { + "symbol":"ADAUSDT", + "askPrice":0.3603, + "askQty":30078.2, + "bidPrice":0.3602, + "bidQty":3571.2, + "closeTime":"2024-10-16T11:58:13.860999936", + "count":182034, + "firstId":509950062, + "highPrice":0.3711, + "lastId":510132095, + "lastPrice":0.3603, + "lastQty":1989.9, + "lowPrice":0.3475, + "openPrice":0.3547, + "openTime":"2024-10-15T11:58:13.860999936", + "prevClosePrice":0.3548, + "priceChange":0.0056, + "priceChangePercent":1.579, + "quoteVolume":4.682515520792e7, + "volume":1.309583227e8, + "weightedAvgPrice":0.35755769 + } + ] \ No newline at end of file diff --git a/test/data/encrypted_archive.zip b/test/data/encrypted_archive.zip new file mode 100644 index 0000000..7d64e68 Binary files /dev/null and b/test/data/encrypted_archive.zip differ diff --git a/test/data/simple_archive.zip b/test/data/simple_archive.zip new file mode 100644 index 0000000..aa09ca4 Binary files /dev/null and b/test/data/simple_archive.zip differ diff --git a/test/runtests.jl b/test/runtests.jl new file mode 100644 index 0000000..e39ebb6 --- /dev/null +++ b/test/runtests.jl @@ -0,0 +1,65 @@ +# runtests + +using LibZip +using Test + +@testset "LibZip" verbose=true begin + @testset "Test 1: ZipArchive open and write files" begin + zip = ZipArchive(; flags = LIBZIP_CREATE) + write(zip, "dir/new_file.json", read("data/binance_spot_tickers.json")) + zip_compress_file!(zip, "dir/new_file.json", LIBZIP_CM_DEFLATE; compression_level = 1) + info = zip_get_file_info(zip, 0) + @test unsafe_string(info.name) == "dir/new_file.json" + @test info.comp_method == 0x0008 + + csv = b""" + "id","name","grade" + 1,"Fred",78.2 + 2,"Benny",82.0 + """ + write(zip, "simple_csv.csv", csv) + info = zip_get_file_info(zip, 1) + @test unsafe_string(info.name) == "simple_csv.csv" + + zip_add_dir!(zip, "empty_dir/") + @test length(zip) == 3 + end + + @testset "Test 2: Iterator ZipArchive" begin + zip = zip_open("data/simple_archive.zip") + @test zip.comment |> isempty + @test length(collect(zip)) == 4 + close(zip) + + zip = ZipArchive(read("data/encrypted_archive.zip")) + @test zip.comment == "password: 1234" + zip_default_password!(zip, "1234") + @test length(collect(zip)) == 4 + end + + @testset "Test 3: ZipError" begin + @test_throws ZipError zip_open("data/simple_archive.zip"; flags = LIBZIP_EXCL) + @test_throws ZipError ZipArchive(read("data/simple_archive.zip"); flags = LIBZIP_EXCL) + zip = ZipArchive(; flags = LIBZIP_CREATE) + write(zip, "test.json", read("data/binance_spot_tickers.json")) + @test_throws ZipError write(zip, "test.json", read("data/binance_spot_tickers.json"); flags = LIBZIP_FL_ENC_GUESS) + @test_throws ZipError zip_compress_file!(zip, 0, LIBZIP_CM_DEFLATE64) + @test_throws ZipError zip_encrypt_file!(zip, 0, "1234"; method = UInt16(2)) + @test_throws ZipError read(zip, 1) + @test_throws ZipError zip_get_file_info(zip, 1) + close(zip) + @test_throws AssertionError length(zip) + end + + @testset "Test 4: ZipArchive to vector" begin + zip1 = ZipArchive(; flags = LIBZIP_CREATE) + write(zip1, "text.txt", b"text") + buffer = read!(zip1) + @test !isopen(zip1) + + zip2 = ZipArchive(buffer; flags = LIBZIP_CREATE) + @test length(zip2) == 1 + @test_nowarn write(zip2, "text2.txt", b"text2") + @test length(zip2) == 2 + end +end