diff options
author | Mario <mario@mariovavti.com> | 2024-11-09 10:24:26 +0000 |
---|---|---|
committer | Mario <mario@mariovavti.com> | 2024-11-09 10:24:26 +0000 |
commit | 0ed08274f16d65b427bd4a5bbd8bd5bd6b2a65c2 (patch) | |
tree | 25b05973f824b95fc5705cf8aa79b86a44cfde00 /vendor/maennchen/zipstream-php | |
parent | 2a152e0803309eb3646316bbe0d2a47353bad2b9 (diff) | |
parent | 0534fe68869aae231259ee48a38b4533f3f1ff99 (diff) | |
download | volse-hubzilla-0ed08274f16d65b427bd4a5bbd8bd5bd6b2a65c2.tar.gz volse-hubzilla-0ed08274f16d65b427bd4a5bbd8bd5bd6b2a65c2.tar.bz2 volse-hubzilla-0ed08274f16d65b427bd4a5bbd8bd5bd6b2a65c2.zip |
Merge branch 'clean-up-some-dependencies' into 'dev'
Clean up deps and upgrade EpubMeta
See merge request hubzilla/core!2162
Diffstat (limited to 'vendor/maennchen/zipstream-php')
91 files changed, 6415 insertions, 0 deletions
diff --git a/vendor/maennchen/zipstream-php/.editorconfig b/vendor/maennchen/zipstream-php/.editorconfig new file mode 100644 index 000000000..f7cd91427 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.editorconfig @@ -0,0 +1,22 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.{yml,md,xml}] +indent_style = space +indent_size = 2 + +[*.{rst,php}] +indent_style = space +indent_size = 4 + +[composer.json] +indent_style = space +indent_size = 2 + +[composer.lock] +indent_style = space +indent_size = 4 diff --git a/vendor/maennchen/zipstream-php/.gitattributes b/vendor/maennchen/zipstream-php/.gitattributes new file mode 100644 index 000000000..e058ebd0a --- /dev/null +++ b/vendor/maennchen/zipstream-php/.gitattributes @@ -0,0 +1,6 @@ +.gitignore text eol=lf +.gitattributes text eol=lf +*.md text eol=lf +*.php text eol=lf +*.yml text eol=lf +*.xml text eol=lf diff --git a/vendor/maennchen/zipstream-php/.github/CODE_OF_CONDUCT.md b/vendor/maennchen/zipstream-php/.github/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..9d75b8763 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +jonatan@maennchen.ch. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][mozilla coc]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][faq]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[mozilla coc]: https://github.com/mozilla/diversity +[faq]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/vendor/maennchen/zipstream-php/.github/CONTRIBUTING.md b/vendor/maennchen/zipstream-php/.github/CONTRIBUTING.md new file mode 100644 index 000000000..d8caee081 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/CONTRIBUTING.md @@ -0,0 +1,139 @@ +# Contributing to ZipStream-PHP + +## Welcome! + +We look forward to your contributions! Here are some examples how you can +contribute: + +- [Report a bug](https://github.com/maennchen/ZipStream-PHP/issues/new?labels=bug&template=BUG.md) +- [Propose a new feature](https://github.com/maennchen/ZipStream-PHP/issues/new?labels=enhancement&template=FEATURE.md) +- [Send a pull request](https://github.com/maennchen/ZipStream-PHP/pulls) + +## We have a Code of Conduct + +Please note that this project is released with a +[Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this +project you agree to abide by its terms. + +## Any contributions you make will be under the MIT License + +When you submit code changes, your submissions are understood to be under the +same [MIT License](https://github.com/maennchen/ZipStream-PHP/blob/main/LICENSE) +that covers the project. By contributing to this project, you agree that your +contributions will be licensed under its MIT License. + +## Write bug reports with detail, background, and sample code + +In your bug report, please provide the following: + +- A quick summary and/or background +- Steps to reproduce + - Be specific! + - Give sample code if you can. +- What you expected would happen +- What actually happens +- Notes (possibly including why you think this might be happening, or stuff you +- tried that didn't work) + +Please do not report a bug for a version of ZIPStream-PHP that is no longer +supported (`< 3.0.0`). Please do not report a bug if you are using a version of +PHP that is not supported by the version of ZipStream-PHP you are using. + +Please post code and output as text +([using proper markup](https://guides.github.com/features/mastering-markdown/)). +Do not post screenshots of code or output. + +Please include the output of `composer info | sort`. + +## Workflow for Pull Requests + +1. Fork the repository. +2. Create your branch from `main` if you plan to implement new functionality or + change existing code significantly; create your branch from the oldest branch + that is affected by the bug if you plan to fix a bug. +3. Implement your change and add tests for it. +4. Ensure the test suite passes. +5. Ensure the code complies with our coding guidelines (see below). +6. Send that pull request! + +Please make sure you have +[set up your user name and email address](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup) +for use with Git. Strings such as `silly nick name <root@localhost>` look really +stupid in the commit history of a project. + +We encourage you to +[sign your Git commits with your GPG key](https://docs.github.com/en/github/authenticating-to-github/signing-commits). + +Pull requests for new features must be based on the `main` branch. + +We are trying to keep backwards compatibility breaks in ZipStream-PHP to a +minimum. Please take this into account when proposing changes. + +Due to time constraints, we are not always able to respond as quickly as we +would like. Please do not take delays personal and feel free to remind us if you +feel that we forgot to respond. + +## Coding Guidelines + +This project comes with a configuration file (located at `/psalm.yml` in the +repository) that you can use to perform static analysis (with a focus on type +checking): + +```bash +$ .composer run test:lint +``` + +This project comes with a configuration file (located at +`/.php-cs-fixer.dist.php` in the repository) that you can use to (re)format your +source code for compliance with this project's coding guidelines: + +```bash +$ composer run format +``` + +Please understand that we will not accept a pull request when its changes +violate this project's coding guidelines. + +## Using ZipStream-PHP from a Git checkout + +The following commands can be used to perform the initial checkout of +ZipStream-PHP: + +```bash +$ git clone git@github.com:maennchen/ZipStream-PHP.git + +$ cd ZipStream-PHP +``` + +Install ZipStream-PHP's dependencies using [Composer](https://getcomposer.org/): + +```bash +$ composer install +$ composer run install:tools # Install phpDocumentor using phive +``` + +## Running ZipStream-PHP's test suite + +After following the steps shown above, ZipStream-PHP's test suite is run like +this: + +```bash +$ composer run test:unit +``` + +There's some slow tests in the test suite that test the handling of big files in +the archives. To skip them use the following command instead: + +```bash +$ composer run test:unit:fast +``` + +## Generating ZipStream-PHP Documentation + +To generate the documentation for the library, run: + +```bash +$ composer run docs:generate +``` + +The guide documentation pages can be found in the `/guides/` directory. diff --git a/vendor/maennchen/zipstream-php/.github/FUNDING.yml b/vendor/maennchen/zipstream-php/.github/FUNDING.yml new file mode 100644 index 000000000..5a4612769 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/FUNDING.yml @@ -0,0 +1 @@ +github: maennchen diff --git a/vendor/maennchen/zipstream-php/.github/ISSUE_TEMPLATE/BUG.yml b/vendor/maennchen/zipstream-php/.github/ISSUE_TEMPLATE/BUG.yml new file mode 100644 index 000000000..0eb8cc772 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/ISSUE_TEMPLATE/BUG.yml @@ -0,0 +1,71 @@ +name: 🐞 Bug Report +description: Something is broken? +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + - Create a discussion instead if you are looking for support: + https://github.com/maennchen/ZipStream-PHP/discussions + - type: input + id: version + attributes: + label: ZipStream-PHP version + placeholder: x.y.z + validations: + required: true + - type: input + id: php-version + attributes: + label: PHP version + placeholder: x.y.z + validations: + required: true + - type: checkboxes + id: constraints + attributes: + label: Constraints for Bug Report + options: + - label: | + I'm using a version of ZipStream that is currently supported: + https://github.com/maennchen/ZipStream-PHP#version-support + required: true + - label: | + I'm using a version of PHP that has active support: + https://www.php.net/supported-versions.php + required: true + - label: | + I'm using a version of PHP that is compatible with your used + ZipStream version. + required: true + - label: | + I'm using the latest release of the used ZipStream major version. + required: true + - type: textarea + id: summary + attributes: + label: Summary + description: Provide a summary describing the problem you are experiencing. + validations: + required: true + - type: textarea + id: current-behaviour + attributes: + label: Current behavior + description: What is the current (buggy) behavior? + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: How to reproduce + description: Provide steps to reproduce the bug. + validations: + required: true + - type: textarea + id: expected-behaviour + attributes: + label: Expected behavior + description: What was the expected (correct) behavior? + validations: + required: true diff --git a/vendor/maennchen/zipstream-php/.github/ISSUE_TEMPLATE/FEATURE.yml b/vendor/maennchen/zipstream-php/.github/ISSUE_TEMPLATE/FEATURE.yml new file mode 100644 index 000000000..e5dec6371 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/ISSUE_TEMPLATE/FEATURE.yml @@ -0,0 +1,11 @@ +name: 🎉 Feature Request +description: You have a neat idea that should be implemented? +labels: ["enhancement"] +body: + - type: textarea + id: description + attributes: + label: Description + description: Provide a summary of the feature you would like to see implemented. + validations: + required: true diff --git a/vendor/maennchen/zipstream-php/.github/PULL_REQUEST_TEMPLATE.md b/vendor/maennchen/zipstream-php/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..6892c571b --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,6 @@ +Please go the the `Preview` tab and select the appropriate sub-template: + +* [🐞 Failing Test](?expand=1&template=FAILING_TEST.md) +* [🐞 Bug Fix](?expand=1&template=FIX.md) +* [⚙ Improvement](?expand=1&template=IMPROVEMENT.md) +* [🎉 New Feature](?expand=1&template=NEW_FEATURE.md) diff --git a/vendor/maennchen/zipstream-php/.github/PULL_REQUEST_TEMPLATE/FAILING_TEST.md b/vendor/maennchen/zipstream-php/.github/PULL_REQUEST_TEMPLATE/FAILING_TEST.md new file mode 100644 index 000000000..24603cb63 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/PULL_REQUEST_TEMPLATE/FAILING_TEST.md @@ -0,0 +1,13 @@ +<!--- +name: 🐞 Failing Test +about: You found a bug and have a failing test? +labels: bug, tests +---> + +<!-- +- Please do not send a pull request for an issue in a version of ZipStream-PHP + that is no longer supported. + See: https://github.com/maennchen/ZipStream-PHP#version-support +- Please target the oldest branch of ZipStream-PHP that is still supported and + where the test fails. +--> diff --git a/vendor/maennchen/zipstream-php/.github/PULL_REQUEST_TEMPLATE/FIX.md b/vendor/maennchen/zipstream-php/.github/PULL_REQUEST_TEMPLATE/FIX.md new file mode 100644 index 000000000..77f65a080 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/PULL_REQUEST_TEMPLATE/FIX.md @@ -0,0 +1,13 @@ +<!--- +name: 🐞 Bug Fix +about: You have a fix for a bug? +labels: bug +---> + +<!-- +- Please do not send a pull request for an issue in a version of ZipStream-PHP + that is no longer supported. + See: https://github.com/maennchen/ZipStream-PHP#version-support +- Please target the oldest branch of ZipStream-PHP that is still supported and + affected by this bug. +--> diff --git a/vendor/maennchen/zipstream-php/.github/PULL_REQUEST_TEMPLATE/IMPROVEMENT.md b/vendor/maennchen/zipstream-php/.github/PULL_REQUEST_TEMPLATE/IMPROVEMENT.md new file mode 100644 index 000000000..3ac8e3100 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/PULL_REQUEST_TEMPLATE/IMPROVEMENT.md @@ -0,0 +1,9 @@ +<!--- +name: ⚙ Improvement +about: You have some improvement to make ZipStream-PHP better? +labels: enhancement +---> + +<!-- +- Please target the `main` branch of ZipStream-PHP. +--> diff --git a/vendor/maennchen/zipstream-php/.github/PULL_REQUEST_TEMPLATE/NEW_FEATURE.md b/vendor/maennchen/zipstream-php/.github/PULL_REQUEST_TEMPLATE/NEW_FEATURE.md new file mode 100644 index 000000000..ca53939c8 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/PULL_REQUEST_TEMPLATE/NEW_FEATURE.md @@ -0,0 +1,9 @@ +<!--- +name: 🎉 New Feature +about: You have implemented some neat idea that you want to make part of ZipStream-PHP? +labels: type/enhancement +---> + +<!-- +- Please target the `main` branch of ZipStream-PHP. +--> diff --git a/vendor/maennchen/zipstream-php/.github/SECURITY.md b/vendor/maennchen/zipstream-php/.github/SECURITY.md new file mode 100644 index 000000000..3046c3107 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/SECURITY.md @@ -0,0 +1,22 @@ +# Security Policy + +[![OpenSSF Vulnerability Disclosure](https://img.shields.io/badge/OpenSSF-Vulnerability_Disclosure-green)](https://github.com/ossf/oss-vulnerability-guide/blob/main/finder-guide.md) +[![GitHub Report](https://img.shields.io/badge/GitHub-Security_Advisories-blue)](https://github.com/maennchen/ZipStream-PHP/security/advisories/new) +[![Email Report](https://img.shields.io/badge/Email-jonatan%40maennchen.ch-blue)](mailto:jonatan@maennchen.ch) + +This repository follows the +[OpenSSF Vulnerability Disclosure guide](https://github.com/ossf/oss-vulnerability-guide/tree/main). +You can learn more about it in the +[Finders Guide](https://github.com/ossf/oss-vulnerability-guide/blob/main/finder-guide.md). + +Please report vulnerabilities via the +[GitHub Security Vulnerability Reporting](https://github.com/maennchen/ZipStream-PHP/security/advisories/new) +or via email to [`jonatan@maennchen.ch`](mailto:jonatan@maennchen.ch) if this does +not work for you. + +Our vulnerability management team will respond within 3 working days of your +report. If the issue is confirmed as a vulnerability, we will open a Security +Advisory. This project follows a 90 day disclosure timeline. + +If you have questions about reporting security issues, email the vulnerability +management team: [`jonatan@maennchen.ch`](mailto:jonatan@maennchen.ch) diff --git a/vendor/maennchen/zipstream-php/.github/dependabot.yml b/vendor/maennchen/zipstream-php/.github/dependabot.yml new file mode 100644 index 000000000..6056437b9 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + github-actions: + applies-to: version-updates
\ No newline at end of file diff --git a/vendor/maennchen/zipstream-php/.github/scorecard.yml b/vendor/maennchen/zipstream-php/.github/scorecard.yml new file mode 100644 index 000000000..219fc0bfd --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/scorecard.yml @@ -0,0 +1,14 @@ +annotations: + - checks: + - fuzzing + reasons: + - reason: not-applicable # PHP is memory safe + - checks: + - packaging + reasons: + - reason: not-supported # Using Composer + - checks: + - signed-releases + reasons: + - reason: not-applicable # Releases are distributed via Composer + diff --git a/vendor/maennchen/zipstream-php/.github/workflows/branch_main.yml b/vendor/maennchen/zipstream-php/.github/workflows/branch_main.yml new file mode 100644 index 000000000..15ff2782c --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/workflows/branch_main.yml @@ -0,0 +1,24 @@ +on: + push: + branches: + - "main" + +name: "Main Branch" + +permissions: + contents: read + +jobs: + test: + name: "Test" + + permissions: + contents: read + security-events: write + + uses: ./.github/workflows/part_test.yml + + docs: + name: "Docs" + + uses: ./.github/workflows/part_docs.yml diff --git a/vendor/maennchen/zipstream-php/.github/workflows/part_dependabot.yml b/vendor/maennchen/zipstream-php/.github/workflows/part_dependabot.yml new file mode 100644 index 000000000..77e466b81 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/workflows/part_dependabot.yml @@ -0,0 +1,30 @@ +on: + workflow_call: {} + +name: "Dependabot" + +permissions: + contents: read + +jobs: + automerge_dependabot: + name: "Automerge PRs" + + runs-on: ubuntu-latest + + permissions: + pull-requests: write + contents: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + + - uses: fastify/github-action-merge-dependabot@3892334d1c649bb8119af3d22a3f3766bd5e593f # v3.10.2 + with: + github-token: ${{ github.token }} + use-github-auto-merge: true + # Major Updates need to be merged manually + target: minor diff --git a/vendor/maennchen/zipstream-php/.github/workflows/part_docs.yml b/vendor/maennchen/zipstream-php/.github/workflows/part_docs.yml new file mode 100644 index 000000000..7af16f3be --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/workflows/part_docs.yml @@ -0,0 +1,51 @@ +on: + workflow_call: {} + +name: "Documentation" + +permissions: + contents: read + +jobs: + generate: + name: "Generate" + + runs-on: ubuntu-latest + + steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + + - name: Checkout Code + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: SetUp PHP + id: setup-php + uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2 + with: + php-version: "8.3" + tools: phive + - name: Cache Tools + uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + id: cache + with: + path: ~/.phive + key: tools-${{ runner.os }}-${{ steps.setup-php.outputs.php-version }}-${{ hashFiles('**/phars.xml') }} + restore-keys: | + tools-${{ runner.os }}-${{ steps.setup-php.outputs.php-version }}- + tools-${{ steps.setup-php.outputs.php-version }}- + tools- + - name: Install Tools + run: composer run install:tools + - name: Generate Docs + run: composer run docs:generate + - uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 + with: + name: docs + path: docs + - name: Package for GitHub Pages + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 + with: + path: docs + diff --git a/vendor/maennchen/zipstream-php/.github/workflows/part_release.yml b/vendor/maennchen/zipstream-php/.github/workflows/part_release.yml new file mode 100644 index 000000000..c0f3867df --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/workflows/part_release.yml @@ -0,0 +1,94 @@ +on: + workflow_call: + inputs: + releaseName: + required: true + type: string + stable: + required: false + type: boolean + default: false + +name: "Release" + +permissions: + contents: read + +jobs: + create: + name: Create Release + + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + + - name: Create prerelease + if: ${{ !inputs.stable }} + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + gh release create \ + --repo ${{ github.repository }} \ + --title ${{ inputs.releaseName }} \ + --prerelease \ + --generate-notes \ + ${{ inputs.releaseName }} + + - name: Create release + if: ${{ inputs.stable }} + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + gh release create \ + --repo ${{ github.repository }} \ + --title ${{ inputs.releaseName }} \ + --generate-notes \ + ${{ inputs.releaseName }} + + upload_release: + name: "Upload" + + needs: ["create"] + + runs-on: ubuntu-latest + + permissions: + id-token: write + contents: write + attestations: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: docs + path: docs + - run: | + tar -czvf docs.tar.gz docs + - name: "Attest Documentation" + id: attestation + uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 + with: + subject-path: "docs.tar.gz" + - name: Copy Attestation + run: cp "$ATTESTATION" docs.tar.gz.sigstore + env: + ATTESTATION: "${{ steps.attestation.outputs.bundle-path }}" + - name: Upload + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + gh release upload --clobber "${{ github.ref_name }}" \ + docs.tar.gz docs.tar.gz.sigstore diff --git a/vendor/maennchen/zipstream-php/.github/workflows/part_test.yml b/vendor/maennchen/zipstream-php/.github/workflows/part_test.yml new file mode 100644 index 000000000..ccf4d660e --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/workflows/part_test.yml @@ -0,0 +1,183 @@ +on: + workflow_call: + +name: "Test" + +permissions: + contents: read + +jobs: + phpunit: + name: PHPUnit (PHP ${{ matrix.php }} on ${{ matrix.os }}) + + runs-on: ${{ matrix.os }} + + continue-on-error: ${{ matrix.experimental }} + + strategy: + fail-fast: false + matrix: + php: ["8.1", "8.2", "8.3"] + os: [ubuntu-latest] + experimental: [false] + include: + - php: nightly + os: ubuntu-latest + experimental: true + - php: "8.3" + os: windows-latest + experimental: false + - php: "8.3" + os: macos-latest + experimental: false + + steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + + - name: Checkout Code + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: SetUp PHP + id: setup-php + uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2 + with: + php-version: "${{ matrix.php }}" + tools: phpunit + coverage: xdebug + extensions: xdebug,zip + - name: Get composer cache directory + id: composer-cache-common + if: "${{ runner.os != 'Windows' }}" + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Get composer cache directory + id: composer-cache-windows + if: "${{ runner.os == 'Windows' }}" + run: echo "dir=$(composer config cache-files-dir)" >> $env:GITHUB_OUTPUT + - name: Cache Deps + uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + id: cache + with: + path: ${{ steps.composer-cache-common.outputs.dir }}${{ steps.composer-cache-windows.outputs.dir }} + key: deps-${{ runner.os }}-${{ steps.setup-php.outputs.php-version }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + deps-${{ runner.os }}-${{ steps.setup-php.outputs.php-version }}-composer- + deps-${{ runner.os }}-${{ steps.setup-php.outputs.php-version }}- + deps-${{ steps.setup-php.outputs.php-version }}- + deps- + - name: Install Deps + if: matrix.php != 'nightly' + run: composer install --prefer-dist + - name: Install Deps (ignore PHP requirement) + if: matrix.php == 'nightly' + run: composer install --prefer-dist --ignore-platform-req=php+ + - name: Run PHPUnit + run: composer run test:unit + env: + XDEBUG_MODE: coverage + - name: Upload coverage results to Coveralls + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_PARALLEL: true + COVERALLS_FLAG_NAME: ${{ runner.os }}-${{ steps.setup-php.outputs.php-version }} + run: composer run coverage:report + continue-on-error: ${{ matrix.experimental }} + + mark_coverage_done: + needs: ["phpunit"] + + runs-on: ubuntu-latest + + steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + + - name: Coveralls Finished + uses: coverallsapp/github-action@643bc377ffa44ace6394b2b5d0d3950076de9f63 # v2.3.0 + with: + github-token: ${{ secrets.github_token }} + parallel-finished: true + + psalm: + name: Run Psalm + + runs-on: "ubuntu-latest" + + permissions: + security-events: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + + - name: Checkout Code + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: SetUp PHP + id: setup-php + uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2 + with: + php-version: "8.3" + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache Deps + uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + id: cache + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: deps-${{ runner.os }}-${{ steps.setup-php.outputs.php-version }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + deps-${{ runner.os }}-${{ steps.setup-php.outputs.php-version }}-composer- + deps-${{ runner.os }}-${{ steps.setup-php.outputs.php-version }}- + deps-${{ steps.setup-php.outputs.php-version }}- + deps- + - name: Install Deps + run: composer install --prefer-dist + - name: Run Psalm + run: composer run test:lint -- --report=results.sarif + - name: "Upload SARIF" + uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3 + with: + sarif_file: results.sarif + + php-cs: + name: Run PHP-CS + + runs-on: "ubuntu-latest" + + steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + + - name: Checkout Code + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: SetUp PHP + id: setup-php + uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2 + with: + php-version: "8.3" + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache Deps + uses: actions/cache@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 + id: cache + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: deps-${{ runner.os }}-${{ steps.setup-php.outputs.php-version }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + deps-${{ runner.os }}-${{ steps.setup-php.outputs.php-version }}-composer- + deps-${{ runner.os }}-${{ steps.setup-php.outputs.php-version }}- + deps-${{ steps.setup-php.outputs.php-version }}- + deps- + - name: Install Deps + run: composer install --prefer-dist + - name: Run PHP-CS + run: composer run test:formatted diff --git a/vendor/maennchen/zipstream-php/.github/workflows/pr.yml b/vendor/maennchen/zipstream-php/.github/workflows/pr.yml new file mode 100644 index 000000000..05259d4f7 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/workflows/pr.yml @@ -0,0 +1,50 @@ +on: + pull_request: + branches: + - "*" + workflow_dispatch: {} + +name: "Pull Request" + +permissions: + contents: read + +jobs: + test: + name: "Test" + + permissions: + contents: read + security-events: write + + uses: ./.github/workflows/part_test.yml + + docs: + name: "Docs" + + uses: ./.github/workflows/part_docs.yml + + dependabot: + name: "Dependabot" + + if: ${{ github.actor == 'dependabot[bot]'}} + + permissions: + pull-requests: write + contents: write + + uses: ./.github/workflows/part_dependabot.yml + + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + + - name: 'Checkout Repository' + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + - name: 'Dependency Review' + uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 diff --git a/vendor/maennchen/zipstream-php/.github/workflows/scorecard.yml b/vendor/maennchen/zipstream-php/.github/workflows/scorecard.yml new file mode 100644 index 000000000..7bb8dbb6c --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/workflows/scorecard.yml @@ -0,0 +1,78 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '28 11 * * 3' + push: + branches: [ "main" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + + - name: "Checkout code" + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4.4.1 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 + with: + sarif_file: results.sarif diff --git a/vendor/maennchen/zipstream-php/.github/workflows/tag-beta.yml b/vendor/maennchen/zipstream-php/.github/workflows/tag-beta.yml new file mode 100644 index 000000000..b3399454a --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/workflows/tag-beta.yml @@ -0,0 +1,29 @@ +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+" + +name: "Beta Tag" + +permissions: + contents: read + +jobs: + docs: + name: "Docs" + + uses: ./.github/workflows/part_docs.yml + + release: + name: "Release" + + needs: ["docs"] + + permissions: + id-token: write + contents: write + attestations: write + + uses: ./.github/workflows/part_release.yml + with: + releaseName: "${{ github.ref_name }}" diff --git a/vendor/maennchen/zipstream-php/.github/workflows/tag-stable.yml b/vendor/maennchen/zipstream-php/.github/workflows/tag-stable.yml new file mode 100644 index 000000000..0e91cf0d0 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.github/workflows/tag-stable.yml @@ -0,0 +1,55 @@ +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" + +name: "Stable Tag" + +permissions: + contents: read + +jobs: + docs: + name: "Docs" + + uses: ./.github/workflows/part_docs.yml + + release: + name: "Release" + + needs: ["docs"] + + permissions: + id-token: write + contents: write + attestations: write + + uses: ./.github/workflows/part_release.yml + with: + releaseName: "${{ github.ref_name }}" + stable: true + + deploy_pages: + name: "Deploy to GitHub Pages" + + needs: ["release", "docs"] + + runs-on: ubuntu-latest + + permissions: + pages: write + id-token: write + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 diff --git a/vendor/maennchen/zipstream-php/.gitignore b/vendor/maennchen/zipstream-php/.gitignore new file mode 100644 index 000000000..e52a49877 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.gitignore @@ -0,0 +1,12 @@ +/composer.lock +/cov +/coverage.clover.xml +/docs +.idea +/.php-cs-fixer.cache +/.phpdoc/cache +/.phpunit.result.cache +/phpunit.xml +/.phpunit.cache +/tools +/vendor diff --git a/vendor/maennchen/zipstream-php/.phive/phars.xml b/vendor/maennchen/zipstream-php/.phive/phars.xml new file mode 100644 index 000000000..183927b12 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.phive/phars.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<phive xmlns="https://phar.io/phive"> + <phar name="phpdocumentor" version="^3.3.1" installed="3.4.3" location="./tools/phpdocumentor" copy="false"/> +</phive> diff --git a/vendor/maennchen/zipstream-php/.php-cs-fixer.dist.php b/vendor/maennchen/zipstream-php/.php-cs-fixer.dist.php new file mode 100644 index 000000000..38d6a7658 --- /dev/null +++ b/vendor/maennchen/zipstream-php/.php-cs-fixer.dist.php @@ -0,0 +1,70 @@ +<?php + +declare(strict_types=1); + +/** + * PHP-CS-Fixer config for ZipStream-PHP + * @author Nicolas CARPi <nico-git@deltablot.email> + * @copyright 2022 Nicolas CARPi + * @see https://github.com/maennchen/ZipStream-PHP + * @license MIT + * @package maennchen/ZipStream-PHP + */ + +use PhpCsFixer\Config; +use PhpCsFixer\Finder; + +$finder = Finder::create() + ->exclude('.github') + ->exclude('.phpdoc') + ->exclude('docs') + ->exclude('tools') + ->exclude('vendor') + ->in(__DIR__); + +$config = new Config(); +return $config->setRules([ + '@PER' => true, + '@PER:risky' => true, + '@PHP82Migration' => true, + '@PHPUnit84Migration:risky' => true, + 'array_syntax' => ['syntax' => 'short'], + 'class_attributes_separation' => true, + 'declare_strict_types' => true, + 'dir_constant' => true, + 'is_null' => true, + 'no_homoglyph_names' => true, + 'no_null_property_initialization' => true, + 'no_php4_constructor' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'non_printable_character' => true, + 'ordered_imports' => true, + 'ordered_class_elements' => true, + 'php_unit_construct' => true, + 'pow_to_exponentiation' => true, + 'psr_autoloading' => true, + 'random_api_migration' => true, + 'return_assignment' => true, + 'self_accessor' => true, + 'semicolon_after_instruction' => true, + 'short_scalar_cast' => true, + 'simplified_null_return' => true, + 'single_class_element_per_statement' => true, + 'single_line_comment_style' => true, + 'single_quote' => true, + 'space_after_semicolon' => true, + 'standardize_not_equals' => true, + 'strict_param' => true, + 'ternary_operator_spaces' => true, + 'trailing_comma_in_multiline' => true, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'global_namespace_import' => [ + 'import_classes' => true, + 'import_functions' => true, + 'import_constants' => true, + ], + ]) + ->setFinder($finder) + ->setRiskyAllowed(true); diff --git a/vendor/maennchen/zipstream-php/.phpdoc/template/base.html.twig b/vendor/maennchen/zipstream-php/.phpdoc/template/base.html.twig new file mode 100644 index 000000000..b7507fb9c --- /dev/null +++ b/vendor/maennchen/zipstream-php/.phpdoc/template/base.html.twig @@ -0,0 +1,15 @@ +{% extends 'layout.html.twig' %} + +{% set topMenu = { + "menu": [ + { "name": "Guides", "url": "https://maennchen.dev/ZipStream-PHP/guide/index.html"}, + { "name": "API", "url": "https://maennchen.dev/ZipStream-PHP/classes/ZipStream-ZipStream.html"}, + { "name": "Issues", "url": "https://github.com/maennchen/ZipStream-PHP/issues"}, + ], + "social": [ + { "iconClass": "fab fa-github", "url": "https://github.com/maennchen/ZipStream-PHP"}, + { "iconClass": "fas fa-envelope-open-text", "url": "https://github.com/maennchen/ZipStream-PHP/discussions"}, + { "iconClass": "fas fa-money-bill", "url": "https://opencollective.com/zipstream"}, + ] +} +%}
\ No newline at end of file diff --git a/vendor/maennchen/zipstream-php/.tool-versions b/vendor/maennchen/zipstream-php/.tool-versions new file mode 100644 index 000000000..4a3dc9dfd --- /dev/null +++ b/vendor/maennchen/zipstream-php/.tool-versions @@ -0,0 +1 @@ +php 8.3.1 diff --git a/vendor/maennchen/zipstream-php/LICENSE b/vendor/maennchen/zipstream-php/LICENSE new file mode 100644 index 000000000..ebe7fe2f8 --- /dev/null +++ b/vendor/maennchen/zipstream-php/LICENSE @@ -0,0 +1,24 @@ +MIT License + +Copyright (C) 2007-2009 Paul Duncan <pabs@pablotron.org> +Copyright (C) 2014 Jonatan Männchen <jonatan@maennchen.ch> +Copyright (C) 2014 Jesse G. Donat <donatj@gmail.com> +Copyright (C) 2018 Nicolas CARPi <nicolas.carpi@curie.fr> + +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/vendor/maennchen/zipstream-php/README.md b/vendor/maennchen/zipstream-php/README.md new file mode 100644 index 000000000..858add09f --- /dev/null +++ b/vendor/maennchen/zipstream-php/README.md @@ -0,0 +1,154 @@ +# ZipStream-PHP + +[![Main Branch](https://github.com/maennchen/ZipStream-PHP/actions/workflows/branch_main.yml/badge.svg)](https://github.com/maennchen/ZipStream-PHP/actions/workflows/branch_main.yml) +[![Coverage Status](https://coveralls.io/repos/github/maennchen/ZipStream-PHP/badge.svg?branch=main)](https://coveralls.io/github/maennchen/ZipStream-PHP?branch=main) +[![Latest Stable Version](https://poser.pugx.org/maennchen/zipstream-php/v/stable)](https://packagist.org/packages/maennchen/zipstream-php) +[![Total Downloads](https://poser.pugx.org/maennchen/zipstream-php/downloads)](https://packagist.org/packages/maennchen/zipstream-php) +[![Financial Contributors on Open Collective](https://opencollective.com/zipstream/all/badge.svg?label=financial+contributors)](https://opencollective.com/zipstream) [![License](https://img.shields.io/github/license/maennchen/zipstream-php.svg)](LICENSE) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9524/badge)](https://www.bestpractices.dev/projects/9524) +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/maennchen/ZipStream-PHP/badge)](https://scorecard.dev/viewer/?uri=github.com/maennchen/ZipStream-PHP) + +## Unstable Branch + +The `main` branch is not stable. Please see the +[releases](https://github.com/maennchen/ZipStream-PHP/releases) for a stable +version. + +## Overview + +A fast and simple streaming zip file downloader for PHP. Using this library will +save you from having to write the Zip to disk. You can directly send it to the +user, which is much faster. It can work with S3 buckets or any PSR7 Stream. + +Please see the [LICENSE](LICENSE) file for licensing and warranty information. + +## Installation + +Simply add a dependency on maennchen/zipstream-php to your project's +`composer.json` file if you use Composer to manage the dependencies of your +project. Use following command to add the package to your project's dependencies: + +```bash +composer require maennchen/zipstream-php +``` + +## Usage + +For detailed instructions, please check the +[Documentation](https://maennchen.github.io/ZipStream-PHP/). + +```php +// Autoload the dependencies +require 'vendor/autoload.php'; + +// create a new zipstream object +$zip = new ZipStream\ZipStream( + outputName: 'example.zip', + + // enable output of HTTP headers + sendHttpHeaders: true, +); + +// create a file named 'hello.txt' +$zip->addFile( + fileName: 'hello.txt', + data: 'This is the contents of hello.txt', +); + +// add a file named 'some_image.jpg' from a local file 'path/to/image.jpg' +$zip->addFileFromPath( + fileName: 'some_image.jpg', + path: 'path/to/image.jpg', +); + +// finish the zip stream +$zip->finish(); +``` + +## Upgrade to version 3.0.0 + +### General + +- Minimum PHP Version: `8.1` +- Only 64bit Architecture is supported. +- The class `ZipStream\Option\Method` has been replaced with the enum + `ZipStream\CompressionMethod`. +- Most clases have been flagged as `@internal` and should not be used from the + outside. + If you're using internal resources to extend this library, please open an + issue so that a clean interface can be added & published. + The externally available classes & enums are: + - `ZipStream\CompressionMethod` + - `ZipStream\Exception*` + - `ZipStream\ZipStream` + +### Archive Options + +- The class `ZipStream\Option\Archive` has been replaced in favor of named + arguments in the `ZipStream\ZipStream` constuctor. +- The archive options `largeFileSize` & `largeFileMethod` has been removed. If + you want different `compressionMethods` based on the file size, you'll have to + implement this yourself. +- The archive option `httpHeaderCallback` changed the type from `callable` to + `Closure`. +- The archive option `zeroHeader` has been replaced with the option + `defaultEnableZeroHeader` and can be overridden for every file. Its default + value changed from `false` to `true`. +- The archive option `statFiles` was removed since the library no longer checks + filesizes this way. +- The archive option `deflateLevel` has been replaced with the option + `defaultDeflateLevel` and can be overridden for every file. +- The first argument (`name`) of the `ZipStream\ZipStream` constuctor has been + replaced with the named argument `outputName`. +- Headers are now also sent if the `outputName` is empty. If you do not want to + automatically send http headers, set `sendHttpHeaders` to `false`. + +### File Options + +- The class `ZipStream\Option\File` has been replaced in favor of named + arguments in the `ZipStream\ZipStream->addFile*` functions. +- The file option `method` has been renamed to `compressionMethod`. +- The file option `time` has been renamed to `lastModificationDateTime`. +- The file option `size` has been renamed to `maxSize`. + +## Upgrade to version 2.0.0 + +https://github.com/maennchen/ZipStream-PHP/tree/2.0.0#upgrade-to-version-200 + +## Upgrade to version 1.0.0 + +https://github.com/maennchen/ZipStream-PHP/tree/2.0.0#upgrade-to-version-100 + +## Contributing + +ZipStream-PHP is a collaborative project. Please take a look at the +[.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) file. + +## Version Support + +Versions are supported according to the table below. + +Please do not open any pull requests contradicting the current version support +status. + +Careful: Always check the `README` on `main` for up-to-date information. + +| Version | New Features | Bugfixes | Security | +|---------|--------------|----------|----------| +| *3* | ✓ | ✓ | ✓ | +| *2* | ✗ | ✗ | ✓ | +| *1* | ✗ | ✗ | ✗ | +| *0* | ✗ | ✗ | ✗ | + +This library aligns itself with the PHP core support. New features and bugfixes +will only target PHP versions according to their current status. + +See: https://www.php.net/supported-versions.php + +## About the Authors + +- Paul Duncan <pabs@pablotron.org> - https://pablotron.org/ +- Jonatan Männchen <jonatan@maennchen.ch> - https://maennchen.dev +- Jesse G. Donat <donatj@gmail.com> - https://donatstudios.com +- Nicolas CARPi <nico-git@deltablot.email> - https://www.deltablot.com +- Nik Barham <nik@brokencube.co.uk> - https://www.brokencube.co.uk diff --git a/vendor/maennchen/zipstream-php/composer.json b/vendor/maennchen/zipstream-php/composer.json new file mode 100644 index 000000000..de5e62413 --- /dev/null +++ b/vendor/maennchen/zipstream-php/composer.json @@ -0,0 +1,88 @@ +{ + "name": "maennchen/zipstream-php", + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": ["zip", "stream"], + "type": "library", + "license": "MIT", + "authors": [{ + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "require": { + "php-64bit": "^8.1", + "ext-mbstring": "*", + "ext-zlib": "*" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "guzzlehttp/guzzle": "^7.5", + "ext-zip": "*", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "friendsofphp/php-cs-fixer": "^3.16", + "vimeo/psalm": "^5.0" + }, + "suggest": { + "psr/http-message": "^2.0", + "guzzlehttp/psr7": "^2.4" + }, + "scripts": { + "format": "php-cs-fixer fix", + "test": [ + "@test:unit", + "@test:formatted", + "@test:lint" + ], + "test:unit": "phpunit --coverage-clover=coverage.clover.xml --coverage-html cov", + "test:unit:slow": "@test:unit --group slow", + "test:unit:fast": "@test:unit --exclude-group slow", + "test:formatted": "@format --dry-run --stop-on-violation --using-cache=no", + "test:lint": "psalm --stats --show-info=true --find-unused-psalm-suppress", + "coverage:report": "php-coveralls --coverage_clover=coverage.clover.xml --json_path=coveralls-upload.json --insecure", + "install:tools": "phive install --trust-gpg-keys 0x67F861C3D889C656 --trust-gpg-keys 0x8AC0BAA79732DD42", + "docs:generate": "tools/phpdocumentor --sourcecode" + }, + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { "ZipStream\\Test\\": "test/" } + }, + "archive": { + "exclude": [ + "/composer.lock", + "/docs", + "/.gitattributes", + "/.github", + "/.gitignore", + "/guides", + "/.phive", + "/.php-cs-fixer.cache", + "/.php-cs-fixer.dist.php", + "/.phpdoc", + "/phpdoc.dist.xml", + "/.phpunit.result.cache", + "/phpunit.xml.dist", + "/psalm.xml", + "/test", + "/tools", + "/.tool-versions", + "/vendor" + ] + } +} diff --git a/vendor/maennchen/zipstream-php/guides/ContentLength.rst b/vendor/maennchen/zipstream-php/guides/ContentLength.rst new file mode 100644 index 000000000..21fea34d7 --- /dev/null +++ b/vendor/maennchen/zipstream-php/guides/ContentLength.rst @@ -0,0 +1,47 @@ +Adding Content-Length header +============= + +Adding a ``Content-Length`` header for ``ZipStream`` can be achieved by +using the options ``SIMULATION_STRICT`` or ``SIMULATION_LAX`` in the +``operationMode`` parameter. + +In the ``SIMULATION_STRICT`` mode, ``ZipStream`` will not allow to calculate the +size based on reading the whole file. ``SIMULATION_LAX`` will read the whole +file if neccessary. + +``SIMULATION_STRICT`` is therefore useful to make sure that the size can be +calculated efficiently. + +.. code-block:: php + use ZipStream\OperationMode; + use ZipStream\ZipStream; + + $zip = new ZipStream( + operationMode: OperationMode::SIMULATE_STRICT, // or SIMULATE_LAX + defaultEnableZeroHeader: false, + sendHttpHeaders: true, + outputStream: $stream, + ); + + // Normally add files + $zip->addFile('sample.txt', 'Sample String Data'); + + // Use addFileFromCallback and exactSize if you want to defer opening of + // the file resource + $zip->addFileFromCallback( + 'sample.txt', + exactSize: 18, + callback: function () { + return fopen('...'); + } + ); + + // Read resulting file size + $size = $zip->finish(); + + // Tell it to the browser + header('Content-Length: '. $size); + + // Execute the Simulation and stream the actual zip to the client + $zip->executeSimulation(); + diff --git a/vendor/maennchen/zipstream-php/guides/FlySystem.rst b/vendor/maennchen/zipstream-php/guides/FlySystem.rst new file mode 100644 index 000000000..4e6c6fb82 --- /dev/null +++ b/vendor/maennchen/zipstream-php/guides/FlySystem.rst @@ -0,0 +1,34 @@ +Usage with FlySystem +=============== + +For saving or uploading the generated zip, you can use the +`Flysystem <https://flysystem.thephpleague.com>`_ package, and its many +adapters. + +For that you will need to provide another stream than the ``php://output`` +default one, and pass it to Flysystem ``putStream`` method. + +.. code-block:: php + + // Open Stream only once for read and write since it's a memory stream and + // the content is lost when closing the stream / opening another one + $tempStream = fopen('php://memory', 'w+'); + + // Create Zip Archive + $zipStream = new ZipStream( + outputStream: $tempStream, + outputName: 'test.zip', + ); + $zipStream->addFile('test.txt', 'text'); + $zipStream->finish(); + + // Store File + // (see Flysystem documentation, and all its framework integration) + // Can be any adapter (AWS, Google, Ftp, etc.) + $adapter = new Local(__DIR__.'/path/to/folder'); + $filesystem = new Filesystem($adapter); + + $filesystem->writeStream('test.zip', $tempStream) + + // Close Stream + fclose($tempStream); diff --git a/vendor/maennchen/zipstream-php/guides/Nginx.rst b/vendor/maennchen/zipstream-php/guides/Nginx.rst new file mode 100644 index 000000000..c53d3000e --- /dev/null +++ b/vendor/maennchen/zipstream-php/guides/Nginx.rst @@ -0,0 +1,16 @@ +Usage with nginx +============= + +If you are using nginx as a webserver, it will try to buffer the response. +So you'll want to disable this with a custom header: + +.. code-block:: php + header('X-Accel-Buffering: no'); + # or with the Response class from Symfony + $response->headers->set('X-Accel-Buffering', 'no'); + +Alternatively, you can tweak the +`fastcgi cache parameters <https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_buffers>`_ +within nginx config. + +See `original issue <https://github.com/maennchen/ZipStream-PHP/issues/77>`_.
\ No newline at end of file diff --git a/vendor/maennchen/zipstream-php/guides/Options.rst b/vendor/maennchen/zipstream-php/guides/Options.rst new file mode 100644 index 000000000..5e92e94d6 --- /dev/null +++ b/vendor/maennchen/zipstream-php/guides/Options.rst @@ -0,0 +1,66 @@ +Available options +=============== + +Here is the full list of options available to you. You can also have a look at +``src/ZipStream.php`` file. + +.. code-block:: php + + use ZipStream\ZipStream; + + require_once 'vendor/autoload.php'; + + $zip = new ZipStream( + // Define output stream + // (argument is eiter a resource or implementing + // `Psr\Http\Message\StreamInterface`) + // + // Setup with `psr/http-message` & `guzzlehttp/psr7` dependencies + // required when using `Psr\Http\Message\StreamInterface`. + outputStream: $filePointer, + + // Set the deflate level (default is 6; use -1 to disable it) + defaultDeflateLevel: 6, + + // Add a comment to the zip file + comment: 'This is a comment.', + + // Send http headers (default is true) + sendHttpHeaders: false, + + // HTTP Content-Disposition. + // Defaults to 'attachment', where FILENAME is the specified filename. + // Note that this does nothing if you are not sending HTTP headers. + contentDisposition: 'attachment', + + // Output Name for HTTP Content-Disposition + // Defaults to no name + outputName: "example.zip", + + // HTTP Content-Type. + // Defaults to 'application/x-zip'. + // Note that this does nothing if you are not sending HTTP headers. + contentType: 'application/x-zip', + + // Set the function called for setting headers. + // Default is the `header()` of PHP + httpHeaderCallback: header(...), + + // Enable streaming files with single read where general purpose bit 3 + // indicates local file header contain zero values in crc and size + // fields, these appear only after file contents in data descriptor + // block. + // Set to true if your input stream is remote + // (used with addFileFromStream()). + // Default is false. + defaultEnableZeroHeader: false, + + // Enable zip64 extension, allowing very large archives + // (> 4Gb or file count > 64k) + // Default is true + enableZip64: true, + + // Flush output buffer after every write + // Default is false + flushOutput: true, + ); diff --git a/vendor/maennchen/zipstream-php/guides/PSR7Streams.rst b/vendor/maennchen/zipstream-php/guides/PSR7Streams.rst new file mode 100644 index 000000000..22af71d4a --- /dev/null +++ b/vendor/maennchen/zipstream-php/guides/PSR7Streams.rst @@ -0,0 +1,21 @@ +Usage with PSR 7 Streams +=============== + +PSR-7 streams are `standardized streams <https://www.php-fig.org/psr/psr-7/>`_. + +ZipStream-PHP supports working with these streams with the function +``addFileFromPsr7Stream``. + +For all parameters of the function see the API documentation. + +Example +--------------- + +.. code-block:: php + + $stream = $response->getBody(); + // add a file named 'streamfile.txt' from the content of the stream + $zip->addFileFromPsr7Stream( + fileName: 'streamfile.txt', + stream: $stream, + ); diff --git a/vendor/maennchen/zipstream-php/guides/StreamOutput.rst b/vendor/maennchen/zipstream-php/guides/StreamOutput.rst new file mode 100644 index 000000000..9f3165b75 --- /dev/null +++ b/vendor/maennchen/zipstream-php/guides/StreamOutput.rst @@ -0,0 +1,39 @@ +Stream Output +=============== + +Stream to S3 Bucket +--------------- + +.. code-block:: php + + use Aws\S3\S3Client; + use Aws\Credentials\CredentialProvider; + use ZipStream\ZipStream; + + $bucket = 'your bucket name'; + $client = new S3Client([ + 'region' => 'your region', + 'version' => 'latest', + 'bucketName' => $bucket, + 'credentials' => CredentialProvider::defaultProvider(), + ]); + $client->registerStreamWrapper(); + + $zipFile = fopen("s3://$bucket/example.zip", 'w'); + + $zip = new ZipStream( + enableZip64: false, + outputStream: $zipFile, + ); + + $zip->addFile( + fileName: 'file1.txt', + data: 'File1 data', + ); + $zip->addFile( + fileName: 'file2.txt', + data: 'File2 data', + ); + $zip->finish(); + + fclose($zipFile); diff --git a/vendor/maennchen/zipstream-php/guides/Symfony.rst b/vendor/maennchen/zipstream-php/guides/Symfony.rst new file mode 100644 index 000000000..902552c92 --- /dev/null +++ b/vendor/maennchen/zipstream-php/guides/Symfony.rst @@ -0,0 +1,130 @@ +Usage with Symfony +=============== + +Overview for using ZipStream in Symfony +-------- + +Using ZipStream in Symfony requires use of Symfony's ``StreamedResponse`` when +used in controller actions. + +Wrap your call to the relevant ``ZipStream`` stream method (i.e. ``addFile``, +``addFileFromPath``, ``addFileFromStream``) in Symfony's ``StreamedResponse`` +function passing in any required arguments for your use case. + +Using Symfony's ``StreamedResponse`` will allow Symfony to stream output from +ZipStream correctly to users' browsers and avoid a corrupted final zip landing +on the users' end. + +Example for using ``ZipStream`` in a controller action to zip stream files +stored in an AWS S3 bucket by key: + +.. code-block:: php + + use Symfony\Component\HttpFoundation\StreamedResponse; + use Aws\S3\S3Client; + use ZipStream; + + //... + + /** + * @Route("/zipstream", name="zipstream") + */ + public function zipStreamAction() + { + // sample test file on s3 + $s3keys = array( + "ziptestfolder/file1.txt" + ); + + $s3Client = $this->get('app.amazon.s3'); //s3client service + $s3Client->registerStreamWrapper(); //required + + // using StreamedResponse to wrap ZipStream functionality + // for files on AWS s3. + $response = new StreamedResponse(function() use($s3keys, $s3Client) + { + // Define suitable options for ZipStream Archive. + // this is needed to prevent issues with truncated zip files + //initialise zipstream with output zip filename and options. + $zip = new ZipStream\ZipStream( + outputName: 'test.zip', + defaultEnableZeroHeader: true, + contentType: 'application/octet-stream', + ); + + //loop keys - useful for multiple files + foreach ($s3keys as $key) { + // Get the file name in S3 key so we can save it to the zip + //file using the same name. + $fileName = basename($key); + + // concatenate s3path. + // replace with your bucket name or get from parameters file. + $bucket = 'bucketname'; + $s3path = "s3://" . $bucket . "/" . $key; + + //addFileFromStream + if ($streamRead = fopen($s3path, 'r')) { + $zip->addFileFromStream( + fileName: $fileName, + stream: $streamRead, + ); + } else { + die('Could not open stream for reading'); + } + } + + $zip->finish(); + + }); + + return $response; + } + +In the above example, files on AWS S3 are being streamed from S3 to the Symfon +application via ``fopen`` call when the s3Client has ``registerStreamWrapper`` +applied. This stream is then passed to ``ZipStream`` via the +``addFileFromStream`` function, which ZipStream then streams as a zip to the +client browser via Symfony's ``StreamedResponse``. No Zip is created server +side, which makes this approach a more efficient solution for streaming zips to +the client browser especially for larger files. + +For the above use case you will need to have installed +`aws/aws-sdk-php-symfony <https://github.com/aws/aws-sdk-php-symfony>`_ to +support accessing S3 objects in your Symfony web application. This is not +required for locally stored files on you server you intend to stream via +``ZipStream``. + +See official Symfony documentation for details on +`Symfony's StreamedResponse <https://symfony.com/doc/current/components/http_foundation.html#streaming-a-response>`_ +``Symfony\Component\HttpFoundation\StreamedResponse``. + +Note from `S3 documentation <https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/s3-stream-wrapper.html>`_: + + Streams opened in "r" mode only allow data to be read from the stream, and + are not seekable by default. This is so that data can be downloaded from + Amazon S3 in a truly streaming manner, where previously read bytes do not + need to be buffered into memory. If you need a stream to be seekable, you + can pass seekable into the stream context options of a function. + +Make sure to configure your S3 context correctly! + +Uploading a file +-------- + +You need to add correct permissions +(see `#120 <https://github.com/maennchen/ZipStream-PHP/issues/120>`_) + +**example code** + + +.. code-block:: php + + $path = "s3://{$adapter->getBucket()}/{$this->getArchivePath()}"; + + // the important bit + $outputContext = stream_context_create([ + 's3' => ['ACL' => 'public-read'], + ]); + + fopen($path, 'w', null, $outputContext); diff --git a/vendor/maennchen/zipstream-php/guides/Varnish.rst b/vendor/maennchen/zipstream-php/guides/Varnish.rst new file mode 100644 index 000000000..952d28749 --- /dev/null +++ b/vendor/maennchen/zipstream-php/guides/Varnish.rst @@ -0,0 +1,22 @@ +Usage with Varnish +============= + +Serving a big zip with varnish in between can cause random stream close. +This can be solved by adding attached code to the vcl file. + +To avoid the problem, add the following to your varnish config file: + +.. code-block:: + sub vcl_recv { + # Varnish can’t intercept the discussion anymore + # helps for streaming big zips + if (req.url ~ "\.(tar|gz|zip|7z|exe)$") { + return (pipe); + } + } + # Varnish can’t intercept the discussion anymore + # helps for streaming big zips + sub vcl_pipe { + set bereq.http.connection = "close"; + return (pipe); + } diff --git a/vendor/maennchen/zipstream-php/guides/index.rst b/vendor/maennchen/zipstream-php/guides/index.rst new file mode 100644 index 000000000..48f465aea --- /dev/null +++ b/vendor/maennchen/zipstream-php/guides/index.rst @@ -0,0 +1,126 @@ +ZipStream PHP +============= + +A fast and simple streaming zip file downloader for PHP. Using this library will +save you from having to write the Zip to disk. You can directly send it to the +user, which is much faster. It can work with S3 buckets or any PSR7 Stream. + +.. toctree:: + + index + Symfony + Options + StreamOutput + FlySystem + PSR7Streams + Nginx + Varnish + ContentLength + +Installation +--------------- + +Simply add a dependency on ``maennchen/zipstream-php`` to your project's +``composer.json`` file if you use Composer to manage the dependencies of your +project. Use following command to add the package to your project's +dependencies: + +.. code-block:: sh + composer require maennchen/zipstream-php + +If you want to use``addFileFromPsr7Stream``` +(``Psr\Http\Message\StreamInterface``) or use a stream instead of a +``resource`` as ``outputStream``, the following dependencies must be installed +as well: + +.. code-block:: sh + composer require psr/http-message guzzlehttp/psr7 + +If ``composer install`` yields the following error, your installation is missing +the `mbstring extension <https://www.php.net/manual/en/book.mbstring.php>`_, +either `install it <https://www.php.net/manual/en/mbstring.installation.php>`_ +or run the follwoing command: + +.. code-block:: + Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires PHP extension ext-mbstring * but it is + missing from your system. Install or enable PHP's mbstrings extension. + +.. code-block:: sh + composer require symfony/polyfill-mbstring + +Usage Intro +--------------- + +Here's a simple example: + +.. code-block:: php + + // Autoload the dependencies + require 'vendor/autoload.php'; + + // create a new zipstream object + $zip = new ZipStream\ZipStream( + outputName: 'example.zip', + + // enable output of HTTP headers + sendHttpHeaders: true, + ); + + // create a file named 'hello.txt' + $zip->addFile( + fileName: 'hello.txt', + data: 'This is the contents of hello.txt', + ); + + // add a file named 'some_image.jpg' from a local file 'path/to/image.jpg' + $zip->addFileFromPath( + fileName: 'some_image.jpg', + path: 'path/to/image.jpg', + ); + + // add a file named 'goodbye.txt' from an open stream resource + $filePointer = tmpfile(); + fwrite($filePointer, 'The quick brown fox jumped over the lazy dog.'); + rewind($filePointer); + $zip->addFileFromStream( + fileName: 'goodbye.txt', + stream: $filePointer, + ); + fclose($filePointer); + + // add a file named 'streamfile.txt' from the body of a `guzzle` response + // Setup with `psr/http-message` & `guzzlehttp/psr7` dependencies required. + $zip->addFileFromPsr7Stream( + fileName: 'streamfile.txt', + stream: $response->getBody(), + ); + + // finish the zip stream + $zip->finish(); + +You can also add comments, modify file timestamps, and customize (or +disable) the HTTP headers. It is also possible to specify the storage method +when adding files, the current default storage method is ``DEFLATE`` +i.e files are stored with Compression mode 0x08. + +Known Issues +--------------- + +The native Mac OS archive extraction tool prior to macOS 10.15 might not open +archives in some conditions. A workaround is to disable the Zip64 feature with +the option ``enableZip64: false``. This limits the archive to 4 Gb and 64k files +but will allow users on macOS 10.14 and below to open them without issue. +See `#116 <https://github.com/maennchen/ZipStream-PHP/issues/116>`_. + +The linux ``unzip`` utility might not handle properly unicode characters. +It is recommended to extract with another tool like +`7-zip <https://www.7-zip.org/>`_. +See `#146 <https://github.com/maennchen/ZipStream-PHP/issues/146>`_. + +It is the responsability of the client code to make sure that files are not +saved with the same path, as it is not possible for the library to figure it out +while streaming a zip. +See `#154 <https://github.com/maennchen/ZipStream-PHP/issues/154>`_. diff --git a/vendor/maennchen/zipstream-php/phpdoc.dist.xml b/vendor/maennchen/zipstream-php/phpdoc.dist.xml new file mode 100644 index 000000000..b98fe1cd2 --- /dev/null +++ b/vendor/maennchen/zipstream-php/phpdoc.dist.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<phpdocumentor + configVersion="3" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="https://www.phpdoc.org" + xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/phpDocumentor/phpDocumentor/master/data/xsd/phpdoc.xsd" +> + <title>💾 ZipStream-PHP</title> + <paths> + <output>docs</output> + </paths> + <version number="3.0.0"> + <folder>latest</folder> + <api> + <source dsn="."> + <path>src</path> + </source> + <output>api</output> + <ignore hidden="true" symlinks="true"> + <path>tests/**/*</path> + <path>vendor/**/*</path> + </ignore> + <extensions> + <extension>php</extension> + </extensions> + <visibility>public</visibility> + <default-package-name>ZipStream</default-package-name> + <include-source>true</include-source> + </api> + <guide> + <source dsn="."> + <path>guides</path> + </source> + <output>guide</output> + </guide> + </version> + <setting name="guides.enabled" value="true"/> + <template name="default" /> +</phpdocumentor>
\ No newline at end of file diff --git a/vendor/maennchen/zipstream-php/phpunit.xml.dist b/vendor/maennchen/zipstream-php/phpunit.xml.dist new file mode 100644 index 000000000..1b02a3af8 --- /dev/null +++ b/vendor/maennchen/zipstream-php/phpunit.xml.dist @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="test/bootstrap.php" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd" cacheDirectory=".phpunit.cache"> + <coverage/> + <testsuites> + <testsuite name="Application"> + <directory>test</directory> + </testsuite> + </testsuites> + <logging/> + <source> + <include> + <directory suffix=".php">src</directory> + </include> + </source> +</phpunit> diff --git a/vendor/maennchen/zipstream-php/psalm.xml b/vendor/maennchen/zipstream-php/psalm.xml new file mode 100644 index 000000000..4da861836 --- /dev/null +++ b/vendor/maennchen/zipstream-php/psalm.xml @@ -0,0 +1,25 @@ +<?xml version="1.0"?> +<psalm + errorLevel="1" + resolveFromConfigFile="true" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="https://getpsalm.org/schema/config" + xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" + findUnusedBaselineEntry="true" + findUnusedCode="true" + phpVersion="8.1.0" +> + <!-- TODO: Update phpVersion when raising the minimum supported version --> + <projectFiles> + <directory name="src" /> + <ignoreFiles> + <directory name="vendor" /> + </ignoreFiles> + </projectFiles> + <issueHandlers> + <!-- Turn off dead code warnings for externally called functions --> + <PossiblyUnusedProperty errorLevel="suppress" /> + <PossiblyUnusedMethod errorLevel="suppress" /> + <PossiblyUnusedReturnValue errorLevel="suppress" /> + </issueHandlers> +</psalm> diff --git a/vendor/maennchen/zipstream-php/results.sarif b/vendor/maennchen/zipstream-php/results.sarif new file mode 100644 index 000000000..c99a3f47f --- /dev/null +++ b/vendor/maennchen/zipstream-php/results.sarif @@ -0,0 +1 @@ +{"version":"2.1.0","$schema":"https:\/\/json.schemastore.org\/sarif-2.1.0.json","runs":[{"tool":{"driver":{"name":"Psalm","informationUri":"https:\/\/psalm.dev","version":"5.26.1@d747f6500b38ac4f7dfc5edbcae6e4b637d7add0"}},"results":[]}]} diff --git a/vendor/maennchen/zipstream-php/src/CentralDirectoryFileHeader.php b/vendor/maennchen/zipstream-php/src/CentralDirectoryFileHeader.php new file mode 100644 index 000000000..ffcfc6e97 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/CentralDirectoryFileHeader.php @@ -0,0 +1,52 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream; + +use DateTimeInterface; + +/** + * @internal + */ +abstract class CentralDirectoryFileHeader +{ + private const SIGNATURE = 0x02014b50; + + public static function generate( + int $versionMadeBy, + int $versionNeededToExtract, + int $generalPurposeBitFlag, + CompressionMethod $compressionMethod, + DateTimeInterface $lastModificationDateTime, + int $crc32, + int $compressedSize, + int $uncompressedSize, + string $fileName, + string $extraField, + string $fileComment, + int $diskNumberStart, + int $internalFileAttributes, + int $externalFileAttributes, + int $relativeOffsetOfLocalHeader, + ): string { + return PackField::pack( + new PackField(format: 'V', value: self::SIGNATURE), + new PackField(format: 'v', value: $versionMadeBy), + new PackField(format: 'v', value: $versionNeededToExtract), + new PackField(format: 'v', value: $generalPurposeBitFlag), + new PackField(format: 'v', value: $compressionMethod->value), + new PackField(format: 'V', value: Time::dateTimeToDosTime($lastModificationDateTime)), + new PackField(format: 'V', value: $crc32), + new PackField(format: 'V', value: $compressedSize), + new PackField(format: 'V', value: $uncompressedSize), + new PackField(format: 'v', value: strlen($fileName)), + new PackField(format: 'v', value: strlen($extraField)), + new PackField(format: 'v', value: strlen($fileComment)), + new PackField(format: 'v', value: $diskNumberStart), + new PackField(format: 'v', value: $internalFileAttributes), + new PackField(format: 'V', value: $externalFileAttributes), + new PackField(format: 'V', value: $relativeOffsetOfLocalHeader), + ) . $fileName . $extraField . $fileComment; + } +} diff --git a/vendor/maennchen/zipstream-php/src/CompressionMethod.php b/vendor/maennchen/zipstream-php/src/CompressionMethod.php new file mode 100644 index 000000000..51e436370 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/CompressionMethod.php @@ -0,0 +1,106 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream; + +enum CompressionMethod: int +{ + /** + * The file is stored (no compression) + */ + case STORE = 0x00; + + // 0x01: legacy algorithm - The file is Shrunk + // 0x02: legacy algorithm - The file is Reduced with compression factor 1 + // 0x03: legacy algorithm - The file is Reduced with compression factor 2 + // 0x04: legacy algorithm - The file is Reduced with compression factor 3 + // 0x05: legacy algorithm - The file is Reduced with compression factor 4 + // 0x06: legacy algorithm - The file is Imploded + // 0x07: Reserved for Tokenizing compression algorithm + + /** + * The file is Deflated + */ + case DEFLATE = 0x08; + + // /** + // * Enhanced Deflating using Deflate64(tm) + // */ + // case DEFLATE_64 = 0x09; + + // /** + // * PKWARE Data Compression Library Imploding (old IBM TERSE) + // */ + // case PKWARE = 0x0a; + + // // 0x0b: Reserved by PKWARE + + // /** + // * File is compressed using BZIP2 algorithm + // */ + // case BZIP2 = 0x0c; + + // // 0x0d: Reserved by PKWARE + + // /** + // * LZMA + // */ + // case LZMA = 0x0e; + + // // 0x0f: Reserved by PKWARE + + // /** + // * IBM z/OS CMPSC Compression + // */ + // case IBM_ZOS_CMPSC = 0x10; + + // // 0x11: Reserved by PKWARE + + // /** + // * File is compressed using IBM TERSE + // */ + // case IBM_TERSE = 0x12; + + // /** + // * IBM LZ77 z Architecture + // */ + // case IBM_LZ77 = 0x13; + + // // 0x14: deprecated (use method 93 for zstd) + + // /** + // * Zstandard (zstd) Compression + // */ + // case ZSTD = 0x5d; + + // /** + // * MP3 Compression + // */ + // case MP3 = 0x5e; + + // /** + // * XZ Compression + // */ + // case XZ = 0x5f; + + // /** + // * JPEG variant + // */ + // case JPEG = 0x60; + + // /** + // * WavPack compressed data + // */ + // case WAV_PACK = 0x61; + + // /** + // * PPMd version I, Rev 1 + // */ + // case PPMD_1_1 = 0x62; + + // /** + // * AE-x encryption marker + // */ + // case AE_X_ENCRYPTION = 0x63; +} diff --git a/vendor/maennchen/zipstream-php/src/DataDescriptor.php b/vendor/maennchen/zipstream-php/src/DataDescriptor.php new file mode 100644 index 000000000..04146190d --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/DataDescriptor.php @@ -0,0 +1,26 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream; + +/** + * @internal + */ +abstract class DataDescriptor +{ + private const SIGNATURE = 0x08074b50; + + public static function generate( + int $crc32UncompressedData, + int $compressedSize, + int $uncompressedSize, + ): string { + return PackField::pack( + new PackField(format: 'V', value: self::SIGNATURE), + new PackField(format: 'V', value: $crc32UncompressedData), + new PackField(format: 'V', value: $compressedSize), + new PackField(format: 'V', value: $uncompressedSize), + ); + } +} diff --git a/vendor/maennchen/zipstream-php/src/EndOfCentralDirectory.php b/vendor/maennchen/zipstream-php/src/EndOfCentralDirectory.php new file mode 100644 index 000000000..4320addc3 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/EndOfCentralDirectory.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream; + +/** + * @internal + */ +abstract class EndOfCentralDirectory +{ + private const SIGNATURE = 0x06054b50; + + public static function generate( + int $numberOfThisDisk, + int $numberOfTheDiskWithCentralDirectoryStart, + int $numberOfCentralDirectoryEntriesOnThisDisk, + int $numberOfCentralDirectoryEntries, + int $sizeOfCentralDirectory, + int $centralDirectoryStartOffsetOnDisk, + string $zipFileComment, + ): string { + /** @psalm-suppress MixedArgument */ + return PackField::pack( + new PackField(format: 'V', value: static::SIGNATURE), + new PackField(format: 'v', value: $numberOfThisDisk), + new PackField(format: 'v', value: $numberOfTheDiskWithCentralDirectoryStart), + new PackField(format: 'v', value: $numberOfCentralDirectoryEntriesOnThisDisk), + new PackField(format: 'v', value: $numberOfCentralDirectoryEntries), + new PackField(format: 'V', value: $sizeOfCentralDirectory), + new PackField(format: 'V', value: $centralDirectoryStartOffsetOnDisk), + new PackField(format: 'v', value: strlen($zipFileComment)), + ) . $zipFileComment; + } +} diff --git a/vendor/maennchen/zipstream-php/src/Exception.php b/vendor/maennchen/zipstream-php/src/Exception.php new file mode 100644 index 000000000..2e81e307b --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Exception.php @@ -0,0 +1,7 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream; + +abstract class Exception extends \Exception {} diff --git a/vendor/maennchen/zipstream-php/src/Exception/DosTimeOverflowException.php b/vendor/maennchen/zipstream-php/src/Exception/DosTimeOverflowException.php new file mode 100644 index 000000000..b8d050808 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Exception/DosTimeOverflowException.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Exception; + +use DateTimeInterface; +use ZipStream\Exception; + +/** + * This Exception gets invoked if a file wasn't found + */ +class DosTimeOverflowException extends Exception +{ + /** + * @internal + */ + public function __construct( + public readonly DateTimeInterface $dateTime + ) { + parent::__construct('The date ' . $dateTime->format(DateTimeInterface::ATOM) . " can't be represented as DOS time / date."); + } +} diff --git a/vendor/maennchen/zipstream-php/src/Exception/FileNotFoundException.php b/vendor/maennchen/zipstream-php/src/Exception/FileNotFoundException.php new file mode 100644 index 000000000..350a7bfe5 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Exception/FileNotFoundException.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Exception; + +use ZipStream\Exception; + +/** + * This Exception gets invoked if a file wasn't found + */ +class FileNotFoundException extends Exception +{ + /** + * @internal + */ + public function __construct( + public readonly string $path + ) { + parent::__construct("The file with the path $path wasn't found."); + } +} diff --git a/vendor/maennchen/zipstream-php/src/Exception/FileNotReadableException.php b/vendor/maennchen/zipstream-php/src/Exception/FileNotReadableException.php new file mode 100644 index 000000000..93d0c6c64 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Exception/FileNotReadableException.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Exception; + +use ZipStream\Exception; + +/** + * This Exception gets invoked if a file wasn't found + */ +class FileNotReadableException extends Exception +{ + /** + * @internal + */ + public function __construct( + public readonly string $path + ) { + parent::__construct("The file with the path $path isn't readable."); + } +} diff --git a/vendor/maennchen/zipstream-php/src/Exception/FileSizeIncorrectException.php b/vendor/maennchen/zipstream-php/src/Exception/FileSizeIncorrectException.php new file mode 100644 index 000000000..11f0b67b3 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Exception/FileSizeIncorrectException.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Exception; + +use ZipStream\Exception; + +/** + * This Exception gets invoked if a file is not as large as it was specified. + */ +class FileSizeIncorrectException extends Exception +{ + /** + * @internal + */ + public function __construct( + public readonly int $expectedSize, + public readonly int $actualSize + ) { + parent::__construct("File is {$actualSize} instead of {$expectedSize} bytes large. Adjust `exactSize` parameter."); + } +} diff --git a/vendor/maennchen/zipstream-php/src/Exception/OverflowException.php b/vendor/maennchen/zipstream-php/src/Exception/OverflowException.php new file mode 100644 index 000000000..09bdafb28 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Exception/OverflowException.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Exception; + +use ZipStream\Exception; + +/** + * This Exception gets invoked if a counter value exceeds storage size + */ +class OverflowException extends Exception +{ + /** + * @internal + */ + public function __construct() + { + parent::__construct('File size exceeds limit of 32 bit integer. Please enable "zip64" option.'); + } +} diff --git a/vendor/maennchen/zipstream-php/src/Exception/ResourceActionException.php b/vendor/maennchen/zipstream-php/src/Exception/ResourceActionException.php new file mode 100644 index 000000000..cbd9b0bb9 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Exception/ResourceActionException.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Exception; + +use ZipStream\Exception; + +/** + * This Exception gets invoked if a resource like `fread` returns false + */ +class ResourceActionException extends Exception +{ + /** + * @var ?resource + */ + public $resource; + + /** + * @param resource $resource + */ + public function __construct( + public readonly string $function, + $resource = null, + ) { + $this->resource = $resource; + parent::__construct('Function ' . $function . 'failed on resource.'); + } +} diff --git a/vendor/maennchen/zipstream-php/src/Exception/SimulationFileUnknownException.php b/vendor/maennchen/zipstream-php/src/Exception/SimulationFileUnknownException.php new file mode 100644 index 000000000..717c1aafe --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Exception/SimulationFileUnknownException.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Exception; + +use ZipStream\Exception; + +/** + * This Exception gets invoked if a strict simulation is executed and the file + * information can't be determined without reading the entire file. + */ +class SimulationFileUnknownException extends Exception +{ + public function __construct() + { + parent::__construct('The details of the strict simulation file could not be determined without reading the entire file.'); + } +} diff --git a/vendor/maennchen/zipstream-php/src/Exception/StreamNotReadableException.php b/vendor/maennchen/zipstream-php/src/Exception/StreamNotReadableException.php new file mode 100644 index 000000000..c1446735a --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Exception/StreamNotReadableException.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Exception; + +use ZipStream\Exception; + +/** + * This Exception gets invoked if a stream can't be read. + */ +class StreamNotReadableException extends Exception +{ + /** + * @internal + */ + public function __construct() + { + parent::__construct('The stream could not be read.'); + } +} diff --git a/vendor/maennchen/zipstream-php/src/Exception/StreamNotSeekableException.php b/vendor/maennchen/zipstream-php/src/Exception/StreamNotSeekableException.php new file mode 100644 index 000000000..606f11f14 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Exception/StreamNotSeekableException.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Exception; + +use ZipStream\Exception; + +/** + * This Exception gets invoked if a non seekable stream is + * provided and zero headers are disabled. + */ +class StreamNotSeekableException extends Exception +{ + /** + * @internal + */ + public function __construct() + { + parent::__construct('enableZeroHeader must be enable to add non seekable streams'); + } +} diff --git a/vendor/maennchen/zipstream-php/src/File.php b/vendor/maennchen/zipstream-php/src/File.php new file mode 100644 index 000000000..0462196e2 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/File.php @@ -0,0 +1,420 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream; + +use Closure; +use DateTimeInterface; +use DeflateContext; +use RuntimeException; +use ZipStream\Exception\FileSizeIncorrectException; +use ZipStream\Exception\OverflowException; +use ZipStream\Exception\ResourceActionException; +use ZipStream\Exception\SimulationFileUnknownException; +use ZipStream\Exception\StreamNotReadableException; +use ZipStream\Exception\StreamNotSeekableException; + +/** + * @internal + */ +class File +{ + private const CHUNKED_READ_BLOCK_SIZE = 0x1000000; + + private Version $version; + + private int $compressedSize = 0; + + private int $uncompressedSize = 0; + + private int $crc = 0; + + private int $generalPurposeBitFlag = 0; + + private readonly string $fileName; + + /** + * @var resource|null + */ + private $stream; + + /** + * @param Closure $dataCallback + * @psalm-param Closure(): resource $dataCallback + */ + public function __construct( + string $fileName, + private readonly Closure $dataCallback, + private readonly OperationMode $operationMode, + private readonly int $startOffset, + private readonly CompressionMethod $compressionMethod, + private readonly string $comment, + private readonly DateTimeInterface $lastModificationDateTime, + private readonly int $deflateLevel, + private readonly ?int $maxSize, + private readonly ?int $exactSize, + private readonly bool $enableZip64, + private readonly bool $enableZeroHeader, + private readonly Closure $send, + private readonly Closure $recordSentBytes, + ) { + $this->fileName = self::filterFilename($fileName); + $this->checkEncoding(); + + if ($this->enableZeroHeader) { + $this->generalPurposeBitFlag |= GeneralPurposeBitFlag::ZERO_HEADER; + } + + $this->version = $this->compressionMethod === CompressionMethod::DEFLATE ? Version::DEFLATE : Version::STORE; + } + + public function cloneSimulationExecution(): self + { + return new self( + $this->fileName, + $this->dataCallback, + OperationMode::NORMAL, + $this->startOffset, + $this->compressionMethod, + $this->comment, + $this->lastModificationDateTime, + $this->deflateLevel, + $this->maxSize, + $this->exactSize, + $this->enableZip64, + $this->enableZeroHeader, + $this->send, + $this->recordSentBytes, + ); + } + + public function process(): string + { + $forecastSize = $this->forecastSize(); + + if ($this->enableZeroHeader) { + // No calculation required + } elseif ($this->isSimulation() && $forecastSize !== null) { + $this->uncompressedSize = $forecastSize; + $this->compressedSize = $forecastSize; + } else { + $this->readStream(send: false); + if (rewind($this->unpackStream()) === false) { + throw new ResourceActionException('rewind', $this->unpackStream()); + } + } + + $this->addFileHeader(); + + $detectedSize = $forecastSize ?? ($this->compressedSize > 0 ? $this->compressedSize : null); + + if ( + $this->isSimulation() && + $detectedSize !== null + ) { + ($this->recordSentBytes)($detectedSize); + } else { + $this->readStream(send: true); + } + + $this->addFileFooter(); + return $this->getCdrFile(); + } + + /** + * @return resource + */ + private function unpackStream() + { + if ($this->stream) { + return $this->stream; + } + + if ($this->operationMode === OperationMode::SIMULATE_STRICT) { + throw new SimulationFileUnknownException(); + } + + $this->stream = ($this->dataCallback)(); + + if (!$this->enableZeroHeader && !stream_get_meta_data($this->stream)['seekable']) { + throw new StreamNotSeekableException(); + } + if (!( + str_contains(stream_get_meta_data($this->stream)['mode'], 'r') + || str_contains(stream_get_meta_data($this->stream)['mode'], 'w+') + || str_contains(stream_get_meta_data($this->stream)['mode'], 'a+') + || str_contains(stream_get_meta_data($this->stream)['mode'], 'x+') + || str_contains(stream_get_meta_data($this->stream)['mode'], 'c+') + )) { + throw new StreamNotReadableException(); + } + + return $this->stream; + } + + private function forecastSize(): ?int + { + if ($this->compressionMethod !== CompressionMethod::STORE) { + return null; + } + if ($this->exactSize !== null) { + return $this->exactSize; + } + $fstat = fstat($this->unpackStream()); + if (!$fstat || !array_key_exists('size', $fstat) || $fstat['size'] < 1) { + return null; + } + + if ($this->maxSize !== null && $this->maxSize < $fstat['size']) { + return $this->maxSize; + } + + return $fstat['size']; + } + + /** + * Create and send zip header for this file. + */ + private function addFileHeader(): void + { + $forceEnableZip64 = $this->enableZeroHeader && $this->enableZip64; + + $footer = $this->buildZip64ExtraBlock($forceEnableZip64); + + $zip64Enabled = $footer !== ''; + + if ($zip64Enabled) { + $this->version = Version::ZIP64; + } + + if ($this->generalPurposeBitFlag & GeneralPurposeBitFlag::EFS) { + // Put the tricky entry to + // force Linux unzip to lookup EFS flag. + $footer .= Zs\ExtendedInformationExtraField::generate(); + } + + $data = LocalFileHeader::generate( + versionNeededToExtract: $this->version->value, + generalPurposeBitFlag: $this->generalPurposeBitFlag, + compressionMethod: $this->compressionMethod, + lastModificationDateTime: $this->lastModificationDateTime, + crc32UncompressedData: $this->crc, + compressedSize: $zip64Enabled + ? 0xFFFFFFFF + : $this->compressedSize, + uncompressedSize: $zip64Enabled + ? 0xFFFFFFFF + : $this->uncompressedSize, + fileName: $this->fileName, + extraField: $footer, + ); + + + ($this->send)($data); + } + + /** + * Strip characters that are not legal in Windows filenames + * to prevent compatibility issues + */ + private static function filterFilename( + /** + * Unprocessed filename + */ + string $fileName + ): string { + // strip leading slashes from file name + // (fixes bug in windows archive viewer) + $fileName = ltrim($fileName, '/'); + + return str_replace(['\\', ':', '*', '?', '"', '<', '>', '|'], '_', $fileName); + } + + private function checkEncoding(): void + { + // Sets Bit 11: Language encoding flag (EFS). If this bit is set, + // the filename and comment fields for this file + // MUST be encoded using UTF-8. (see APPENDIX D) + if (mb_check_encoding($this->fileName, 'UTF-8') && + mb_check_encoding($this->comment, 'UTF-8')) { + $this->generalPurposeBitFlag |= GeneralPurposeBitFlag::EFS; + } + } + + private function buildZip64ExtraBlock(bool $force = false): string + { + $outputZip64ExtraBlock = false; + + $originalSize = null; + if ($force || $this->uncompressedSize > 0xFFFFFFFF) { + $outputZip64ExtraBlock = true; + $originalSize = $this->uncompressedSize; + } + + $compressedSize = null; + if ($force || $this->compressedSize > 0xFFFFFFFF) { + $outputZip64ExtraBlock = true; + $compressedSize = $this->compressedSize; + } + + // If this file will start over 4GB limit in ZIP file, + // CDR record will have to use Zip64 extension to describe offset + // to keep consistency we use the same value here + $relativeHeaderOffset = null; + if ($this->startOffset > 0xFFFFFFFF) { + $outputZip64ExtraBlock = true; + $relativeHeaderOffset = $this->startOffset; + } + + if (!$outputZip64ExtraBlock) { + return ''; + } + + if (!$this->enableZip64) { + throw new OverflowException(); + } + + return Zip64\ExtendedInformationExtraField::generate( + originalSize: $originalSize, + compressedSize: $compressedSize, + relativeHeaderOffset: $relativeHeaderOffset, + diskStartNumber: null, + ); + } + + private function addFileFooter(): void + { + if (($this->compressedSize > 0xFFFFFFFF || $this->uncompressedSize > 0xFFFFFFFF) && $this->version !== Version::ZIP64) { + throw new OverflowException(); + } + + if (!$this->enableZeroHeader) { + return; + } + + if ($this->version === Version::ZIP64) { + $footer = Zip64\DataDescriptor::generate( + crc32UncompressedData: $this->crc, + compressedSize: $this->compressedSize, + uncompressedSize: $this->uncompressedSize, + ); + } else { + $footer = DataDescriptor::generate( + crc32UncompressedData: $this->crc, + compressedSize: $this->compressedSize, + uncompressedSize: $this->uncompressedSize, + ); + } + + ($this->send)($footer); + } + + private function readStream(bool $send): void + { + $this->compressedSize = 0; + $this->uncompressedSize = 0; + $hash = hash_init('crc32b'); + + $deflate = $this->compressionInit(); + + while ( + !feof($this->unpackStream()) && + ($this->maxSize === null || $this->uncompressedSize < $this->maxSize) && + ($this->exactSize === null || $this->uncompressedSize < $this->exactSize) + ) { + $readLength = min( + ($this->maxSize ?? PHP_INT_MAX) - $this->uncompressedSize, + ($this->exactSize ?? PHP_INT_MAX) - $this->uncompressedSize, + self::CHUNKED_READ_BLOCK_SIZE + ); + + $data = fread($this->unpackStream(), $readLength); + + hash_update($hash, $data); + + $this->uncompressedSize += strlen($data); + + if ($deflate) { + $data = deflate_add( + $deflate, + $data, + feof($this->unpackStream()) ? ZLIB_FINISH : ZLIB_NO_FLUSH + ); + } + + $this->compressedSize += strlen($data); + + if ($send) { + ($this->send)($data); + } + } + + if ($this->exactSize !== null && $this->uncompressedSize !== $this->exactSize) { + throw new FileSizeIncorrectException(expectedSize: $this->exactSize, actualSize: $this->uncompressedSize); + } + + $this->crc = hexdec(hash_final($hash)); + } + + private function compressionInit(): ?DeflateContext + { + switch ($this->compressionMethod) { + case CompressionMethod::STORE: + // Noting to do + return null; + case CompressionMethod::DEFLATE: + $deflateContext = deflate_init( + ZLIB_ENCODING_RAW, + ['level' => $this->deflateLevel] + ); + + if (!$deflateContext) { + // @codeCoverageIgnoreStart + throw new RuntimeException("Can't initialize deflate context."); + // @codeCoverageIgnoreEnd + } + + // False positive, resource is no longer returned from this function + return $deflateContext; + default: + // @codeCoverageIgnoreStart + throw new RuntimeException('Unsupported Compression Method ' . print_r($this->compressionMethod, true)); + // @codeCoverageIgnoreEnd + } + } + + private function getCdrFile(): string + { + $footer = $this->buildZip64ExtraBlock(); + + return CentralDirectoryFileHeader::generate( + versionMadeBy: ZipStream::ZIP_VERSION_MADE_BY, + versionNeededToExtract: $this->version->value, + generalPurposeBitFlag: $this->generalPurposeBitFlag, + compressionMethod: $this->compressionMethod, + lastModificationDateTime: $this->lastModificationDateTime, + crc32: $this->crc, + compressedSize: $this->compressedSize > 0xFFFFFFFF + ? 0xFFFFFFFF + : $this->compressedSize, + uncompressedSize: $this->uncompressedSize > 0xFFFFFFFF + ? 0xFFFFFFFF + : $this->uncompressedSize, + fileName: $this->fileName, + extraField: $footer, + fileComment: $this->comment, + diskNumberStart: 0, + internalFileAttributes: 0, + externalFileAttributes: 32, + relativeOffsetOfLocalHeader: $this->startOffset > 0xFFFFFFFF + ? 0xFFFFFFFF + : $this->startOffset, + ); + } + + private function isSimulation(): bool + { + return $this->operationMode === OperationMode::SIMULATE_LAX || $this->operationMode === OperationMode::SIMULATE_STRICT; + } +} diff --git a/vendor/maennchen/zipstream-php/src/GeneralPurposeBitFlag.php b/vendor/maennchen/zipstream-php/src/GeneralPurposeBitFlag.php new file mode 100644 index 000000000..23a66d889 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/GeneralPurposeBitFlag.php @@ -0,0 +1,89 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream; + +/** + * @internal + */ +abstract class GeneralPurposeBitFlag +{ + /** + * If set, indicates that the file is encrypted. + */ + public const ENCRYPTED = 1 << 0; + + /** + * (For Methods 8 and 9 - Deflating) + * Normal (-en) compression option was used. + */ + public const DEFLATE_COMPRESSION_NORMAL = 0 << 1; + + /** + * (For Methods 8 and 9 - Deflating) + * Maximum (-exx/-ex) compression option was used. + */ + public const DEFLATE_COMPRESSION_MAXIMUM = 1 << 1; + + /** + * (For Methods 8 and 9 - Deflating) + * Fast (-ef) compression option was used. + */ + public const DEFLATE_COMPRESSION_FAST = 10 << 1; + + /** + * (For Methods 8 and 9 - Deflating) + * Super Fast (-es) compression option was used. + */ + public const DEFLATE_COMPRESSION_SUPERFAST = 11 << 1; + + /** + * If the compression method used was type 14, + * LZMA, then this bit, if set, indicates + * an end-of-stream (EOS) marker is used to + * mark the end of the compressed data stream. + * If clear, then an EOS marker is not present + * and the compressed data size must be known + * to extract. + */ + public const LZMA_EOS = 1 << 1; + + /** + * If this bit is set, the fields crc-32, compressed + * size and uncompressed size are set to zero in the + * local header. The correct values are put in the + * data descriptor immediately following the compressed + * data. + */ + public const ZERO_HEADER = 1 << 3; + + /** + * If this bit is set, this indicates that the file is + * compressed patched data. + */ + public const COMPRESSED_PATCHED_DATA = 1 << 5; + + /** + * Strong encryption. If this bit is set, you MUST + * set the version needed to extract value to at least + * 50 and you MUST also set bit 0. If AES encryption + * is used, the version needed to extract value MUST + * be at least 51. + */ + public const STRONG_ENCRYPTION = 1 << 6; + + /** + * Language encoding flag (EFS). If this bit is set, + * the filename and comment fields for this file + * MUST be encoded using UTF-8. + */ + public const EFS = 1 << 11; + + /** + * Set when encrypting the Central Directory to indicate + * selected data values in the Local Header are masked to + * hide their actual values. + */ + public const ENCRYPT_CENTRAL_DIRECTORY = 1 << 13; +} diff --git a/vendor/maennchen/zipstream-php/src/LocalFileHeader.php b/vendor/maennchen/zipstream-php/src/LocalFileHeader.php new file mode 100644 index 000000000..e08b65610 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/LocalFileHeader.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream; + +use DateTimeInterface; + +/** + * @internal + */ +abstract class LocalFileHeader +{ + private const SIGNATURE = 0x04034b50; + + public static function generate( + int $versionNeededToExtract, + int $generalPurposeBitFlag, + CompressionMethod $compressionMethod, + DateTimeInterface $lastModificationDateTime, + int $crc32UncompressedData, + int $compressedSize, + int $uncompressedSize, + string $fileName, + string $extraField, + ): string { + return PackField::pack( + new PackField(format: 'V', value: self::SIGNATURE), + new PackField(format: 'v', value: $versionNeededToExtract), + new PackField(format: 'v', value: $generalPurposeBitFlag), + new PackField(format: 'v', value: $compressionMethod->value), + new PackField(format: 'V', value: Time::dateTimeToDosTime($lastModificationDateTime)), + new PackField(format: 'V', value: $crc32UncompressedData), + new PackField(format: 'V', value: $compressedSize), + new PackField(format: 'V', value: $uncompressedSize), + new PackField(format: 'v', value: strlen($fileName)), + new PackField(format: 'v', value: strlen($extraField)), + ) . $fileName . $extraField; + } +} diff --git a/vendor/maennchen/zipstream-php/src/OperationMode.php b/vendor/maennchen/zipstream-php/src/OperationMode.php new file mode 100644 index 000000000..dd650f070 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/OperationMode.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream; + +/** + * ZipStream execution operation modes + */ +enum OperationMode +{ + /** + * Stream file into output stream + */ + case NORMAL; + + /** + * Simulate the zip to figure out the resulting file size + * + * This only supports entries where the file size is known beforehand and + * deflation is disabled. + */ + case SIMULATE_STRICT; + + /** + * Simulate the zip to figure out the resulting file size + * + * If the file size is not known beforehand or deflation is enabled, the + * entry streams will be read and rewound. + * + * If the entry does not support rewinding either, you will not be able to + * use the same stream in a later operation mode like `NORMAL`. + */ + case SIMULATE_LAX; +} diff --git a/vendor/maennchen/zipstream-php/src/PackField.php b/vendor/maennchen/zipstream-php/src/PackField.php new file mode 100644 index 000000000..892b4009a --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/PackField.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream; + +use RuntimeException; + +/** + * @internal + * TODO: Make class readonly when requiring PHP 8.2 exclusively + */ +class PackField +{ + public const MAX_V = 0xFFFFFFFF; + + public const MAX_v = 0xFFFF; + + public function __construct( + public readonly string $format, + public readonly int|string $value + ) {} + + /** + * Create a format string and argument list for pack(), then call + * pack() and return the result. + */ + public static function pack(self ...$fields): string + { + $fmt = array_reduce($fields, function (string $acc, self $field) { + return $acc . $field->format; + }, ''); + + $args = array_map(function (self $field) { + switch ($field->format) { + case 'V': + if ($field->value > self::MAX_V) { + throw new RuntimeException(print_r($field->value, true) . ' is larger than 32 bits'); + } + break; + case 'v': + if ($field->value > self::MAX_v) { + throw new RuntimeException(print_r($field->value, true) . ' is larger than 16 bits'); + } + break; + case 'P': break; + default: + break; + } + + return $field->value; + }, $fields); + + return pack($fmt, ...$args); + } +} diff --git a/vendor/maennchen/zipstream-php/src/Time.php b/vendor/maennchen/zipstream-php/src/Time.php new file mode 100644 index 000000000..1b4121ca9 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Time.php @@ -0,0 +1,39 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream; + +use DateInterval; +use DateTimeImmutable; +use DateTimeInterface; +use ZipStream\Exception\DosTimeOverflowException; + +/** + * @internal + */ +abstract class Time +{ + private const DOS_MINIMUM_DATE = '1980-01-01 00:00:00Z'; + + public static function dateTimeToDosTime(DateTimeInterface $dateTime): int + { + $dosMinimumDate = new DateTimeImmutable(self::DOS_MINIMUM_DATE); + + if ($dateTime->getTimestamp() < $dosMinimumDate->getTimestamp()) { + throw new DosTimeOverflowException(dateTime: $dateTime); + } + + $dateTime = DateTimeImmutable::createFromInterface($dateTime)->sub(new DateInterval('P1980Y')); + + [$year, $month, $day, $hour, $minute, $second] = explode(' ', $dateTime->format('Y n j G i s')); + + return + ((int) $year << 25) | + ((int) $month << 21) | + ((int) $day << 16) | + ((int) $hour << 11) | + ((int) $minute << 5) | + ((int) $second >> 1); + } +} diff --git a/vendor/maennchen/zipstream-php/src/Version.php b/vendor/maennchen/zipstream-php/src/Version.php new file mode 100644 index 000000000..c014f8a10 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Version.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream; + +enum Version: int +{ + case STORE = 0x000A; // 1.00 + case DEFLATE = 0x0014; // 2.00 + case ZIP64 = 0x002D; // 4.50 +} diff --git a/vendor/maennchen/zipstream-php/src/Zip64/DataDescriptor.php b/vendor/maennchen/zipstream-php/src/Zip64/DataDescriptor.php new file mode 100644 index 000000000..041c5579d --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Zip64/DataDescriptor.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Zip64; + +use ZipStream\PackField; + +/** + * @internal + */ +abstract class DataDescriptor +{ + private const SIGNATURE = 0x08074b50; + + public static function generate( + int $crc32UncompressedData, + int $compressedSize, + int $uncompressedSize, + ): string { + return PackField::pack( + new PackField(format: 'V', value: self::SIGNATURE), + new PackField(format: 'V', value: $crc32UncompressedData), + new PackField(format: 'P', value: $compressedSize), + new PackField(format: 'P', value: $uncompressedSize), + ); + } +} diff --git a/vendor/maennchen/zipstream-php/src/Zip64/EndOfCentralDirectory.php b/vendor/maennchen/zipstream-php/src/Zip64/EndOfCentralDirectory.php new file mode 100644 index 000000000..08588e49c --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Zip64/EndOfCentralDirectory.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Zip64; + +use ZipStream\PackField; + +/** + * @internal + */ +abstract class EndOfCentralDirectory +{ + private const SIGNATURE = 0x06064b50; + + public static function generate( + int $versionMadeBy, + int $versionNeededToExtract, + int $numberOfThisDisk, + int $numberOfTheDiskWithCentralDirectoryStart, + int $numberOfCentralDirectoryEntriesOnThisDisk, + int $numberOfCentralDirectoryEntries, + int $sizeOfCentralDirectory, + int $centralDirectoryStartOffsetOnDisk, + string $extensibleDataSector, + ): string { + $recordSize = 44 + strlen($extensibleDataSector); // (length of block - 12) = 44; + + /** @psalm-suppress MixedArgument */ + return PackField::pack( + new PackField(format: 'V', value: static::SIGNATURE), + new PackField(format: 'P', value: $recordSize), + new PackField(format: 'v', value: $versionMadeBy), + new PackField(format: 'v', value: $versionNeededToExtract), + new PackField(format: 'V', value: $numberOfThisDisk), + new PackField(format: 'V', value: $numberOfTheDiskWithCentralDirectoryStart), + new PackField(format: 'P', value: $numberOfCentralDirectoryEntriesOnThisDisk), + new PackField(format: 'P', value: $numberOfCentralDirectoryEntries), + new PackField(format: 'P', value: $sizeOfCentralDirectory), + new PackField(format: 'P', value: $centralDirectoryStartOffsetOnDisk), + ) . $extensibleDataSector; + } +} diff --git a/vendor/maennchen/zipstream-php/src/Zip64/EndOfCentralDirectoryLocator.php b/vendor/maennchen/zipstream-php/src/Zip64/EndOfCentralDirectoryLocator.php new file mode 100644 index 000000000..ef431c347 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Zip64/EndOfCentralDirectoryLocator.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Zip64; + +use ZipStream\PackField; + +/** + * @internal + */ +abstract class EndOfCentralDirectoryLocator +{ + private const SIGNATURE = 0x07064b50; + + public static function generate( + int $numberOfTheDiskWithZip64CentralDirectoryStart, + int $zip64centralDirectoryStartOffsetOnDisk, + int $totalNumberOfDisks, + ): string { + /** @psalm-suppress MixedArgument */ + return PackField::pack( + new PackField(format: 'V', value: static::SIGNATURE), + new PackField(format: 'V', value: $numberOfTheDiskWithZip64CentralDirectoryStart), + new PackField(format: 'P', value: $zip64centralDirectoryStartOffsetOnDisk), + new PackField(format: 'V', value: $totalNumberOfDisks), + ); + } +} diff --git a/vendor/maennchen/zipstream-php/src/Zip64/ExtendedInformationExtraField.php b/vendor/maennchen/zipstream-php/src/Zip64/ExtendedInformationExtraField.php new file mode 100644 index 000000000..aaac51c83 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Zip64/ExtendedInformationExtraField.php @@ -0,0 +1,45 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Zip64; + +use ZipStream\PackField; + +/** + * @internal + */ +abstract class ExtendedInformationExtraField +{ + private const TAG = 0x0001; + + public static function generate( + ?int $originalSize = null, + ?int $compressedSize = null, + ?int $relativeHeaderOffset = null, + ?int $diskStartNumber = null, + ): string { + return PackField::pack( + new PackField(format: 'v', value: self::TAG), + new PackField( + format: 'v', + value: ($originalSize === null ? 0 : 8) + + ($compressedSize === null ? 0 : 8) + + ($relativeHeaderOffset === null ? 0 : 8) + + ($diskStartNumber === null ? 0 : 4) + ), + ...($originalSize === null ? [] : [ + new PackField(format: 'P', value: $originalSize), + ]), + ...($compressedSize === null ? [] : [ + new PackField(format: 'P', value: $compressedSize), + ]), + ...($relativeHeaderOffset === null ? [] : [ + new PackField(format: 'P', value: $relativeHeaderOffset), + ]), + ...($diskStartNumber === null ? [] : [ + new PackField(format: 'V', value: $diskStartNumber), + ]), + ); + } +} diff --git a/vendor/maennchen/zipstream-php/src/ZipStream.php b/vendor/maennchen/zipstream-php/src/ZipStream.php new file mode 100644 index 000000000..3f4f481a8 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/ZipStream.php @@ -0,0 +1,865 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream; + +use Closure; +use DateTimeImmutable; +use DateTimeInterface; +use GuzzleHttp\Psr7\StreamWrapper; +use Psr\Http\Message\StreamInterface; +use RuntimeException; +use ZipStream\Exception\FileNotFoundException; +use ZipStream\Exception\FileNotReadableException; +use ZipStream\Exception\OverflowException; +use ZipStream\Exception\ResourceActionException; + +/** + * Streamed, dynamically generated zip archives. + * + * ## Usage + * + * Streaming zip archives is a simple, three-step process: + * + * 1. Create the zip stream: + * + * ```php + * $zip = new ZipStream(outputName: 'example.zip'); + * ``` + * + * 2. Add one or more files to the archive: + * + * ```php + * // add first file + * $zip->addFile(fileName: 'world.txt', data: 'Hello World'); + * + * // add second file + * $zip->addFile(fileName: 'moon.txt', data: 'Hello Moon'); + * ``` + * + * 3. Finish the zip stream: + * + * ```php + * $zip->finish(); + * ``` + * + * You can also add an archive comment, add comments to individual files, + * and adjust the timestamp of files. See the API documentation for each + * method below for additional information. + * + * ## Example + * + * ```php + * // create a new zip stream object + * $zip = new ZipStream(outputName: 'some_files.zip'); + * + * // list of local files + * $files = array('foo.txt', 'bar.jpg'); + * + * // read and add each file to the archive + * foreach ($files as $path) + * $zip->addFileFromPath(fileName: $path, $path); + * + * // write archive footer to stream + * $zip->finish(); + * ``` + */ +class ZipStream +{ + /** + * This number corresponds to the ZIP version/OS used (2 bytes) + * From: https://www.iana.org/assignments/media-types/application/zip + * The upper byte (leftmost one) indicates the host system (OS) for the + * file. Software can use this information to determine + * the line record format for text files etc. The current + * mappings are: + * + * 0 - MS-DOS and OS/2 (F.A.T. file systems) + * 1 - Amiga 2 - VAX/VMS + * 3 - *nix 4 - VM/CMS + * 5 - Atari ST 6 - OS/2 H.P.F.S. + * 7 - Macintosh 8 - Z-System + * 9 - CP/M 10 thru 255 - unused + * + * The lower byte (rightmost one) indicates the version number of the + * software used to encode the file. The value/10 + * indicates the major version number, and the value + * mod 10 is the minor version number. + * Here we are using 6 for the OS, indicating OS/2 H.P.F.S. + * to prevent file permissions issues upon extract (see #84) + * 0x603 is 00000110 00000011 in binary, so 6 and 3 + * + * @internal + */ + public const ZIP_VERSION_MADE_BY = 0x603; + + private bool $ready = true; + + private int $offset = 0; + + /** + * @var string[] + */ + private array $centralDirectoryRecords = []; + + /** + * @var resource + */ + private $outputStream; + + private readonly Closure $httpHeaderCallback; + + /** + * @var File[] + */ + private array $recordedSimulation = []; + + /** + * Create a new ZipStream object. + * + * ##### Examples + * + * ```php + * // create a new zip file named 'foo.zip' + * $zip = new ZipStream(outputName: 'foo.zip'); + * + * // create a new zip file named 'bar.zip' with a comment + * $zip = new ZipStream( + * outputName: 'bar.zip', + * comment: 'this is a comment for the zip file.', + * ); + * ``` + * + * @param OperationMode $operationMode + * The mode can be used to switch between `NORMAL` and `SIMULATION_*` modes. + * For details see the `OperationMode` documentation. + * + * Default to `NORMAL`. + * + * @param string $comment + * Archive Level Comment + * + * @param StreamInterface|resource|null $outputStream + * Override the output of the archive to a different target. + * + * By default the archive is sent to `STDOUT`. + * + * @param CompressionMethod $defaultCompressionMethod + * How to handle file compression. Legal values are + * `CompressionMethod::DEFLATE` (the default), or + * `CompressionMethod::STORE`. `STORE` sends the file raw and is + * significantly faster, while `DEFLATE` compresses the file and + * is much, much slower. + * + * @param int $defaultDeflateLevel + * Default deflation level. Only relevant if `compressionMethod` + * is `DEFLATE`. + * + * See details of [`deflate_init`](https://www.php.net/manual/en/function.deflate-init.php#refsect1-function.deflate-init-parameters) + * + * @param bool $enableZip64 + * Enable Zip64 extension, supporting very large + * archives (any size > 4 GB or file count > 64k) + * + * @param bool $defaultEnableZeroHeader + * Enable streaming files with single read. + * + * When the zero header is set, the file is streamed into the output + * and the size & checksum are added at the end of the file. This is the + * fastest method and uses the least memory. Unfortunately not all + * ZIP clients fully support this and can lead to clients reporting + * the generated ZIP files as corrupted in combination with other + * circumstances. (Zip64 enabled, using UTF8 in comments / names etc.) + * + * When the zero header is not set, the length & checksum need to be + * defined before the file is actually added. To prevent loading all + * the data into memory, the data has to be read twice. If the data + * which is added is not seekable, this call will fail. + * + * @param bool $sendHttpHeaders + * Boolean indicating whether or not to send + * the HTTP headers for this file. + * + * @param ?Closure $httpHeaderCallback + * The method called to send HTTP headers + * + * @param string|null $outputName + * The name of the created archive. + * + * Only relevant if `$sendHttpHeaders = true`. + * + * @param string $contentDisposition + * HTTP Content-Disposition + * + * Only relevant if `sendHttpHeaders = true`. + * + * @param string $contentType + * HTTP Content Type + * + * Only relevant if `sendHttpHeaders = true`. + * + * @param bool $flushOutput + * Enable flush after every write to output stream. + * + * @return self + */ + public function __construct( + private OperationMode $operationMode = OperationMode::NORMAL, + private readonly string $comment = '', + $outputStream = null, + private readonly CompressionMethod $defaultCompressionMethod = CompressionMethod::DEFLATE, + private readonly int $defaultDeflateLevel = 6, + private readonly bool $enableZip64 = true, + private readonly bool $defaultEnableZeroHeader = true, + private bool $sendHttpHeaders = true, + ?Closure $httpHeaderCallback = null, + private readonly ?string $outputName = null, + private readonly string $contentDisposition = 'attachment', + private readonly string $contentType = 'application/x-zip', + private bool $flushOutput = false, + ) { + $this->outputStream = self::normalizeStream($outputStream); + $this->httpHeaderCallback = $httpHeaderCallback ?? header(...); + } + + /** + * Add a file to the archive. + * + * ##### File Options + * + * See {@see addFileFromPsr7Stream()} + * + * ##### Examples + * + * ```php + * // add a file named 'world.txt' + * $zip->addFile(fileName: 'world.txt', data: 'Hello World!'); + * + * // add a file named 'bar.jpg' with a comment and a last-modified + * // time of two hours ago + * $zip->addFile( + * fileName: 'bar.jpg', + * data: $data, + * comment: 'this is a comment about bar.jpg', + * lastModificationDateTime: new DateTime('2 hours ago'), + * ); + * ``` + * + * @param string $data + * + * contents of file + */ + public function addFile( + string $fileName, + string $data, + string $comment = '', + ?CompressionMethod $compressionMethod = null, + ?int $deflateLevel = null, + ?DateTimeInterface $lastModificationDateTime = null, + ?int $maxSize = null, + ?int $exactSize = null, + ?bool $enableZeroHeader = null, + ): void { + $this->addFileFromCallback( + fileName: $fileName, + callback: fn() => $data, + comment: $comment, + compressionMethod: $compressionMethod, + deflateLevel: $deflateLevel, + lastModificationDateTime: $lastModificationDateTime, + maxSize: $maxSize, + exactSize: $exactSize, + enableZeroHeader: $enableZeroHeader, + ); + } + + /** + * Add a file at path to the archive. + * + * ##### File Options + * + * See {@see addFileFromPsr7Stream()} + * + * ###### Examples + * + * ```php + * // add a file named 'foo.txt' from the local file '/tmp/foo.txt' + * $zip->addFileFromPath( + * fileName: 'foo.txt', + * path: '/tmp/foo.txt', + * ); + * + * // add a file named 'bigfile.rar' from the local file + * // '/usr/share/bigfile.rar' with a comment and a last-modified + * // time of two hours ago + * $zip->addFileFromPath( + * fileName: 'bigfile.rar', + * path: '/usr/share/bigfile.rar', + * comment: 'this is a comment about bigfile.rar', + * lastModificationDateTime: new DateTime('2 hours ago'), + * ); + * ``` + * + * @throws \ZipStream\Exception\FileNotFoundException + * @throws \ZipStream\Exception\FileNotReadableException + */ + public function addFileFromPath( + /** + * name of file in archive (including directory path). + */ + string $fileName, + + /** + * path to file on disk (note: paths should be encoded using + * UNIX-style forward slashes -- e.g '/path/to/some/file'). + */ + string $path, + string $comment = '', + ?CompressionMethod $compressionMethod = null, + ?int $deflateLevel = null, + ?DateTimeInterface $lastModificationDateTime = null, + ?int $maxSize = null, + ?int $exactSize = null, + ?bool $enableZeroHeader = null, + ): void { + if (!is_readable($path)) { + if (!file_exists($path)) { + throw new FileNotFoundException($path); + } + throw new FileNotReadableException($path); + } + + $fileTime = filemtime($path); + if ($fileTime !== false) { + $lastModificationDateTime ??= (new DateTimeImmutable())->setTimestamp($fileTime); + } + + $this->addFileFromCallback( + fileName: $fileName, + callback: function () use ($path) { + + $stream = fopen($path, 'rb'); + + if (!$stream) { + // @codeCoverageIgnoreStart + throw new ResourceActionException('fopen'); + // @codeCoverageIgnoreEnd + } + + return $stream; + }, + comment: $comment, + compressionMethod: $compressionMethod, + deflateLevel: $deflateLevel, + lastModificationDateTime: $lastModificationDateTime, + maxSize: $maxSize, + exactSize: $exactSize, + enableZeroHeader: $enableZeroHeader, + ); + } + + /** + * Add an open stream (resource) to the archive. + * + * ##### File Options + * + * See {@see addFileFromPsr7Stream()} + * + * ##### Examples + * + * ```php + * // create a temporary file stream and write text to it + * $filePointer = tmpfile(); + * fwrite($filePointer, 'The quick brown fox jumped over the lazy dog.'); + * + * // add a file named 'streamfile.txt' from the content of the stream + * $archive->addFileFromStream( + * fileName: 'streamfile.txt', + * stream: $filePointer, + * ); + * ``` + * + * @param resource $stream contents of file as a stream resource + */ + public function addFileFromStream( + string $fileName, + $stream, + string $comment = '', + ?CompressionMethod $compressionMethod = null, + ?int $deflateLevel = null, + ?DateTimeInterface $lastModificationDateTime = null, + ?int $maxSize = null, + ?int $exactSize = null, + ?bool $enableZeroHeader = null, + ): void { + $this->addFileFromCallback( + fileName: $fileName, + callback: fn() => $stream, + comment: $comment, + compressionMethod: $compressionMethod, + deflateLevel: $deflateLevel, + lastModificationDateTime: $lastModificationDateTime, + maxSize: $maxSize, + exactSize: $exactSize, + enableZeroHeader: $enableZeroHeader, + ); + } + + /** + * Add an open stream to the archive. + * + * ##### Examples + * + * ```php + * $stream = $response->getBody(); + * // add a file named 'streamfile.txt' from the content of the stream + * $archive->addFileFromPsr7Stream( + * fileName: 'streamfile.txt', + * stream: $stream, + * ); + * ``` + * + * @param string $fileName + * path of file in archive (including directory) + * + * @param StreamInterface $stream + * contents of file as a stream resource + * + * @param string $comment + * ZIP comment for this file + * + * @param ?CompressionMethod $compressionMethod + * Override `defaultCompressionMethod` + * + * See {@see __construct()} + * + * @param ?int $deflateLevel + * Override `defaultDeflateLevel` + * + * See {@see __construct()} + * + * @param ?DateTimeInterface $lastModificationDateTime + * Set last modification time of file. + * + * Default: `now` + * + * @param ?int $maxSize + * Only read `maxSize` bytes from file. + * + * The file is considered done when either reaching `EOF` + * or the `maxSize`. + * + * @param ?int $exactSize + * Read exactly `exactSize` bytes from file. + * If `EOF` is reached before reading `exactSize` bytes, an error will be + * thrown. The parameter allows for faster size calculations if the `stream` + * does not support `fstat` size or is slow and otherwise known beforehand. + * + * @param ?bool $enableZeroHeader + * Override `defaultEnableZeroHeader` + * + * See {@see __construct()} + */ + public function addFileFromPsr7Stream( + string $fileName, + StreamInterface $stream, + string $comment = '', + ?CompressionMethod $compressionMethod = null, + ?int $deflateLevel = null, + ?DateTimeInterface $lastModificationDateTime = null, + ?int $maxSize = null, + ?int $exactSize = null, + ?bool $enableZeroHeader = null, + ): void { + $this->addFileFromCallback( + fileName: $fileName, + callback: fn() => $stream, + comment: $comment, + compressionMethod: $compressionMethod, + deflateLevel: $deflateLevel, + lastModificationDateTime: $lastModificationDateTime, + maxSize: $maxSize, + exactSize: $exactSize, + enableZeroHeader: $enableZeroHeader, + ); + } + + /** + * Add a file based on a callback. + * + * This is useful when you want to simulate a lot of files without keeping + * all of the file handles open at the same time. + * + * ##### Examples + * + * ```php + * foreach($files as $name => $size) { + * $archive->addFileFromCallback( + * fileName: 'streamfile.txt', + * exactSize: $size, + * callback: function() use($name): Psr\Http\Message\StreamInterface { + * $response = download($name); + * return $response->getBody(); + * } + * ); + * } + * ``` + * + * @param string $fileName + * path of file in archive (including directory) + * + * @param Closure $callback + * @psalm-param Closure(): (resource|StreamInterface|string) $callback + * A callback to get the file contents in the shape of a PHP stream, + * a Psr StreamInterface implementation, or a string. + * + * @param string $comment + * ZIP comment for this file + * + * @param ?CompressionMethod $compressionMethod + * Override `defaultCompressionMethod` + * + * See {@see __construct()} + * + * @param ?int $deflateLevel + * Override `defaultDeflateLevel` + * + * See {@see __construct()} + * + * @param ?DateTimeInterface $lastModificationDateTime + * Set last modification time of file. + * + * Default: `now` + * + * @param ?int $maxSize + * Only read `maxSize` bytes from file. + * + * The file is considered done when either reaching `EOF` + * or the `maxSize`. + * + * @param ?int $exactSize + * Read exactly `exactSize` bytes from file. + * If `EOF` is reached before reading `exactSize` bytes, an error will be + * thrown. The parameter allows for faster size calculations if the `stream` + * does not support `fstat` size or is slow and otherwise known beforehand. + * + * @param ?bool $enableZeroHeader + * Override `defaultEnableZeroHeader` + * + * See {@see __construct()} + */ + public function addFileFromCallback( + string $fileName, + Closure $callback, + string $comment = '', + ?CompressionMethod $compressionMethod = null, + ?int $deflateLevel = null, + ?DateTimeInterface $lastModificationDateTime = null, + ?int $maxSize = null, + ?int $exactSize = null, + ?bool $enableZeroHeader = null, + ): void { + $file = new File( + dataCallback: function () use ($callback, $maxSize) { + $data = $callback(); + + if (is_resource($data)) { + return $data; + } + + if ($data instanceof StreamInterface) { + return StreamWrapper::getResource($data); + } + + + $stream = fopen('php://memory', 'rw+'); + if ($stream === false) { + // @codeCoverageIgnoreStart + throw new ResourceActionException('fopen'); + // @codeCoverageIgnoreEnd + } + if ($maxSize !== null && fwrite($stream, $data, $maxSize) === false) { + // @codeCoverageIgnoreStart + throw new ResourceActionException('fwrite', $stream); + // @codeCoverageIgnoreEnd + } elseif (fwrite($stream, $data) === false) { + // @codeCoverageIgnoreStart + throw new ResourceActionException('fwrite', $stream); + // @codeCoverageIgnoreEnd + } + if (rewind($stream) === false) { + // @codeCoverageIgnoreStart + throw new ResourceActionException('rewind', $stream); + // @codeCoverageIgnoreEnd + } + + return $stream; + + }, + send: $this->send(...), + recordSentBytes: $this->recordSentBytes(...), + operationMode: $this->operationMode, + fileName: $fileName, + startOffset: $this->offset, + compressionMethod: $compressionMethod ?? $this->defaultCompressionMethod, + comment: $comment, + deflateLevel: $deflateLevel ?? $this->defaultDeflateLevel, + lastModificationDateTime: $lastModificationDateTime ?? new DateTimeImmutable(), + maxSize: $maxSize, + exactSize: $exactSize, + enableZip64: $this->enableZip64, + enableZeroHeader: $enableZeroHeader ?? $this->defaultEnableZeroHeader, + ); + + if ($this->operationMode !== OperationMode::NORMAL) { + $this->recordedSimulation[] = $file; + } + + $this->centralDirectoryRecords[] = $file->process(); + } + + /** + * Add a directory to the archive. + * + * ##### File Options + * + * See {@see addFileFromPsr7Stream()} + * + * ##### Examples + * + * ```php + * // add a directory named 'world/' + * $zip->addDirectory(fileName: 'world/'); + * ``` + */ + public function addDirectory( + string $fileName, + string $comment = '', + ?DateTimeInterface $lastModificationDateTime = null, + ): void { + if (!str_ends_with($fileName, '/')) { + $fileName .= '/'; + } + + $this->addFile( + fileName: $fileName, + data: '', + comment: $comment, + compressionMethod: CompressionMethod::STORE, + deflateLevel: null, + lastModificationDateTime: $lastModificationDateTime, + maxSize: 0, + exactSize: 0, + enableZeroHeader: false, + ); + } + + /** + * Executes a previously calculated simulation. + * + * ##### Example + * + * ```php + * $zip = new ZipStream( + * outputName: 'foo.zip', + * operationMode: OperationMode::SIMULATE_STRICT, + * ); + * + * $zip->addFile('test.txt', 'Hello World'); + * + * $size = $zip->finish(); + * + * header('Content-Length: '. $size); + * + * $zip->executeSimulation(); + * ``` + */ + public function executeSimulation(): void + { + if ($this->operationMode !== OperationMode::NORMAL) { + throw new RuntimeException('Zip simulation is not finished.'); + } + + foreach ($this->recordedSimulation as $file) { + $this->centralDirectoryRecords[] = $file->cloneSimulationExecution()->process(); + } + + $this->finish(); + } + + /** + * Write zip footer to stream. + * + * The clase is left in an unusable state after `finish`. + * + * ##### Example + * + * ```php + * // write footer to stream + * $zip->finish(); + * ``` + */ + public function finish(): int + { + $centralDirectoryStartOffsetOnDisk = $this->offset; + $sizeOfCentralDirectory = 0; + + // add trailing cdr file records + foreach ($this->centralDirectoryRecords as $centralDirectoryRecord) { + $this->send($centralDirectoryRecord); + $sizeOfCentralDirectory += strlen($centralDirectoryRecord); + } + + // Add 64bit headers (if applicable) + if (count($this->centralDirectoryRecords) >= 0xFFFF || + $centralDirectoryStartOffsetOnDisk > 0xFFFFFFFF || + $sizeOfCentralDirectory > 0xFFFFFFFF) { + if (!$this->enableZip64) { + throw new OverflowException(); + } + + $this->send(Zip64\EndOfCentralDirectory::generate( + versionMadeBy: self::ZIP_VERSION_MADE_BY, + versionNeededToExtract: Version::ZIP64->value, + numberOfThisDisk: 0, + numberOfTheDiskWithCentralDirectoryStart: 0, + numberOfCentralDirectoryEntriesOnThisDisk: count($this->centralDirectoryRecords), + numberOfCentralDirectoryEntries: count($this->centralDirectoryRecords), + sizeOfCentralDirectory: $sizeOfCentralDirectory, + centralDirectoryStartOffsetOnDisk: $centralDirectoryStartOffsetOnDisk, + extensibleDataSector: '', + )); + + $this->send(Zip64\EndOfCentralDirectoryLocator::generate( + numberOfTheDiskWithZip64CentralDirectoryStart: 0x00, + zip64centralDirectoryStartOffsetOnDisk: $centralDirectoryStartOffsetOnDisk + $sizeOfCentralDirectory, + totalNumberOfDisks: 1, + )); + } + + // add trailing cdr eof record + $numberOfCentralDirectoryEntries = min(count($this->centralDirectoryRecords), 0xFFFF); + $this->send(EndOfCentralDirectory::generate( + numberOfThisDisk: 0x00, + numberOfTheDiskWithCentralDirectoryStart: 0x00, + numberOfCentralDirectoryEntriesOnThisDisk: $numberOfCentralDirectoryEntries, + numberOfCentralDirectoryEntries: $numberOfCentralDirectoryEntries, + sizeOfCentralDirectory: min($sizeOfCentralDirectory, 0xFFFFFFFF), + centralDirectoryStartOffsetOnDisk: min($centralDirectoryStartOffsetOnDisk, 0xFFFFFFFF), + zipFileComment: $this->comment, + )); + + $size = $this->offset; + + // The End + $this->clear(); + + return $size; + } + + /** + * @param StreamInterface|resource|null $outputStream + * @return resource + */ + private static function normalizeStream($outputStream) + { + if ($outputStream instanceof StreamInterface) { + return StreamWrapper::getResource($outputStream); + } + if (is_resource($outputStream)) { + return $outputStream; + } + return fopen('php://output', 'wb'); + } + + /** + * Record sent bytes + */ + private function recordSentBytes(int $sentBytes): void + { + $this->offset += $sentBytes; + } + + /** + * Send string, sending HTTP headers if necessary. + * Flush output after write if configure option is set. + */ + private function send(string $data): void + { + if (!$this->ready) { + throw new RuntimeException('Archive is already finished'); + } + + if ($this->operationMode === OperationMode::NORMAL && $this->sendHttpHeaders) { + $this->sendHttpHeaders(); + $this->sendHttpHeaders = false; + } + + $this->recordSentBytes(strlen($data)); + + if ($this->operationMode === OperationMode::NORMAL) { + if (fwrite($this->outputStream, $data) === false) { + throw new ResourceActionException('fwrite', $this->outputStream); + } + + if ($this->flushOutput) { + // flush output buffer if it is on and flushable + $status = ob_get_status(); + if (isset($status['flags']) && is_int($status['flags']) && ($status['flags'] & PHP_OUTPUT_HANDLER_FLUSHABLE)) { + ob_flush(); + } + + // Flush system buffers after flushing userspace output buffer + flush(); + } + } + } + + /** + * Send HTTP headers for this stream. + */ + private function sendHttpHeaders(): void + { + // grab content disposition + $disposition = $this->contentDisposition; + + if ($this->outputName !== null) { + // Various different browsers dislike various characters here. Strip them all for safety. + $safeOutput = trim(str_replace(['"', "'", '\\', ';', "\n", "\r"], '', $this->outputName)); + + // Check if we need to UTF-8 encode the filename + $urlencoded = rawurlencode($safeOutput); + $disposition .= "; filename*=UTF-8''{$urlencoded}"; + } + + $headers = [ + 'Content-Type' => $this->contentType, + 'Content-Disposition' => $disposition, + 'Pragma' => 'public', + 'Cache-Control' => 'public, must-revalidate', + 'Content-Transfer-Encoding' => 'binary', + ]; + + foreach ($headers as $key => $val) { + ($this->httpHeaderCallback)("$key: $val"); + } + } + + /** + * Clear all internal variables. Note that the stream object is not + * usable after this. + */ + private function clear(): void + { + $this->centralDirectoryRecords = []; + $this->offset = 0; + + if ($this->operationMode === OperationMode::NORMAL) { + $this->ready = false; + $this->recordedSimulation = []; + } else { + $this->operationMode = OperationMode::NORMAL; + } + } +} diff --git a/vendor/maennchen/zipstream-php/src/Zs/ExtendedInformationExtraField.php b/vendor/maennchen/zipstream-php/src/Zs/ExtendedInformationExtraField.php new file mode 100644 index 000000000..bf621bc09 --- /dev/null +++ b/vendor/maennchen/zipstream-php/src/Zs/ExtendedInformationExtraField.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Zs; + +use ZipStream\PackField; + +/** + * @internal + */ +abstract class ExtendedInformationExtraField +{ + private const TAG = 0x5653; + + public static function generate(): string + { + return PackField::pack( + new PackField(format: 'v', value: self::TAG), + new PackField(format: 'v', value: 0x0000), + ); + } +} diff --git a/vendor/maennchen/zipstream-php/test/Assertions.php b/vendor/maennchen/zipstream-php/test/Assertions.php new file mode 100644 index 000000000..8d7670eff --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/Assertions.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test; + +trait Assertions +{ + protected function assertFileContains(string $filePath, string $needle): void + { + $last = ''; + + $handle = fopen($filePath, 'r'); + while (!feof($handle)) { + $line = fgets($handle, 1024); + + if (str_contains($last . $line, $needle)) { + fclose($handle); + return; + } + + $last = $line; + } + + fclose($handle); + + $this->fail("File {$filePath} must contain {$needle}"); + } + + protected function assertFileDoesNotContain(string $filePath, string $needle): void + { + $last = ''; + + $handle = fopen($filePath, 'r'); + while (!feof($handle)) { + $line = fgets($handle, 1024); + + if (str_contains($last . $line, $needle)) { + fclose($handle); + + $this->fail("File {$filePath} must not contain {$needle}"); + } + + $last = $line; + } + + fclose($handle); + } +} diff --git a/vendor/maennchen/zipstream-php/test/CentralDirectoryFileHeaderTest.php b/vendor/maennchen/zipstream-php/test/CentralDirectoryFileHeaderTest.php new file mode 100644 index 000000000..5457b4f44 --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/CentralDirectoryFileHeaderTest.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test; + +use DateTimeImmutable; +use PHPUnit\Framework\TestCase; +use ZipStream\CentralDirectoryFileHeader; +use ZipStream\CompressionMethod; + +class CentralDirectoryFileHeaderTest extends TestCase +{ + public function testSerializesCorrectly(): void + { + $dateTime = new DateTimeImmutable('2022-01-01 01:01:01Z'); + + $header = CentralDirectoryFileHeader::generate( + versionMadeBy: 0x603, + versionNeededToExtract: 0x002D, + generalPurposeBitFlag: 0x2222, + compressionMethod: CompressionMethod::DEFLATE, + lastModificationDateTime: $dateTime, + crc32: 0x11111111, + compressedSize: 0x77777777, + uncompressedSize: 0x99999999, + fileName: 'test.png', + extraField: 'some content', + fileComment: 'some comment', + diskNumberStart: 0, + internalFileAttributes: 0, + externalFileAttributes: 32, + relativeOffsetOfLocalHeader: 0x1234, + ); + + $this->assertSame( + bin2hex($header), + '504b0102' . // 4 bytes; central file header signature + '0306' . // 2 bytes; version made by + '2d00' . // 2 bytes; version needed to extract + '2222' . // 2 bytes; general purpose bit flag + '0800' . // 2 bytes; compression method + '2008' . // 2 bytes; last mod file time + '2154' . // 2 bytes; last mod file date + '11111111' . // 4 bytes; crc-32 + '77777777' . // 4 bytes; compressed size + '99999999' . // 4 bytes; uncompressed size + '0800' . // 2 bytes; file name length (n) + '0c00' . // 2 bytes; extra field length (m) + '0c00' . // 2 bytes; file comment length (o) + '0000' . // 2 bytes; disk number start + '0000' . // 2 bytes; internal file attributes + '20000000' . // 4 bytes; external file attributes + '34120000' . // 4 bytes; relative offset of local header + '746573742e706e67' . // n bytes; file name + '736f6d6520636f6e74656e74' . // m bytes; extra field + '736f6d6520636f6d6d656e74' // o bytes; file comment + ); + } +} diff --git a/vendor/maennchen/zipstream-php/test/DataDescriptorTest.php b/vendor/maennchen/zipstream-php/test/DataDescriptorTest.php new file mode 100644 index 000000000..cc886c74b --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/DataDescriptorTest.php @@ -0,0 +1,26 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test; + +use PHPUnit\Framework\TestCase; +use ZipStream\DataDescriptor; + +class DataDescriptorTest extends TestCase +{ + public function testSerializesCorrectly(): void + { + $this->assertSame( + bin2hex(DataDescriptor::generate( + crc32UncompressedData: 0x11111111, + compressedSize: 0x77777777, + uncompressedSize: 0x99999999, + )), + '504b0708' . // 4 bytes; Optional data descriptor signature = 0x08074b50 + '11111111' . // 4 bytes; CRC-32 of uncompressed data + '77777777' . // 4 bytes; Compressed size + '99999999' // 4 bytes; Uncompressed size + ); + } +} diff --git a/vendor/maennchen/zipstream-php/test/EndOfCentralDirectoryTest.php b/vendor/maennchen/zipstream-php/test/EndOfCentralDirectoryTest.php new file mode 100644 index 000000000..be0a90743 --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/EndOfCentralDirectoryTest.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test; + +use PHPUnit\Framework\TestCase; +use ZipStream\EndOfCentralDirectory; + +class EndOfCentralDirectoryTest extends TestCase +{ + public function testSerializesCorrectly(): void + { + $this->assertSame( + bin2hex(EndOfCentralDirectory::generate( + numberOfThisDisk: 0x00, + numberOfTheDiskWithCentralDirectoryStart: 0x00, + numberOfCentralDirectoryEntriesOnThisDisk: 0x10, + numberOfCentralDirectoryEntries: 0x10, + sizeOfCentralDirectory: 0x22, + centralDirectoryStartOffsetOnDisk: 0x33, + zipFileComment: 'foo', + )), + '504b0506' . // 4 bytes; end of central dir signature 0x06054b50 + '0000' . // 2 bytes; number of this disk + '0000' . // 2 bytes; number of the disk with the start of the central directory + '1000' . // 2 bytes; total number of entries in the central directory on this disk + '1000' . // 2 bytes; total number of entries in the central directory + '22000000' . // 4 bytes; size of the central directory + '33000000' . // 4 bytes; offset of start of central directory with respect to the starting disk number + '0300' . // 2 bytes; .ZIP file comment length + bin2hex('foo') + ); + } +} diff --git a/vendor/maennchen/zipstream-php/test/EndlessCycleStream.php b/vendor/maennchen/zipstream-php/test/EndlessCycleStream.php new file mode 100644 index 000000000..d9e7df1fb --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/EndlessCycleStream.php @@ -0,0 +1,104 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test; + +use Psr\Http\Message\StreamInterface; +use RuntimeException; + +class EndlessCycleStream implements StreamInterface +{ + private int $offset = 0; + + public function __construct(private readonly string $toRepeat = '0') {} + + public function __toString(): string + { + throw new RuntimeException('Infinite Stream!'); + } + + public function close(): void + { + $this->detach(); + } + + /** + * @return null + */ + public function detach() + { + return; + } + + public function getSize(): ?int + { + return null; + } + + public function tell(): int + { + return $this->offset; + } + + public function eof(): bool + { + return false; + } + + public function isSeekable(): bool + { + return true; + } + + public function seek(int $offset, int $whence = SEEK_SET): void + { + switch ($whence) { + case SEEK_SET: + $this->offset = $offset; + break; + case SEEK_CUR: + $this->offset += $offset; + break; + case SEEK_END: + throw new RuntimeException('Infinite Stream!'); + break; + } + } + + public function rewind(): void + { + $this->seek(0); + } + + public function isWritable(): bool + { + return false; + } + + public function write(string $string): int + { + throw new RuntimeException('Not writeable'); + } + + public function isReadable(): bool + { + return true; + } + + public function read(int $length): string + { + $this->offset += $length; + return substr(str_repeat($this->toRepeat, (int) ceil($length / strlen($this->toRepeat))), 0, $length); + } + + public function getContents(): string + { + throw new RuntimeException('Infinite Stream!'); + } + + public function getMetadata(?string $key = null): array|null + { + return $key !== null ? null : []; + } +} diff --git a/vendor/maennchen/zipstream-php/test/FaultInjectionResource.php b/vendor/maennchen/zipstream-php/test/FaultInjectionResource.php new file mode 100644 index 000000000..3d4440e8a --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/FaultInjectionResource.php @@ -0,0 +1,141 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test; + +class FaultInjectionResource +{ + public const NAME = 'zipstream-php-test-broken-resource'; + + /** @var resource */ + public $context; + + private array $injectFaults; + + private string $mode; + + /** + * @return resource + */ + public static function getResource(array $injectFaults) + { + self::register(); + + return fopen(self::NAME . '://foobar', 'rw+', false, self::createStreamContext($injectFaults)); + } + + public function stream_open(string $path, string $mode, int $options, string &$opened_path = null): bool + { + $options = stream_context_get_options($this->context); + + if (!isset($options[self::NAME]['injectFaults'])) { + return false; + } + + $this->mode = $mode; + $this->injectFaults = $options[self::NAME]['injectFaults']; + + if ($this->shouldFail(__FUNCTION__)) { + return false; + } + + return true; + } + + public function stream_write(string $data) + { + if ($this->shouldFail(__FUNCTION__)) { + return false; + } + return true; + } + + public function stream_eof() + { + return true; + } + + public function stream_seek(int $offset, int $whence): bool + { + if ($this->shouldFail(__FUNCTION__)) { + return false; + } + + return true; + } + + public function stream_tell(): int + { + if ($this->shouldFail(__FUNCTION__)) { + return false; + } + + return 0; + } + + public static function register(): void + { + if (!in_array(self::NAME, stream_get_wrappers(), true)) { + stream_wrapper_register(self::NAME, __CLASS__); + } + } + + public function stream_stat(): array + { + static $modeMap = [ + 'r' => 33060, + 'rb' => 33060, + 'r+' => 33206, + 'w' => 33188, + 'wb' => 33188, + ]; + + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => $modeMap[$this->mode], + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => 0, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0, + ]; + } + + public function url_stat(string $path, int $flags): array + { + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => 0, + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => 0, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0, + ]; + } + + private static function createStreamContext(array $injectFaults) + { + return stream_context_create([ + self::NAME => ['injectFaults' => $injectFaults], + ]); + } + + private function shouldFail(string $function): bool + { + return in_array($function, $this->injectFaults, true); + } +} diff --git a/vendor/maennchen/zipstream-php/test/LocalFileHeaderTest.php b/vendor/maennchen/zipstream-php/test/LocalFileHeaderTest.php new file mode 100644 index 000000000..196dd0fe3 --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/LocalFileHeaderTest.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test; + +use DateTimeImmutable; +use PHPUnit\Framework\TestCase; +use ZipStream\CompressionMethod; +use ZipStream\LocalFileHeader; + +class LocalFileHeaderTest extends TestCase +{ + public function testSerializesCorrectly(): void + { + $dateTime = new DateTimeImmutable('2022-01-01 01:01:01Z'); + + $header = LocalFileHeader::generate( + versionNeededToExtract: 0x002D, + generalPurposeBitFlag: 0x2222, + compressionMethod: CompressionMethod::DEFLATE, + lastModificationDateTime: $dateTime, + crc32UncompressedData: 0x11111111, + compressedSize: 0x77777777, + uncompressedSize: 0x99999999, + fileName: 'test.png', + extraField: 'some content' + ); + + $this->assertSame( + bin2hex((string) $header), + '504b0304' . // 4 bytes; Local file header signature + '2d00' . // 2 bytes; Version needed to extract (minimum) + '2222' . // 2 bytes; General purpose bit flag + '0800' . // 2 bytes; Compression method; e.g. none = 0, DEFLATE = 8 + '2008' . // 2 bytes; File last modification time + '2154' . // 2 bytes; File last modification date + '11111111' . // 4 bytes; CRC-32 of uncompressed data + '77777777' . // 4 bytes; Compressed size (or 0xffffffff for ZIP64) + '99999999' . // 4 bytes; Uncompressed size (or 0xffffffff for ZIP64) + '0800' . // 2 bytes; File name length (n) + '0c00' . // 2 bytes; Extra field length (m) + '746573742e706e67' . // n bytes; File name + '736f6d6520636f6e74656e74' // m bytes; Extra field + ); + } +} diff --git a/vendor/maennchen/zipstream-php/test/PackFieldTest.php b/vendor/maennchen/zipstream-php/test/PackFieldTest.php new file mode 100644 index 000000000..ecd66bac7 --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/PackFieldTest.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test; + +use PHPUnit\Framework\TestCase; +use RuntimeException; +use ZipStream\PackField; + +class PackFieldTest extends TestCase +{ + public function testPacksFields(): void + { + $this->assertSame( + bin2hex(PackField::pack(new PackField(format: 'v', value: 0x1122))), + '2211', + ); + } + + public function testOverflow2(): void + { + $this->expectException(RuntimeException::class); + + PackField::pack(new PackField(format: 'v', value: 0xFFFFF)); + } + + public function testOverflow4(): void + { + $this->expectException(RuntimeException::class); + + PackField::pack(new PackField(format: 'V', value: 0xFFFFFFFFF)); + } + + public function testUnknownOperator(): void + { + $this->assertSame( + bin2hex(PackField::pack(new PackField(format: 'a', value: 0x1122))), + '34', + ); + } +} diff --git a/vendor/maennchen/zipstream-php/test/ResourceStream.php b/vendor/maennchen/zipstream-php/test/ResourceStream.php new file mode 100644 index 000000000..752a1a357 --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/ResourceStream.php @@ -0,0 +1,159 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test; + +use Psr\Http\Message\StreamInterface; +use RuntimeException; + +/** + * @internal + */ +class ResourceStream implements StreamInterface +{ + public function __construct( + /** + * @var resource + */ + private $stream + ) {} + + public function __toString(): string + { + if ($this->isSeekable()) { + $this->seek(0); + } + return (string) stream_get_contents($this->stream); + } + + public function close(): void + { + $stream = $this->detach(); + if ($stream) { + fclose($stream); + } + } + + public function detach() + { + $result = $this->stream; + // According to the interface, the stream is left in an unusable state; + /** @psalm-suppress PossiblyNullPropertyAssignmentValue */ + $this->stream = null; + return $result; + } + + public function seek(int $offset, int $whence = SEEK_SET): void + { + if (!$this->isSeekable()) { + throw new RuntimeException(); + } + if (fseek($this->stream, $offset, $whence) !== 0) { + // @codeCoverageIgnoreStart + throw new RuntimeException(); + // @codeCoverageIgnoreEnd + } + } + + public function isSeekable(): bool + { + return (bool) $this->getMetadata('seekable'); + } + + public function getMetadata(?string $key = null) + { + $metadata = stream_get_meta_data($this->stream); + return $key !== null ? @$metadata[$key] : $metadata; + } + + public function getSize(): ?int + { + $stats = fstat($this->stream); + return $stats['size']; + } + + public function tell(): int + { + $position = ftell($this->stream); + if ($position === false) { + // @codeCoverageIgnoreStart + throw new RuntimeException(); + // @codeCoverageIgnoreEnd + } + return $position; + } + + public function eof(): bool + { + return feof($this->stream); + } + + public function rewind(): void + { + $this->seek(0); + } + + public function write(string $string): int + { + if (!$this->isWritable()) { + throw new RuntimeException(); + } + if (fwrite($this->stream, $string) === false) { + // @codeCoverageIgnoreStart + throw new RuntimeException(); + // @codeCoverageIgnoreEnd + } + return strlen($string); + } + + public function isWritable(): bool + { + $mode = $this->getMetadata('mode'); + if (!is_string($mode)) { + // @codeCoverageIgnoreStart + throw new RuntimeException('Could not get stream mode from metadata!'); + // @codeCoverageIgnoreEnd + } + return preg_match('/[waxc+]/', $mode) === 1; + } + + public function read(int $length): string + { + if (!$this->isReadable()) { + throw new RuntimeException(); + } + $result = fread($this->stream, $length); + if ($result === false) { + // @codeCoverageIgnoreStart + throw new RuntimeException(); + // @codeCoverageIgnoreEnd + } + return $result; + } + + public function isReadable(): bool + { + $mode = $this->getMetadata('mode'); + if (!is_string($mode)) { + // @codeCoverageIgnoreStart + throw new RuntimeException('Could not get stream mode from metadata!'); + // @codeCoverageIgnoreEnd + } + return preg_match('/[r+]/', $mode) === 1; + } + + public function getContents(): string + { + if (!$this->isReadable()) { + throw new RuntimeException(); + } + $result = stream_get_contents($this->stream); + if ($result === false) { + // @codeCoverageIgnoreStart + throw new RuntimeException(); + // @codeCoverageIgnoreEnd + } + return $result; + } +} diff --git a/vendor/maennchen/zipstream-php/test/Tempfile.php b/vendor/maennchen/zipstream-php/test/Tempfile.php new file mode 100644 index 000000000..7ef9c61f9 --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/Tempfile.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test; + +trait Tempfile +{ + protected string|null $tempfile; + + /** + * @var resource + */ + protected $tempfileStream; + + protected function setUp(): void + { + [$tempfile, $tempfileStream] = $this->getTmpFileStream(); + + $this->tempfile = $tempfile; + $this->tempfileStream = $tempfileStream; + } + + protected function tearDown(): void + { + unlink($this->tempfile); + if (is_resource($this->tempfileStream)) { + fclose($this->tempfileStream); + } + + $this->tempfile = null; + $this->tempfileStream = null; + } + + protected function getTmpFileStream(): array + { + $tmp = tempnam(sys_get_temp_dir(), 'zipstreamtest'); + $stream = fopen($tmp, 'wb+'); + + return [$tmp, $stream]; + } +} diff --git a/vendor/maennchen/zipstream-php/test/TimeTest.php b/vendor/maennchen/zipstream-php/test/TimeTest.php new file mode 100644 index 000000000..61cfe0388 --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/TimeTest.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test; + +use DateTimeImmutable; +use PHPUnit\Framework\TestCase; +use ZipStream\Exception\DosTimeOverflowException; +use ZipStream\Time; + +class TimeTest extends TestCase +{ + public function testNormalDateToDosTime(): void + { + $this->assertSame( + Time::dateTimeToDosTime(new DateTimeImmutable('2014-11-17T17:46:08Z')), + 1165069764 + ); + + // January 1 1980 - DOS Epoch. + $this->assertSame( + Time::dateTimeToDosTime(new DateTimeImmutable('1980-01-01T00:00:00+00:00')), + 2162688 + ); + + // Local timezone different than UTC. + $prevLocalTimezone = date_default_timezone_get(); + date_default_timezone_set('Europe/Berlin'); + $this->assertSame( + Time::dateTimeToDosTime(new DateTimeImmutable('1980-01-01T00:00:00+00:00')), + 2162688 + ); + date_default_timezone_set($prevLocalTimezone); + } + + public function testTooEarlyDateToDosTime(): void + { + $this->expectException(DosTimeOverflowException::class); + + // January 1 1980 is the minimum DOS Epoch. + Time::dateTimeToDosTime(new DateTimeImmutable('1970-01-01T00:00:00+00:00')); + } +} diff --git a/vendor/maennchen/zipstream-php/test/Util.php b/vendor/maennchen/zipstream-php/test/Util.php new file mode 100644 index 000000000..86592b429 --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/Util.php @@ -0,0 +1,127 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test; + +use function fgets; +use function pclose; +use function popen; +use function preg_match; + +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; + +use function strtolower; + +use ZipArchive; + +trait Util +{ + protected function cmdExists(string $command): bool + { + if (strtolower(\substr(PHP_OS, 0, 3)) === 'win') { + $fp = popen("where $command", 'r'); + $result = fgets($fp, 255); + $exists = !preg_match('#Could not find files#', $result); + pclose($fp); + } else { // non-Windows + $fp = popen("which $command", 'r'); + $result = fgets($fp, 255); + $exists = !empty($result); + pclose($fp); + } + + return $exists; + } + + protected function dumpZipContents(string $path): string + { + if (!$this->cmdExists('hexdump')) { + return ''; + } + + $output = []; + + if (!exec("hexdump -C \"$path\" | head -n 50", $output)) { + return ''; + } + + return "\nHexdump:\n" . implode("\n", $output); + } + + protected function validateAndExtractZip(string $zipPath): string + { + $tmpDir = $this->getTmpDir(); + + $zipArchive = new ZipArchive(); + $result = $zipArchive->open($zipPath); + + if ($result !== true) { + $codeName = $this->zipArchiveOpenErrorCodeName($result); + $debugInformation = $this->dumpZipContents($zipPath); + + $this->fail("Failed to open {$zipPath}. Code: $result ($codeName)$debugInformation"); + + return $tmpDir; + } + + $this->assertSame(0, $zipArchive->status); + $this->assertSame(0, $zipArchive->statusSys); + + $zipArchive->extractTo($tmpDir); + $zipArchive->close(); + + return $tmpDir; + } + + protected function zipArchiveOpenErrorCodeName(int $code): string + { + switch ($code) { + case ZipArchive::ER_EXISTS: return 'ER_EXISTS'; + case ZipArchive::ER_INCONS: return 'ER_INCONS'; + case ZipArchive::ER_INVAL: return 'ER_INVAL'; + case ZipArchive::ER_MEMORY: return 'ER_MEMORY'; + case ZipArchive::ER_NOENT: return 'ER_NOENT'; + case ZipArchive::ER_NOZIP: return 'ER_NOZIP'; + case ZipArchive::ER_OPEN: return 'ER_OPEN'; + case ZipArchive::ER_READ: return 'ER_READ'; + case ZipArchive::ER_SEEK: return 'ER_SEEK'; + default: return 'unknown'; + } + } + + protected function getTmpDir(): string + { + $tmp = tempnam(sys_get_temp_dir(), 'zipstreamtest'); + unlink($tmp); + mkdir($tmp) or $this->fail('Failed to make directory'); + + return $tmp; + } + + /** + * @return string[] + */ + protected function getRecursiveFileList(string $path, bool $includeDirectories = false): array + { + $data = []; + $path = (string) realpath($path); + $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)); + + $pathLen = strlen($path); + foreach ($files as $file) { + $filePath = $file->getRealPath(); + + if (is_dir($filePath) && !$includeDirectories) { + continue; + } + + $data[] = substr($filePath, $pathLen + 1); + } + + sort($data); + + return $data; + } +} diff --git a/vendor/maennchen/zipstream-php/test/Zip64/DataDescriptorTest.php b/vendor/maennchen/zipstream-php/test/Zip64/DataDescriptorTest.php new file mode 100644 index 000000000..49fb2ccb2 --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/Zip64/DataDescriptorTest.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test\Zip64; + +use PHPUnit\Framework\TestCase; +use ZipStream\Zip64\DataDescriptor; + +class DataDescriptorTest extends TestCase +{ + public function testSerializesCorrectly(): void + { + $descriptor = DataDescriptor::generate( + crc32UncompressedData: 0x11111111, + compressedSize: (0x77777777 << 32) + 0x66666666, + uncompressedSize: (0x99999999 << 32) + 0x88888888, + ); + + $this->assertSame( + bin2hex($descriptor), + '504b0708' . // 4 bytes; Optional data descriptor signature = 0x08074b50 + '11111111' . // 4 bytes; CRC-32 of uncompressed data + '6666666677777777' . // 8 bytes; Compressed size + '8888888899999999' // 8 bytes; Uncompressed size + ); + } +} diff --git a/vendor/maennchen/zipstream-php/test/Zip64/EndOfCentralDirectoryLocatorTest.php b/vendor/maennchen/zipstream-php/test/Zip64/EndOfCentralDirectoryLocatorTest.php new file mode 100644 index 000000000..271a29862 --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/Zip64/EndOfCentralDirectoryLocatorTest.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test\Zip64; + +use PHPUnit\Framework\TestCase; +use ZipStream\Zip64\EndOfCentralDirectoryLocator; + +class EndOfCentralDirectoryLocatorTest extends TestCase +{ + public function testSerializesCorrectly(): void + { + $descriptor = EndOfCentralDirectoryLocator::generate( + numberOfTheDiskWithZip64CentralDirectoryStart: 0x11111111, + zip64centralDirectoryStartOffsetOnDisk: (0x22222222 << 32) + 0x33333333, + totalNumberOfDisks: 0x44444444, + ); + + $this->assertSame( + bin2hex($descriptor), + '504b0607' . // 4 bytes; zip64 end of central dir locator signature - 0x07064b50 + '11111111' . // 4 bytes; number of the disk with the start of the zip64 end of central directory + '3333333322222222' . // 28 bytes; relative offset of the zip64 end of central directory record + '44444444' // 4 bytes;total number of disks + ); + } +} diff --git a/vendor/maennchen/zipstream-php/test/Zip64/EndOfCentralDirectoryTest.php b/vendor/maennchen/zipstream-php/test/Zip64/EndOfCentralDirectoryTest.php new file mode 100644 index 000000000..b86fb1781 --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/Zip64/EndOfCentralDirectoryTest.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test\Zip64; + +use PHPUnit\Framework\TestCase; +use ZipStream\Zip64\EndOfCentralDirectory; + +class EndOfCentralDirectoryTest extends TestCase +{ + public function testSerializesCorrectly(): void + { + $descriptor = EndOfCentralDirectory::generate( + versionMadeBy: 0x3333, + versionNeededToExtract: 0x4444, + numberOfThisDisk: 0x55555555, + numberOfTheDiskWithCentralDirectoryStart: 0x66666666, + numberOfCentralDirectoryEntriesOnThisDisk: (0x77777777 << 32) + 0x88888888, + numberOfCentralDirectoryEntries: (0x99999999 << 32) + 0xAAAAAAAA, + sizeOfCentralDirectory: (0xBBBBBBBB << 32) + 0xCCCCCCCC, + centralDirectoryStartOffsetOnDisk: (0xDDDDDDDD << 32) + 0xEEEEEEEE, + extensibleDataSector: 'foo', + ); + + $this->assertSame( + bin2hex($descriptor), + '504b0606' . // 4 bytes;zip64 end of central dir signature - 0x06064b50 + '2f00000000000000' . // 8 bytes; size of zip64 end of central directory record + '3333' . // 2 bytes; version made by + '4444' . // 2 bytes; version needed to extract + '55555555' . // 4 bytes; number of this disk + '66666666' . // 4 bytes; number of the disk with the start of the central directory + '8888888877777777' . // 8 bytes; total number of entries in the central directory on this disk + 'aaaaaaaa99999999' . // 8 bytes; total number of entries in the central directory + 'ccccccccbbbbbbbb' . // 8 bytes; size of the central directory + 'eeeeeeeedddddddd' . // 8 bytes; offset of start of central directory with respect to the starting disk number + bin2hex('foo') + ); + } +} diff --git a/vendor/maennchen/zipstream-php/test/Zip64/ExtendedInformationExtraFieldTest.php b/vendor/maennchen/zipstream-php/test/Zip64/ExtendedInformationExtraFieldTest.php new file mode 100644 index 000000000..904783d86 --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/Zip64/ExtendedInformationExtraFieldTest.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test\Zip64; + +use PHPUnit\Framework\TestCase; +use ZipStream\Zip64\ExtendedInformationExtraField; + +class ExtendedInformationExtraFieldTest extends TestCase +{ + public function testSerializesCorrectly(): void + { + $extraField = ExtendedInformationExtraField::generate( + originalSize: (0x77777777 << 32) + 0x66666666, + compressedSize: (0x99999999 << 32) + 0x88888888, + relativeHeaderOffset: (0x22222222 << 32) + 0x11111111, + diskStartNumber: 0x33333333, + ); + + $this->assertSame( + bin2hex($extraField), + '0100' . // 2 bytes; Tag for this "extra" block type + '1c00' . // 2 bytes; Size of this "extra" block + '6666666677777777' . // 8 bytes; Original uncompressed file size + '8888888899999999' . // 8 bytes; Size of compressed data + '1111111122222222' . // 8 bytes; Offset of local header record + '33333333' // 4 bytes; Number of the disk on which this file starts + ); + } + + public function testSerializesEmptyCorrectly(): void + { + $extraField = ExtendedInformationExtraField::generate(); + + $this->assertSame( + bin2hex($extraField), + '0100' . // 2 bytes; Tag for this "extra" block type + '0000' // 2 bytes; Size of this "extra" block + ); + } +} diff --git a/vendor/maennchen/zipstream-php/test/ZipStreamTest.php b/vendor/maennchen/zipstream-php/test/ZipStreamTest.php new file mode 100644 index 000000000..9b10ba65d --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/ZipStreamTest.php @@ -0,0 +1,1195 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test; + +use DateTimeImmutable; +use GuzzleHttp\Psr7\Response; +use GuzzleHttp\Psr7\StreamWrapper; +use org\bovigo\vfs\vfsStream; +use PHPUnit\Framework\TestCase; +use Psr\Http\Message\StreamInterface; +use RuntimeException; +use ZipArchive; +use ZipStream\CompressionMethod; +use ZipStream\Exception\FileNotFoundException; +use ZipStream\Exception\FileNotReadableException; +use ZipStream\Exception\FileSizeIncorrectException; +use ZipStream\Exception\OverflowException; +use ZipStream\Exception\ResourceActionException; +use ZipStream\Exception\SimulationFileUnknownException; +use ZipStream\Exception\StreamNotReadableException; +use ZipStream\Exception\StreamNotSeekableException; +use ZipStream\OperationMode; +use ZipStream\PackField; +use ZipStream\ZipStream; + +class ZipStreamTest extends TestCase +{ + use Util; + use Assertions; + use Tempfile; + + public function testAddFile(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + ); + + $zip->addFile('sample.txt', 'Sample String Data'); + $zip->addFile('test/sample.txt', 'More Simple Sample Data'); + + $zip->finish(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + + $files = $this->getRecursiveFileList($tmpDir); + $this->assertSame(['sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'], $files); + + $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data'); + $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data'); + } + + public function testAddFileUtf8NameComment(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + ); + + $name = 'árvíztűrő tükörfúrógép.txt'; + $content = 'Sample String Data'; + $comment = + 'Filename has every special characters ' . + 'from Hungarian language in lowercase. ' . + 'In uppercase: ÁÍŰŐÜÖÚÓÉ'; + + $zip->addFile(fileName: $name, data: $content, comment: $comment); + $zip->finish(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + + $files = $this->getRecursiveFileList($tmpDir); + $this->assertSame([$name], $files); + $this->assertStringEqualsFile($tmpDir . '/' . $name, $content); + + $zipArchive = new ZipArchive(); + $zipArchive->open($this->tempfile); + $this->assertSame($comment, $zipArchive->getCommentName($name)); + } + + public function testAddFileUtf8NameNonUtfComment(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + ); + + $name = 'á.txt'; + $content = 'any'; + $comment = mb_convert_encoding('á', 'ISO-8859-2', 'UTF-8'); + + // @see https://libzip.org/documentation/zip_file_get_comment.html + // + // mb_convert_encoding hasn't CP437. + // nearly CP850 (DOS-Latin-1) + $guessComment = mb_convert_encoding($comment, 'UTF-8', 'CP850'); + + $zip->addFile(fileName: $name, data: $content, comment: $comment); + + $zip->finish(); + + $zipArch = new ZipArchive(); + $zipArch->open($this->tempfile); + $this->assertSame($guessComment, $zipArch->getCommentName($name)); + $this->assertSame($comment, $zipArch->getCommentName($name, ZipArchive::FL_ENC_RAW)); + } + + public function testAddFileWithStorageMethod(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + ); + + $zip->addFile(fileName: 'sample.txt', data: 'Sample String Data', compressionMethod: CompressionMethod::STORE); + $zip->addFile(fileName: 'test/sample.txt', data: 'More Simple Sample Data'); + $zip->finish(); + + $zipArchive = new ZipArchive(); + $zipArchive->open($this->tempfile); + + $sample1 = $zipArchive->statName('sample.txt'); + $sample12 = $zipArchive->statName('test/sample.txt'); + $this->assertSame($sample1['comp_method'], CompressionMethod::STORE->value); + $this->assertSame($sample12['comp_method'], CompressionMethod::DEFLATE->value); + + $zipArchive->close(); + } + + public function testAddFileFromPath(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + ); + + [$tmpExample, $streamExample] = $this->getTmpFileStream(); + fwrite($streamExample, 'Sample String Data'); + fclose($streamExample); + $zip->addFileFromPath(fileName: 'sample.txt', path: $tmpExample); + + [$tmpExample, $streamExample] = $this->getTmpFileStream(); + fwrite($streamExample, 'More Simple Sample Data'); + fclose($streamExample); + $zip->addFileFromPath(fileName: 'test/sample.txt', path: $tmpExample); + + $zip->finish(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + + $files = $this->getRecursiveFileList($tmpDir); + $this->assertSame(['sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'], $files); + + $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data'); + $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data'); + + unlink($tmpExample); + } + + public function testAddFileFromPathFileNotFoundException(): void + { + $this->expectException(FileNotFoundException::class); + + // Get ZipStream Object + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + ); + + // Trigger error by adding a file which doesn't exist + $zip->addFileFromPath(fileName: 'foobar.php', path: '/foo/bar/foobar.php'); + } + + public function testAddFileFromPathFileNotReadableException(): void + { + $this->expectException(FileNotReadableException::class); + + // create new virtual filesystem + $root = vfsStream::setup('vfs'); + // create a virtual file with no permissions + $file = vfsStream::newFile('foo.txt', 0)->at($root)->setContent('bar'); + + // Get ZipStream Object + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + ); + + $zip->addFileFromPath('foo.txt', $file->url()); + } + + public function testAddFileFromPathWithStorageMethod(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + ); + + [$tmpExample, $streamExample] = $this->getTmpFileStream(); + fwrite($streamExample, 'Sample String Data'); + fclose($streamExample); + $zip->addFileFromPath(fileName: 'sample.txt', path: $tmpExample, compressionMethod: CompressionMethod::STORE); + + [$tmpExample, $streamExample] = $this->getTmpFileStream(); + fwrite($streamExample, 'More Simple Sample Data'); + fclose($streamExample); + $zip->addFileFromPath('test/sample.txt', $tmpExample); + + $zip->finish(); + + $zipArchive = new ZipArchive(); + $zipArchive->open($this->tempfile); + + $sample1 = $zipArchive->statName('sample.txt'); + $this->assertSame(CompressionMethod::STORE->value, $sample1['comp_method']); + + $sample2 = $zipArchive->statName('test/sample.txt'); + $this->assertSame(CompressionMethod::DEFLATE->value, $sample2['comp_method']); + + $zipArchive->close(); + } + + public function testAddLargeFileFromPath(): void + { + foreach ([CompressionMethod::DEFLATE, CompressionMethod::STORE] as $compressionMethod) { + foreach ([false, true] as $zeroHeader) { + foreach ([false, true] as $zip64) { + if ($zeroHeader && $compressionMethod === CompressionMethod::DEFLATE) { + continue; + } + $this->addLargeFileFileFromPath( + compressionMethod: $compressionMethod, + zeroHeader: $zeroHeader, + zip64: $zip64 + ); + } + } + } + } + + public function testAddFileFromStream(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + ); + + // In this test we can't use temporary stream to feed data + // because zlib.deflate filter gives empty string before PHP 7 + // it works fine with file stream + $streamExample = fopen(__FILE__, 'rb'); + $zip->addFileFromStream('sample.txt', $streamExample); + fclose($streamExample); + + $streamExample2 = fopen('php://temp', 'wb+'); + fwrite($streamExample2, 'More Simple Sample Data'); + rewind($streamExample2); // move the pointer back to the beginning of file. + $zip->addFileFromStream('test/sample.txt', $streamExample2); //, $fileOptions); + fclose($streamExample2); + + $zip->finish(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + + $files = $this->getRecursiveFileList($tmpDir); + $this->assertSame(['sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'], $files); + + $this->assertStringEqualsFile(__FILE__, file_get_contents($tmpDir . '/sample.txt')); + $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data'); + } + + public function testAddFileFromStreamUnreadableInput(): void + { + $this->expectException(StreamNotReadableException::class); + + [$tmpInput] = $this->getTmpFileStream(); + + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + ); + + $streamUnreadable = fopen($tmpInput, 'w'); + + $zip->addFileFromStream('sample.json', $streamUnreadable); + } + + public function testAddFileFromStreamBrokenOutputWrite(): void + { + $this->expectException(ResourceActionException::class); + + $outputStream = FaultInjectionResource::getResource(['stream_write']); + + $zip = new ZipStream( + outputStream: $outputStream, + sendHttpHeaders: false, + ); + + $zip->addFile('sample.txt', 'foobar'); + } + + public function testAddFileFromStreamBrokenInputRewind(): void + { + $this->expectException(ResourceActionException::class); + + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + defaultEnableZeroHeader: false, + ); + + $fileStream = FaultInjectionResource::getResource(['stream_seek']); + + $zip->addFileFromStream('sample.txt', $fileStream, maxSize: 0); + } + + public function testAddFileFromStreamUnseekableInputWithoutZeroHeader(): void + { + $this->expectException(StreamNotSeekableException::class); + + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + defaultEnableZeroHeader: false, + ); + + if (file_exists('/dev/null')) { + $streamUnseekable = fopen('/dev/null', 'w+'); + } elseif (file_exists('NUL')) { + $streamUnseekable = fopen('NUL', 'w+'); + } else { + $this->markTestSkipped('Needs file /dev/null'); + } + + $zip->addFileFromStream('sample.txt', $streamUnseekable, maxSize: 2); + } + + public function testAddFileFromStreamUnseekableInputWithZeroHeader(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + defaultEnableZeroHeader: true, + defaultCompressionMethod: CompressionMethod::STORE, + ); + + $streamUnseekable = StreamWrapper::getResource(new class ('test') extends EndlessCycleStream { + public function isSeekable(): bool + { + return false; + } + + public function seek(int $offset, int $whence = SEEK_SET): void + { + throw new RuntimeException('Not seekable'); + } + }); + + $zip->addFileFromStream('sample.txt', $streamUnseekable, maxSize: 7); + + $zip->finish(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + + $files = $this->getRecursiveFileList($tmpDir); + $this->assertSame(['sample.txt'], $files); + + $this->assertSame(filesize($tmpDir . '/sample.txt'), 7); + } + + public function testAddFileFromStreamWithStorageMethod(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + ); + + $streamExample = fopen('php://temp', 'wb+'); + fwrite($streamExample, 'Sample String Data'); + rewind($streamExample); // move the pointer back to the beginning of file. + $zip->addFileFromStream('sample.txt', $streamExample, compressionMethod: CompressionMethod::STORE); + fclose($streamExample); + + $streamExample2 = fopen('php://temp', 'bw+'); + fwrite($streamExample2, 'More Simple Sample Data'); + rewind($streamExample2); // move the pointer back to the beginning of file. + $zip->addFileFromStream('test/sample.txt', $streamExample2, compressionMethod: CompressionMethod::DEFLATE); + fclose($streamExample2); + + $zip->finish(); + + $zipArchive = new ZipArchive(); + $zipArchive->open($this->tempfile); + + $sample1 = $zipArchive->statName('sample.txt'); + $this->assertSame(CompressionMethod::STORE->value, $sample1['comp_method']); + + $sample2 = $zipArchive->statName('test/sample.txt'); + $this->assertSame(CompressionMethod::DEFLATE->value, $sample2['comp_method']); + + $zipArchive->close(); + } + + public function testAddFileFromPsr7Stream(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + ); + + $body = 'Sample String Data'; + $response = new Response(200, [], $body); + + $zip->addFileFromPsr7Stream('sample.json', $response->getBody()); + $zip->finish(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + + $files = $this->getRecursiveFileList($tmpDir); + $this->assertSame(['sample.json'], $files); + $this->assertStringEqualsFile($tmpDir . '/sample.json', $body); + } + + /** + * @group slow + */ + public function testAddLargeFileFromPsr7Stream(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + enableZip64: true, + ); + + $zip->addFileFromPsr7Stream( + fileName: 'sample.json', + stream: new EndlessCycleStream('0'), + maxSize: 0x100000000, + compressionMethod: CompressionMethod::STORE, + lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'), + ); + $zip->finish(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + + $files = $this->getRecursiveFileList($tmpDir); + $this->assertSame(['sample.json'], $files); + $this->assertFileIsReadable($tmpDir . '/sample.json'); + $this->assertStringStartsWith('000000', file_get_contents(filename: $tmpDir . '/sample.json', length: 20)); + } + + public function testContinueFinishedZip(): void + { + $this->expectException(RuntimeException::class); + + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + ); + $zip->finish(); + + $zip->addFile('sample.txt', '1234'); + } + + /** + * @group slow + */ + public function testManyFilesWithoutZip64(): void + { + $this->expectException(OverflowException::class); + + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + enableZip64: false, + ); + + for ($i = 0; $i <= 0xFFFF; $i++) { + $zip->addFile('sample' . $i, ''); + } + + $zip->finish(); + } + + /** + * @group slow + */ + public function testManyFilesWithZip64(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + enableZip64: true, + ); + + for ($i = 0; $i <= 0xFFFF; $i++) { + $zip->addFile('sample' . $i, ''); + } + + $zip->finish(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + + $files = $this->getRecursiveFileList($tmpDir); + + $this->assertSame(count($files), 0x10000); + } + + /** + * @group slow + */ + public function testLongZipWithout64(): void + { + $this->expectException(OverflowException::class); + + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + enableZip64: false, + defaultCompressionMethod: CompressionMethod::STORE, + ); + + for ($i = 0; $i < 4; $i++) { + $zip->addFileFromPsr7Stream( + fileName: 'sample' . $i, + stream: new EndlessCycleStream('0'), + maxSize: 0xFFFFFFFF, + compressionMethod: CompressionMethod::STORE, + lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'), + ); + } + } + + /** + * @group slow + */ + public function testLongZipWith64(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + enableZip64: true, + defaultCompressionMethod: CompressionMethod::STORE, + ); + + for ($i = 0; $i < 4; $i++) { + $zip->addFileFromPsr7Stream( + fileName: 'sample' . $i, + stream: new EndlessCycleStream('0'), + maxSize: 0x5FFFFFFF, + compressionMethod: CompressionMethod::STORE, + lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'), + ); + } + + $zip->finish(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + + $files = $this->getRecursiveFileList($tmpDir); + $this->assertSame(['sample0', 'sample1', 'sample2', 'sample3'], $files); + } + + /** + * @group slow + */ + public function testAddLargeFileWithoutZip64WithZeroHeader(): void + { + $this->expectException(OverflowException::class); + + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + enableZip64: false, + defaultEnableZeroHeader: true, + ); + + $zip->addFileFromPsr7Stream( + fileName: 'sample.json', + stream: new EndlessCycleStream('0'), + maxSize: 0x100000000, + compressionMethod: CompressionMethod::STORE, + lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'), + ); + } + + /** + * @group slow + */ + public function testAddsZip64HeaderWhenNeeded(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + enableZip64: true, + defaultEnableZeroHeader: false, + ); + + $zip->addFileFromPsr7Stream( + fileName: 'sample.json', + stream: new EndlessCycleStream('0'), + maxSize: 0x100000000, + compressionMethod: CompressionMethod::STORE, + lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'), + ); + + $zip->finish(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + $files = $this->getRecursiveFileList($tmpDir); + + $this->assertSame(['sample.json'], $files); + $this->assertFileContains($this->tempfile, PackField::pack( + new PackField(format: 'V', value: 0x06064b50) + )); + } + + /** + * @group slow + */ + public function testDoesNotAddZip64HeaderWhenNotNeeded(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + enableZip64: true, + defaultEnableZeroHeader: false, + ); + + $zip->addFileFromPsr7Stream( + fileName: 'sample.json', + stream: new EndlessCycleStream('0'), + maxSize: 0x10, + compressionMethod: CompressionMethod::STORE, + lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'), + ); + + $zip->finish(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + $files = $this->getRecursiveFileList($tmpDir); + + $this->assertSame(['sample.json'], $files); + $this->assertFileDoesNotContain($this->tempfile, PackField::pack( + new PackField(format: 'V', value: 0x06064b50) + )); + } + + /** + * @group slow + */ + public function testAddLargeFileWithoutZip64WithoutZeroHeader(): void + { + $this->expectException(OverflowException::class); + + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + enableZip64: false, + defaultEnableZeroHeader: false, + ); + + $zip->addFileFromPsr7Stream( + fileName: 'sample.json', + stream: new EndlessCycleStream('0'), + maxSize: 0x100000000, + compressionMethod: CompressionMethod::STORE, + lastModificationDateTime: new DateTimeImmutable('2022-01-01 01:01:01Z'), + ); + } + + public function testAddFileFromPsr7StreamWithOutputToPsr7Stream(): void + { + $psr7OutputStream = new ResourceStream($this->tempfileStream); + + $zip = new ZipStream( + outputStream: $psr7OutputStream, + sendHttpHeaders: false, + ); + + $body = 'Sample String Data'; + $response = new Response(200, [], $body); + + $zip->addFileFromPsr7Stream( + fileName: 'sample.json', + stream: $response->getBody(), + compressionMethod: CompressionMethod::STORE, + ); + $zip->finish(); + $psr7OutputStream->close(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + $files = $this->getRecursiveFileList($tmpDir); + + $this->assertSame(['sample.json'], $files); + $this->assertStringEqualsFile($tmpDir . '/sample.json', $body); + } + + public function testAddFileFromPsr7StreamWithFileSizeSet(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + ); + + $body = 'Sample String Data'; + $fileSize = strlen($body); + // Add fake padding + $fakePadding = "\0\0\0\0\0\0"; + $response = new Response(200, [], $body . $fakePadding); + + $zip->addFileFromPsr7Stream( + fileName: 'sample.json', + stream: $response->getBody(), + compressionMethod: CompressionMethod::STORE, + maxSize: $fileSize + ); + $zip->finish(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + + $files = $this->getRecursiveFileList($tmpDir); + $this->assertSame(['sample.json'], $files); + $this->assertStringEqualsFile($tmpDir . '/sample.json', $body); + } + + public function testCreateArchiveHeaders(): void + { + $headers = []; + + $httpHeaderCallback = function (string $header) use (&$headers) { + $headers[] = $header; + }; + + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: true, + outputName: 'example.zip', + httpHeaderCallback: $httpHeaderCallback, + ); + + $zip->addFile( + fileName: 'sample.json', + data: 'foo', + ); + $zip->finish(); + + $this->assertContains('Content-Type: application/x-zip', $headers); + $this->assertContains("Content-Disposition: attachment; filename*=UTF-8''example.zip", $headers); + $this->assertContains('Pragma: public', $headers); + $this->assertContains('Cache-Control: public, must-revalidate', $headers); + $this->assertContains('Content-Transfer-Encoding: binary', $headers); + } + + public function testCreateArchiveWithFlushOptionSet(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + flushOutput: true, + sendHttpHeaders: false, + ); + + $zip->addFile('sample.txt', 'Sample String Data'); + $zip->addFile('test/sample.txt', 'More Simple Sample Data'); + + $zip->finish(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + + $files = $this->getRecursiveFileList($tmpDir); + $this->assertSame(['sample.txt', 'test' . DIRECTORY_SEPARATOR . 'sample.txt'], $files); + + $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data'); + $this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data'); + } + + public function testCreateArchiveWithOutputBufferingOffAndFlushOptionSet(): void + { + // WORKAROUND (1/2): remove phpunit's output buffer in order to run test without any buffering + ob_end_flush(); + $this->assertSame(0, ob_get_level()); + + $zip = new ZipStream( + outputStream: $this->tempfileStream, + flushOutput: true, + sendHttpHeaders: false, + ); + + $zip->addFile('sample.txt', 'Sample String Data'); + + $zip->finish(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + $this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data'); + + // WORKAROUND (2/2): add back output buffering so that PHPUnit doesn't complain that it is missing + ob_start(); + } + + public function testAddEmptyDirectory(): void + { + $zip = new ZipStream( + outputStream: $this->tempfileStream, + sendHttpHeaders: false, + ); + + $zip->addDirectory('foo'); + + $zip->finish(); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + + $files = $this->getRecursiveFileList($tmpDir, includeDirectories: true); + + $this->assertContains('foo', $files); + + $this->assertFileExists($tmpDir . DIRECTORY_SEPARATOR . 'foo'); + $this->assertDirectoryExists($tmpDir . DIRECTORY_SEPARATOR . 'foo'); + } + + public function testAddFileSimulate(): void + { + $create = function (OperationMode $operationMode): int { + $zip = new ZipStream( + sendHttpHeaders: false, + operationMode: $operationMode, + defaultEnableZeroHeader: true, + outputStream: $this->tempfileStream, + ); + + $zip->addFile('sample.txt', 'Sample String Data'); + $zip->addFile('test/sample.txt', 'More Simple Sample Data'); + + return $zip->finish(); + }; + + + $sizeExpected = $create(OperationMode::NORMAL); + $sizeActual = $create(OperationMode::SIMULATE_LAX); + + $this->assertEquals($sizeExpected, $sizeActual); + } + + public function testAddFileSimulateWithMaxSize(): void + { + $create = function (OperationMode $operationMode): int { + $zip = new ZipStream( + sendHttpHeaders: false, + operationMode: $operationMode, + defaultCompressionMethod: CompressionMethod::STORE, + defaultEnableZeroHeader: true, + outputStream: $this->tempfileStream, + ); + + $zip->addFile('sample.txt', 'Sample String Data', maxSize: 0); + + return $zip->finish(); + }; + + + $sizeExpected = $create(OperationMode::NORMAL); + $sizeActual = $create(OperationMode::SIMULATE_LAX); + + $this->assertEquals($sizeExpected, $sizeActual); + } + + public function testAddFileSimulateWithFstat(): void + { + $create = function (OperationMode $operationMode): int { + $zip = new ZipStream( + sendHttpHeaders: false, + operationMode: $operationMode, + defaultCompressionMethod: CompressionMethod::STORE, + defaultEnableZeroHeader: true, + outputStream: $this->tempfileStream, + ); + + $zip->addFile('sample.txt', 'Sample String Data'); + $zip->addFile('test/sample.txt', 'More Simple Sample Data'); + + return $zip->finish(); + }; + + + $sizeExpected = $create(OperationMode::NORMAL); + $sizeActual = $create(OperationMode::SIMULATE_LAX); + + $this->assertEquals($sizeExpected, $sizeActual); + } + + public function testAddFileSimulateWithExactSizeZero(): void + { + $create = function (OperationMode $operationMode): int { + $zip = new ZipStream( + sendHttpHeaders: false, + operationMode: $operationMode, + defaultCompressionMethod: CompressionMethod::STORE, + defaultEnableZeroHeader: true, + outputStream: $this->tempfileStream, + ); + + $zip->addFile('sample.txt', 'Sample String Data', exactSize: 18); + + return $zip->finish(); + }; + + + $sizeExpected = $create(OperationMode::NORMAL); + $sizeActual = $create(OperationMode::SIMULATE_LAX); + + $this->assertEquals($sizeExpected, $sizeActual); + } + + public function testAddFileSimulateWithExactSizeInitial(): void + { + $create = function (OperationMode $operationMode): int { + $zip = new ZipStream( + sendHttpHeaders: false, + operationMode: $operationMode, + defaultCompressionMethod: CompressionMethod::STORE, + defaultEnableZeroHeader: false, + outputStream: $this->tempfileStream, + ); + + $zip->addFile('sample.txt', 'Sample String Data', exactSize: 18); + + return $zip->finish(); + }; + + $sizeExpected = $create(OperationMode::NORMAL); + $sizeActual = $create(OperationMode::SIMULATE_LAX); + + $this->assertEquals($sizeExpected, $sizeActual); + } + + public function testAddFileSimulateWithZeroSizeInFstat(): void + { + $create = function (OperationMode $operationMode): int { + $zip = new ZipStream( + sendHttpHeaders: false, + operationMode: $operationMode, + defaultCompressionMethod: CompressionMethod::STORE, + defaultEnableZeroHeader: false, + outputStream: $this->tempfileStream, + ); + + $zip->addFileFromPsr7Stream('sample.txt', new class implements StreamInterface { + public $pos = 0; + + public function __toString(): string + { + return 'test'; + } + + public function close(): void {} + + public function detach() {} + + public function getSize(): ?int + { + return null; + } + + public function tell(): int + { + return $this->pos; + } + + public function eof(): bool + { + return $this->pos >= 4; + } + + public function isSeekable(): bool + { + return true; + } + + public function seek(int $offset, int $whence = SEEK_SET): void + { + $this->pos = $offset; + } + + public function rewind(): void + { + $this->pos = 0; + } + + public function isWritable(): bool + { + return false; + } + + public function write(string $string): int + { + return 0; + } + + public function isReadable(): bool + { + return true; + } + + public function read(int $length): string + { + $data = substr('test', $this->pos, $length); + $this->pos += strlen($data); + return $data; + } + + public function getContents(): string + { + return $this->read(4); + } + + public function getMetadata(?string $key = null) + { + return $key !== null ? null : []; + } + }); + + return $zip->finish(); + }; + + $sizeExpected = $create(OperationMode::NORMAL); + $sizeActual = $create(OperationMode::SIMULATE_LAX); + + + $this->assertEquals($sizeExpected, $sizeActual); + } + + public function testAddFileSimulateWithWrongExactSize(): void + { + $this->expectException(FileSizeIncorrectException::class); + + $zip = new ZipStream( + sendHttpHeaders: false, + operationMode: OperationMode::SIMULATE_LAX, + ); + + $zip->addFile('sample.txt', 'Sample String Data', exactSize: 1000); + } + + public function testAddFileSimulateStrictZero(): void + { + $this->expectException(SimulationFileUnknownException::class); + + $zip = new ZipStream( + sendHttpHeaders: false, + operationMode: OperationMode::SIMULATE_STRICT, + defaultEnableZeroHeader: true + ); + + $zip->addFile('sample.txt', 'Sample String Data'); + } + + public function testAddFileSimulateStrictInitial(): void + { + $this->expectException(SimulationFileUnknownException::class); + + $zip = new ZipStream( + sendHttpHeaders: false, + operationMode: OperationMode::SIMULATE_STRICT, + defaultEnableZeroHeader: false + ); + + $zip->addFile('sample.txt', 'Sample String Data'); + } + + public function testAddFileCallbackStrict(): void + { + $this->expectException(SimulationFileUnknownException::class); + + $zip = new ZipStream( + sendHttpHeaders: false, + operationMode: OperationMode::SIMULATE_STRICT, + defaultEnableZeroHeader: false + ); + + $zip->addFileFromCallback('sample.txt', callback: function () { + return ''; + }); + } + + public function testAddFileCallbackLax(): void + { + $zip = new ZipStream( + operationMode: OperationMode::SIMULATE_LAX, + defaultEnableZeroHeader: false, + sendHttpHeaders: false, + ); + + $zip->addFileFromCallback('sample.txt', callback: function () { + return 'Sample String Data'; + }); + + $size = $zip->finish(); + + $this->assertEquals($size, 142); + } + + public function testExecuteSimulation(): void + { + $zip = new ZipStream( + operationMode: OperationMode::SIMULATE_STRICT, + defaultCompressionMethod: CompressionMethod::STORE, + defaultEnableZeroHeader: false, + sendHttpHeaders: false, + outputStream: $this->tempfileStream, + ); + + $zip->addFileFromCallback( + 'sample.txt', + exactSize: 18, + callback: function () { + return 'Sample String Data'; + } + ); + + $zip->addFileFromCallback( + '.gitkeep', + exactSize: 0, + callback: function () { + return ''; + } + ); + + $size = $zip->finish(); + + $this->assertEquals(filesize($this->tempfile), 0); + + $zip->executeSimulation(); + + clearstatcache(); + + $this->assertEquals(filesize($this->tempfile), $size); + + $tmpDir = $this->validateAndExtractZip($this->tempfile); + + $files = $this->getRecursiveFileList($tmpDir); + $this->assertSame(['.gitkeep', 'sample.txt'], $files); + } + + public function testExecuteSimulationBeforeFinish(): void + { + $this->expectException(RuntimeException::class); + + $zip = new ZipStream( + operationMode: OperationMode::SIMULATE_LAX, + defaultEnableZeroHeader: false, + sendHttpHeaders: false, + outputStream: $this->tempfileStream, + ); + + $zip->executeSimulation(); + } + + private function addLargeFileFileFromPath(CompressionMethod $compressionMethod, $zeroHeader, $zip64): void + { + [$tmp, $stream] = $this->getTmpFileStream(); + + $zip = new ZipStream( + outputStream: $stream, + sendHttpHeaders: false, + defaultEnableZeroHeader: $zeroHeader, + enableZip64: $zip64, + ); + + [$tmpExample, $streamExample] = $this->getTmpFileStream(); + for ($i = 0; $i <= 10000; $i++) { + fwrite($streamExample, sha1((string) $i)); + if ($i % 100 === 0) { + fwrite($streamExample, "\n"); + } + } + fclose($streamExample); + $shaExample = sha1_file($tmpExample); + $zip->addFileFromPath('sample.txt', $tmpExample); + unlink($tmpExample); + + $zip->finish(); + fclose($stream); + + $tmpDir = $this->validateAndExtractZip($tmp); + + $files = $this->getRecursiveFileList($tmpDir); + $this->assertSame(['sample.txt'], $files); + + $this->assertSame(sha1_file($tmpDir . '/sample.txt'), $shaExample, "SHA-1 Mismatch Method: {$compressionMethod->value}"); + + unlink($tmp); + } +} diff --git a/vendor/maennchen/zipstream-php/test/Zs/ExtendedInformationExtraFieldTest.php b/vendor/maennchen/zipstream-php/test/Zs/ExtendedInformationExtraFieldTest.php new file mode 100644 index 000000000..2b8dbed4a --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/Zs/ExtendedInformationExtraFieldTest.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +namespace ZipStream\Test\Zs; + +use PHPUnit\Framework\TestCase; +use ZipStream\Zs\ExtendedInformationExtraField; + +class ExtendedInformationExtraFieldTest extends TestCase +{ + public function testSerializesCorrectly(): void + { + $extraField = ExtendedInformationExtraField::generate(); + + $this->assertSame( + bin2hex((string) $extraField), + '5356' . // 2 bytes; Tag for this "extra" block type + '0000' // 2 bytes; TODO: Document + ); + } +} diff --git a/vendor/maennchen/zipstream-php/test/bootstrap.php b/vendor/maennchen/zipstream-php/test/bootstrap.php new file mode 100644 index 000000000..13c7a0e6c --- /dev/null +++ b/vendor/maennchen/zipstream-php/test/bootstrap.php @@ -0,0 +1,7 @@ +<?php + +declare(strict_types=1); + +date_default_timezone_set('UTC'); + +require __DIR__ . '/../vendor/autoload.php'; |