zip_kit 6.4.1 → 6.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c9a2d7c76b813cfcbfb622fce1164f1566e42ef1487f8f44095d2464b76f282
4
- data.tar.gz: 45ef948824bebe50569949c030cbf2d2c720568e5c87e011ac124d4dccaccc55
3
+ metadata.gz: c94f6db106a2c4fb28f882cdbaf3411a2fb4142a3691f337fd3adbefc8cf566b
4
+ data.tar.gz: e5eef7ee71cb2a2b7c5a091399308f97ff69e9f64d350540de9fdf908e77da08
5
5
  SHA512:
6
- metadata.gz: 45d55f2c2038c0d19c3da829a113b44c5cea973a9f54b7118b6c21c0a75f6edf14d6d31e55ce27c6713b4e7d04cab4e752fb1fd62264b72440e88147c0d9fd7c
7
- data.tar.gz: 9740f7b322e6a6b32a3e1ff2c38e0b07fb20b92cdac2b30863c65bead2e04d05646d28c881edb993aee0b192009a38f30b04dd06ebb3174fc85bc31daf80f847
6
+ metadata.gz: 11236fe274f019b9f4c0a0c90814578ed61f074d911dc2c110289455507e6728c0b3e9bd8e9f820f06bfd3075cb1136d5bd6ea6812b0b3fa22ad9a674904555e
7
+ data.tar.gz: cdf1dfe461549d4df702a7b058e0fce429102edf1a1721df2c7ce769d4dfa67fc04e7d6cd8576427ded83e9529a818c1fa4104b5bab81a883882fb9565517829
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 6.4.2
4
+
5
+ * Write every ZIP record with a single `Array#pack` and a single `<<` to the destination, instead of one per field. Archives with lots of entries get written about 1.5x faster.
6
+ * Unpack the fixed fields of local and central directory headers in one go in `FileReader`, and read 4KB upfront for a local file header instead of the maximum possible 128KB. `FileReader.read_zip_structure` with local headers gets about 5x faster on archives with many entries, and parsing the central directory alone about 2x.
7
+ * Fix `ZipWriter` converting the given `mtime` to UTC in place. The local file header got the DOS time in local time and the central directory header got it in UTC, and the caller was left with a Time object in UTC.
8
+ * Remove the `C_CHAR` and `C_INT4` constants from `ZipWriter`, which were never meant to be public (the other packspec constants were private already).
9
+ * Add `"cache-control" => "no-transform"` to `OutputEnumerator.streaming_http_headers`, and add `no-transform` to the `Cache-Control` of the response in `zip_kit_stream` (keeping whatever the controller has set there). `Rack::Deflater` only leaves a response alone if it has `no-transform` or a content encoding other than `identity`, so the `content-encoding: identity` we were sending did not stop it - it gzipped the ZIP all the same, and with `sync: false` buffered all of it to do so. If you replace our `cache-control` with your own, keep `no-transform` in it.
10
+
3
11
  ## 6.4.1
4
12
 
5
13
  * Use lowercase names for the HTTP headers ZipKit produces, since Rack 3 requires them (`Rack::Lint` rejects uppercase header names). `OutputEnumerator.streaming_http_headers` now returns `"content-type"`, `"last-modified"`, `"content-encoding"` and `"x-accel-buffering"`. This is a breaking change if you look those headers up or override them in the returned Hash using the capitalised names: `headers["Content-Type"]` is now `nil`, and `headers["Content-Type"] = "application/epub+zip"` adds a second content type instead of replacing ours. Use the lowercase names instead. `zip_kit_stream` in Rails is not affected: it now merges our headers into the response regardless of case, so headers Rails sets itself (like the content type) still take precedence and do not get sent twice.
data/CODE_OF_CONDUCT.md CHANGED
@@ -1,4 +1,8 @@
1
- # Contributor Covenant Code of Conduct
1
+ # Code of Conduct
2
+
3
+ ## Relationship to CONTRIBUTING
4
+
5
+ This Code of Conduct works together with [CONTRIBUTING.md](CONTRIBUTING.md). Where the two differ, CONTRIBUTING.md takes precedence.
2
6
 
3
7
  ## Our Pledge
4
8
 
@@ -24,23 +28,23 @@ Examples of unacceptable behavior by participants include:
24
28
 
25
29
  ## Our Responsibilities
26
30
 
27
- Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
31
+ The maintainer is responsible for clarifying the standards of acceptable behavior and is expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28
32
 
29
- Project maintainers 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, or to ban temporarily or permanently any contributor for other behaviours that they deem inappropriate, threatening, offensive, or harmful.
33
+ The maintainer has 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, or to ban temporarily or permanently any contributor for other behaviours that the maintainer deems inappropriate, threatening, offensive, or harmful.
30
34
 
31
35
  ## Scope
32
36
 
33
- This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
37
+ This Code of Conduct applies within project spaces: the repository, its issues, pull requests and discussions, and any other channels the project runs.
34
38
 
35
- ## Enforcement
39
+ Conduct outside project spaces is not considered, either for or against anyone, as described in [What happens elsewhere stays elsewhere](CONTRIBUTING.md#what-happens-elsewhere-stays-elsewhere).
36
40
 
37
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at me@julik.nl. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
41
+ ## Enforcement
38
42
 
39
- Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
43
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the maintainer at me@julik.nl. The maintainer will review and investigate all complaints, and will respond in a way the maintainer deems appropriate to the circumstances. The maintainer is obligated to maintain confidentiality with regard to the reporter of an incident. Enforcement decisions are final.
40
44
 
41
45
  ## Attribution
42
46
 
43
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
47
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, with modifications, available at [http://contributor-covenant.org/version/1/4][version]
44
48
 
45
49
  [homepage]: http://contributor-covenant.org
46
50
  [version]: http://contributor-covenant.org/version/1/4/
data/CONTRIBUTING.md CHANGED
@@ -1,153 +1,89 @@
1
1
  # Contributing to zip_kit
2
2
 
3
- Please take a moment to review this document in order to make the contribution
4
- process easy and effective for everyone involved.
3
+ Contributions are welcome! Please take a moment to review this document so that
4
+ contributing stays easy and effective for everyone involved.
5
5
 
6
- Following these guidelines helps to communicate that you respect the time of
7
- the developers managing and developing this open source project. In return,
8
- they should reciprocate that respect in addressing your issue or assessing
9
- patches and features.
6
+ I'd especially love to get:
10
7
 
11
- ## What do I need to know to help?
8
+ * Reports of archives that a specific unarchiver refuses to open or extracts incorrectly
9
+ * Performance work
10
+ * Documentation fixes
12
11
 
13
- If you are already familiar with the [Ruby Programming Language](https://www.ruby-lang.org/) you can start contributing code right away, otherwise look for issues labeled with *documentation* or *good first issue* to get started.
12
+ ## What do I need to know to help?
14
13
 
15
- If you are interested in contributing code and would like to learn more about the technologies that we use, check out the (non-exhaustive) list below. You can also get in touch with us via an issue or email to get additional information.
14
+ If you would like to learn more about the technologies zip_kit builds on, check out the (non-exhaustive) list below. You can also get in touch with me via an issue or email.
16
15
 
17
16
  - [ruby](https://ruby-doc.org)
18
- - [rubyzip](https://github.com/rubyzip/rubyzip) as we like to test "our implementation against theirs"
19
- - [rspec](http://rspec.info/) for testing
20
- - [zip files](https://en.wikipedia.org/wiki/Zip_(file_format))
21
-
22
- # How do I make a contribution?
23
-
24
- ## Using the issue tracker
25
-
26
- The issue tracker is the preferred channel for [bug reports](#bug-reports),
27
- [feature requests](#feature-requests) and [submitting pull
28
- requests](#pull-requests), but please respect the following restrictions:
29
-
30
- * Please **do not** derail or troll issues. Keep the discussion on topic and respect the opinions of others. Adhere to the principles set out in the [Code of Conduct](https://github.com/WeTransfer/zip_kit/blob/main/CODE_OF_CONDUCT.md).
31
-
32
- ## Bug reports
33
-
34
- A bug is a _demonstrable problem_ that is caused by code in the repository.
35
-
36
- Good bug reports are extremely helpful-thank you!
17
+ - [ZIP files](https://en.wikipedia.org/wiki/Zip_(file_format)) and the [APPNOTE.TXT](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT) specification
18
+ - [rubyzip](https://github.com/rubyzip/rubyzip), as we like to test "our implementation against theirs"
19
+ - [rspec](https://rspec.info/) for testing
37
20
 
38
- Guidelines for bug reports:
21
+ ## Issues and pull requests
39
22
 
40
- 1. **Use the GitHub issue search** &mdash; check if the issue has already been
41
- reported.
23
+ * Opening an issue first is often the fastest way to get something in. I can shape it into something that fits the library. Please do this before any significant PR (new features, refactoring), so you don't spend time on something that won't be merged.
24
+ * Search the issues first, and check whether the problem still happens on the latest `main`.
25
+ * The most useful bug report is a small script that produces the broken archive (or the archive itself), plus the name, version and OS of the tool that fails to open it.
26
+ * I may close a PR and implement the idea differently. That's not a rejection of the idea, and you'll be credited (`Co-authored-by`).
27
+ * Feel free to propose opinionated changes, but be prepared that your opinion won't be shared. It is a give-and-take.
28
+ * Keep PRs focused, without unrelated commits.
42
29
 
43
- 2. **Check if the issue has been fixed** &mdash; try to reproduce it using the
44
- latest `main` branch in the repository.
30
+ ## Code changes
45
31
 
46
- 3. **Isolate the problem** &mdash; create a [reduced test
47
- case](http://css-tricks.com/reduced-test-cases/) and a live example.
48
-
49
- A good bug report shouldn't leave others needing to chase you up for more
50
- information. Please try to be as detailed as possible in your report. What is
51
- your environment? What steps will reproduce the issue? What tool(s) or OS will
52
- experience the problem? What would you expect to be the outcome? All these
53
- details will help people to fix any potential bugs.
54
-
55
- Example:
56
-
57
- > Short and descriptive example bug report title
58
- >
59
- > A summary of the issue and the OS environment in which it occurs. If
60
- > suitable, include the steps required to reproduce the bug.
61
- >
62
- > 1. This is the first step
63
- > 2. This is the second step
64
- > 3. Further steps, etc.
65
- >
66
- > `<url>` - a link to the reduced test case, if possible. Feel free to use a [Gist](https://gist.github.com).
67
- >
68
- > Any other information you want to share that is relevant to the issue being
69
- > reported. This might include the lines of code that you have identified as
70
- > causing the bug, and potential solutions (and your opinions on their
71
- > merits).
72
-
73
-
74
- ## Feature requests
75
-
76
- Feature requests are welcome. But take a moment to find out whether your idea
77
- fits with the scope and aims of the project. It's up to *you* to make a strong
78
- case to convince the project's developers of the merits of this feature. Please
79
- provide as much detail and context as possible.
32
+ Please adhere to the coding conventions used throughout the project (indentation,
33
+ accurate and terse comments, etc.) and any other requirements (such as test coverage).
80
34
 
35
+ Run the `rake` default task before submitting. It runs the test suite (`bundle exec rspec`),
36
+ checks formatting with [standard](https://github.com/standardrb/standard) (`bundle exec rake format`
37
+ fixes most of it), and regenerates the type signatures in `rbi/`. Commit the regenerated signatures.
81
38
 
82
- ## Pull requests
39
+ zip_kit supports Ruby 2.6 and up, and CI runs the tests and the linter on 2.6. Don't use syntax or
40
+ stdlib features newer than that. CI also runs the suite with frozen string literals enforced, so don't
41
+ mutate string literals.
83
42
 
84
- Good pull requests-patches, improvements, new features-are a fantastic
85
- help. They should remain focused in scope and avoid containing unrelated
86
- commits.
43
+ ## Basic expectations
87
44
 
88
- **Please ask first** before embarking on any significant pull request (e.g.
89
- implementing features, refactoring code, porting to a different language),
90
- otherwise you risk spending a lot of time working on something that the
91
- project's developers might not want to merge into the project.
45
+ This project is something created with love, and I - as an author - expect you, as a contributor, to return the favor. The ground rules:
92
46
 
93
- Please adhere to the coding conventions used throughout the project (indentation,
94
- accurate comments, etc.) and any other requirements (such as test coverage).
47
+ * Do your best to be kind.
48
+ * Be polite and direct. No abrasiveness, no passive-aggression.
49
+ * [Low-context](https://www.ebsco.com/research-starters/communication-and-mass-media/high-context-and-low-context-cultures) communication is preferred and permitted
50
+ * No ad-hominems, also in code comments
51
+ * No sexualized or aggressive language
95
52
 
96
- The project uses Rubocop which can be run using `bundle exec rubocop`. The test
97
- suite can be run with `bundle exec rspec`. You are also encouraged to use the
98
- script in the `testing` directory to create test files that you can then verify
99
- with various zip/unzip utilities. Further instructions are [here](https://github.com/WeTransfer/zip_kit/blob/main/testing/README_TESTING.md).
53
+ These rules sit alongside the [Code of Conduct](CODE_OF_CONDUCT.md), which applies to everyone taking part in the project.
100
54
 
101
- Follow this process if you'd like your work considered for inclusion in the
102
- project:
55
+ As a maintainer, I reserve the right to deny any contribution, discussion post, comment or pull request I find inappropriate or that is not aligned with the Code of Conduct.
103
56
 
104
- 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork,
105
- and configure the remotes:
57
+ Similarly, I reserve the right to not accept any contribution that doesn't fit the goals or spirit of the project (or my own), without explaining why it has been refused.
106
58
 
107
- ```bash
108
- # Clone your fork of the repo into the current directory
109
- git clone git@github.com:julik/zip_kit.git
110
- # Navigate to the newly cloned directory
111
- cd zip_kit
112
- # Assign the original repo to a remote called "upstream"
113
- git remote add upstream git@github.com:julik/zip_kit.git
114
- ```
59
+ ## Decisions are final
115
60
 
116
- 2. If you cloned a while ago, get the latest changes from upstream:
61
+ You're welcome to make your case once, clearly. After that, please don't reopen closed issues, start meta-threads about the decision, or argue about the wording of this document. This is a list of expectations, not a contract to be lawyered. If something isn't covered, the maintainer's judgment applies.
117
62
 
118
- ```bash
119
- git checkout <dev-branch>
120
- git pull upstream <dev-branch>
121
- ```
63
+ This applies to Code of Conduct enforcement too. Reporting a violation is always welcome, privately, at the address given in the [Code of Conduct](CODE_OF_CONDUCT.md#enforcement), but the outcome is final.
122
64
 
123
- 3. Create a new topic branch (off the main project development branch) to
124
- contain your feature, change, or fix:
65
+ ## What happens elsewhere stays elsewhere
125
66
 
126
- ```bash
127
- git checkout -b <topic-branch-name>
128
- ```
67
+ Contributions are judged on what they are, not on who sent them. What anyone says or does outside this project (on social media, at their job, in other repos, in politics) is not a topic here and will not be considered, either for or against them. Please don't bring it in: no call-outs, no "are you aware this person…" threads, no requests to ban someone over things that didn't happen here.
129
68
 
130
- 4. Commit your changes in logical chunks and/or squash them for readability and
131
- conciseness. Check out [this post](https://chris.beams.io/posts/git-commit/) or
132
- [this other post](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) for some tips re: writing good commit messages.
69
+ The [Code of Conduct](CODE_OF_CONDUCT.md#scope) applies within project spaces only, and defers to this document where the two differ.
133
70
 
134
- 5. Locally merge (or rebase) the upstream development branch into your topic branch:
71
+ ## AI-generated issues and code
135
72
 
136
- ```bash
137
- git pull [--rebase] upstream <dev-branch>
138
- ```
73
+ This project uses LLMs to produce code and artifacts, and it's fine if you do the same when contributing. LLM use in this project is settled: threads about whether it should be used (utility, harm, societal implications etc.) will be closed.
139
74
 
140
- 6. Push your topic branch up to your fork:
75
+ If you require LLM-untainted code for your needs, feel free to use zip_kit prior to and including commit 2f123ae41cffe7255f1ff0824af44420d5dbfaa7 - but I am not going to maintain an "LLM-free" branch of zip_kit just for your needs.
76
+
77
+ Please disclose LLM use in every issue and PR: say which model or tool you used and what for (e.g. "Claude wrote the fix, I wrote the test"). This is for transparency, not for judgment. Disclosed LLM use is treated the same as no LLM use.
141
78
 
142
- ```bash
143
- git push origin <topic-branch-name>
144
- ```
79
+ However, the following is on you:
145
80
 
146
- Make sure to run `bundle exec appraisal rspec` to make sure your changes are compatible with all the Rails versions we support.
81
+ * Make your LLM output _terse._ Nobody likes reading slop, even my agents. Make it get to the point.
82
+ * Make sure what you submit actually ran. Don't file issues about behavior or APIs your LLM imagined.
83
+ * zip_kit has no runtime dependencies. Do not add any (or have your LLM do it) unless absolutely necessary
84
+ * If you file a PR, make at least one pass with "Make this at least 50% less code"
85
+ * Code generated by your LLM will be considered as written by you, with the same requirements
147
86
 
148
- 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/)
149
- with a clear title and description.
87
+ ## License
150
88
 
151
- **IMPORTANT**: By submitting a patch, you agree to allow the project owner to
152
- license your work under the same license as that used by the project, which you
153
- can see by clicking [here](LICENSE.txt).
89
+ By submitting a patch, you agree to allow the project owner to license your work under the same license as that used by the project, which you can see [here](LICENSE.txt).
@@ -291,9 +291,8 @@ class ZipKit::FileReader
291
291
 
292
292
  # Parse the local header entry and get the offset in the IO at which the
293
293
  # actual compressed data of the file starts within the ZIP.
294
- # The method will eager-read the entire local header for the file
295
- # (the maximum size the local header may use), starting at the given offset,
296
- # and will then compute its size. That size plus the local header offset
294
+ # The method will read the local header for the file, starting at the
295
+ # given offset, and will then compute its size. That size plus the local header offset
297
296
  # given will be the compressed data offset of the entry (read starting at
298
297
  # this offset to get the data).
299
298
  #
@@ -303,28 +302,43 @@ class ZipKit::FileReader
303
302
  def read_local_file_header(io:)
304
303
  local_file_header_offset = io.tell
305
304
 
306
- # Reading in bulk is cheaper - grab the maximum length of the local header,
307
- # including any headroom for extra fields etc.
308
- local_file_header_str_plus_headroom = io.read(MAX_LOCAL_HEADER_SIZE)
309
- raise ReadError if local_file_header_str_plus_headroom.nil? # reached EOF
305
+ # Reading in bulk is cheaper - grab enough for most local headers, including
306
+ # some headroom for the filename and extra fields. Reading the maximum possible
307
+ # size (about 128KB) for every entry costs more than the parsing itself.
308
+ header = io.read(LOCAL_HEADER_READ_SIZE)
309
+ if header.nil? || header.bytesize < SIZE_OF_LOCAL_HEADER_FIXED_FIELDS
310
+ raise ReadError, "Expected to read at least #{SIZE_OF_LOCAL_HEADER_FIXED_FIELDS} bytes of the local file header, but the IO was at the end"
311
+ end
310
312
 
311
- io_starting_at_local_header = StringIO.new(local_file_header_str_plus_headroom)
313
+ signature,
314
+ version_needed_to_extract,
315
+ gp_flags,
316
+ storage_mode,
317
+ dos_time,
318
+ dos_date,
319
+ crc32,
320
+ compressed_size,
321
+ uncompressed_size,
322
+ filename_size,
323
+ extra_size = header.unpack(LOCAL_HEADER_FIXED_FIELDS_UNPACKSPEC)
324
+ assert_signature_value(signature, 0x04034b50)
325
+
326
+ # ...and if the filename and extras did not fit, read the rest
327
+ size_of_header = SIZE_OF_LOCAL_HEADER_FIXED_FIELDS + filename_size + extra_size
328
+ header << read_n(io, size_of_header - header.bytesize) if header.bytesize < size_of_header
312
329
 
313
- assert_signature(io_starting_at_local_header, 0x04034b50)
314
330
  e = ZipEntry.new
315
- e.version_needed_to_extract = read_2b(io_starting_at_local_header) # Version needed to extract
316
- e.gp_flags = read_2b(io_starting_at_local_header) # gp flags
317
- e.storage_mode = read_2b(io_starting_at_local_header) # storage mode
318
- e.dos_time = read_2b(io_starting_at_local_header) # dos time
319
- e.dos_date = read_2b(io_starting_at_local_header) # dos date
320
- e.crc32 = read_4b(io_starting_at_local_header) # CRC32
321
- e.compressed_size = read_4b(io_starting_at_local_header) # Comp size
322
- e.uncompressed_size = read_4b(io_starting_at_local_header) # Uncomp size
323
-
324
- filename_size = read_2b(io_starting_at_local_header)
325
- extra_size = read_2b(io_starting_at_local_header)
326
- e.filename = read_n(io_starting_at_local_header, filename_size)
327
- extra_fields_str = read_n(io_starting_at_local_header, extra_size)
331
+ e.version_needed_to_extract = version_needed_to_extract
332
+ e.gp_flags = gp_flags
333
+ e.storage_mode = storage_mode
334
+ e.dos_time = dos_time
335
+ e.dos_date = dos_date
336
+ e.crc32 = crc32
337
+ e.compressed_size = compressed_size
338
+ e.uncompressed_size = uncompressed_size
339
+
340
+ e.filename = header.byteslice(SIZE_OF_LOCAL_HEADER_FIXED_FIELDS, filename_size)
341
+ extra_fields_str = header.byteslice(SIZE_OF_LOCAL_HEADER_FIXED_FIELDS + filename_size, extra_size)
328
342
 
329
343
  # Parse out the extra fields
330
344
  extra_table = parse_out_extra_fields(extra_fields_str)
@@ -351,16 +365,14 @@ class ZipKit::FileReader
351
365
  e.compressed_size = read_8b(zip64_extra) if e.compressed_size == 0xFFFFFFFF
352
366
  end
353
367
 
354
- offset = local_file_header_offset + io_starting_at_local_header.tell
355
- e.compressed_data_offset = offset
368
+ e.compressed_data_offset = local_file_header_offset + size_of_header
356
369
 
357
370
  e
358
371
  end
359
372
 
360
373
  # Get the offset in the IO at which the actual compressed data of the file
361
- # starts within the ZIP. The method will eager-read the entire local header
362
- # for the file (the maximum size the local header may use), starting at the
363
- # given offset, and will then compute its size. That size plus the local
374
+ # starts within the ZIP. The method will read the local header for the file,
375
+ # starting at the given offset, and will then compute its size. That size plus the local
364
376
  # header offset given will be the compressed data offset of the entry
365
377
  # (read starting at this offset to get the data).
366
378
  #
@@ -431,7 +443,10 @@ class ZipKit::FileReader
431
443
  end
432
444
 
433
445
  def assert_signature(io, signature_magic_number)
434
- readback = read_4b(io)
446
+ assert_signature_value(read_4b(io), signature_magic_number)
447
+ end
448
+
449
+ def assert_signature_value(readback, signature_magic_number)
435
450
  if readback != signature_magic_number
436
451
  expected = "0x0" + signature_magic_number.to_s(16)
437
452
  actual = "0x0" + readback.to_s(16)
@@ -475,24 +490,39 @@ class ZipKit::FileReader
475
490
 
476
491
  def read_cdir_entry(io)
477
492
  # read_cdir_entry is too high. [45.66/15]
478
- assert_signature(io, 0x02014b50)
493
+ signature,
494
+ made_by,
495
+ version_needed_to_extract,
496
+ gp_flags,
497
+ storage_mode,
498
+ dos_time,
499
+ dos_date,
500
+ crc32,
501
+ compressed_size,
502
+ uncompressed_size,
503
+ filename_size,
504
+ extra_size,
505
+ comment_len,
506
+ disk_number_start,
507
+ internal_attrs,
508
+ external_attrs,
509
+ local_file_header_offset = read_n(io, SIZE_OF_CDIR_ENTRY_FIXED_FIELDS).unpack(CDIR_ENTRY_FIXED_FIELDS_UNPACKSPEC)
510
+ assert_signature_value(signature, 0x02014b50)
511
+
479
512
  ZipEntry.new.tap do |e|
480
- e.made_by = read_2b(io)
481
- e.version_needed_to_extract = read_2b(io)
482
- e.gp_flags = read_2b(io)
483
- e.storage_mode = read_2b(io)
484
- e.dos_time = read_2b(io)
485
- e.dos_date = read_2b(io)
486
- e.crc32 = read_4b(io)
487
- e.compressed_size = read_4b(io)
488
- e.uncompressed_size = read_4b(io)
489
- filename_size = read_2b(io)
490
- extra_size = read_2b(io)
491
- comment_len = read_2b(io)
492
- e.disk_number_start = read_2b(io)
493
- e.internal_attrs = read_2b(io)
494
- e.external_attrs = read_4b(io)
495
- e.local_file_header_offset = read_4b(io)
513
+ e.made_by = made_by
514
+ e.version_needed_to_extract = version_needed_to_extract
515
+ e.gp_flags = gp_flags
516
+ e.storage_mode = storage_mode
517
+ e.dos_time = dos_time
518
+ e.dos_date = dos_date
519
+ e.crc32 = crc32
520
+ e.compressed_size = compressed_size
521
+ e.uncompressed_size = uncompressed_size
522
+ e.disk_number_start = disk_number_start
523
+ e.internal_attrs = internal_attrs
524
+ e.external_attrs = external_attrs
525
+ e.local_file_header_offset = local_file_header_offset
496
526
  e.filename = read_n(io, filename_size)
497
527
 
498
528
  # Extra fields
@@ -660,6 +690,16 @@ class ZipKit::FileReader
660
690
  C_UINT2 = "v"
661
691
  C_UINT8 = "Q<"
662
692
 
693
+ # The fixed-size fields get unpacked in one go, instead of one `read` and `unpack` per field.
694
+ # Signature, version needed, gp flags, storage mode, DOS time and date, CRC32, both sizes and
695
+ # the filename and extra field lengths
696
+ LOCAL_HEADER_FIXED_FIELDS_UNPACKSPEC = "VvvvvvVVVvv"
697
+ SIZE_OF_LOCAL_HEADER_FIXED_FIELDS = 30
698
+ # Same as the local header, but with "made by" after the signature and the comment length,
699
+ # disk number, internal and external attributes and the local header offset at the end
700
+ CDIR_ENTRY_FIXED_FIELDS_UNPACKSPEC = "VvvvvvvVVVvvvvvVV"
701
+ SIZE_OF_CDIR_ENTRY_FIXED_FIELDS = 46
702
+
663
703
  # To prevent too many tiny reads, read the maximum possible size of end of
664
704
  # central directory record upfront (all the fixed fields + at most 0xFFFF
665
705
  # bytes of the archive comment)
@@ -673,22 +713,9 @@ class ZipKit::FileReader
673
713
  2 + # The comment size
674
714
  0xFFFF # Maximum comment size
675
715
 
676
- # To prevent too many tiny reads, read the maximum possible size of the local file header upfront.
677
- # The maximum size is all the usual items, plus the maximum size
678
- # of the filename (0xFFFF bytes) and the maximum size of the extras (0xFFFF bytes)
679
- MAX_LOCAL_HEADER_SIZE = 4 + # signature
680
- 2 + # Version needed to extract
681
- 2 + # gp flags
682
- 2 + # storage mode
683
- 2 + # dos time
684
- 2 + # dos date
685
- 4 + # CRC32
686
- 4 + # Comp size
687
- 4 + # Uncomp size
688
- 2 + # Filename size
689
- 2 + # Extra fields size
690
- 0xFFFF + # Maximum filename size
691
- 0xFFFF # Maximum extra fields size
716
+ # How much to read upfront for a local file header. Filenames and extra fields rarely
717
+ # take more than a few hundred bytes, the rest gets read separately if needed
718
+ LOCAL_HEADER_READ_SIZE = 4 * 1024
692
719
 
693
720
  SIZE_OF_USABLE_EOCD_RECORD = 4 + # Signature
694
721
  2 + # Number of this disk
@@ -716,7 +743,9 @@ class ZipKit::FileReader
716
743
  end
717
744
 
718
745
  private_constant :C_UINT4, :C_UINT2, :C_UINT8, :MAX_END_OF_CENTRAL_DIRECTORY_RECORD_SIZE,
719
- :MAX_LOCAL_HEADER_SIZE, :SIZE_OF_USABLE_EOCD_RECORD
746
+ :LOCAL_HEADER_FIXED_FIELDS_UNPACKSPEC, :SIZE_OF_LOCAL_HEADER_FIXED_FIELDS,
747
+ :CDIR_ENTRY_FIXED_FIELDS_UNPACKSPEC, :SIZE_OF_CDIR_ENTRY_FIXED_FIELDS,
748
+ :LOCAL_HEADER_READ_SIZE, :SIZE_OF_USABLE_EOCD_RECORD
720
749
 
721
750
  # Is provided as a stub to be overridden in a subclass if you need it. Will report
722
751
  # during various stages of reading. The log message is contained in the return value
@@ -116,9 +116,12 @@ class ZipKit::OutputEnumerator
116
116
  #
117
117
  # * `last-modified` will be set to "now" so that the response is considered "fresh" by `Rack::ETag`. This is done so that `Rack::ETag` won't try to
118
118
  # calculate a lax ETag value and thus won't start buffering your response out of nowhere
119
- # * `content-encoding` will be set to `identity`. This is so that proxies or the Rack middleware that applies compression to the response (like gzip)
120
- # is not going to try to compress your response. It also tells the receiving browsers (or downstream proxies) that they should not attempt to
121
- # open or uncompress the response before saving it or passing it onwards.
119
+ # * `cache-control` will be set to `no-transform`. This is so that `Rack::Deflater` and proxies which compress responses (like gzip)
120
+ # are not going to try to compress your response again (and, with `Rack::Deflater` set to `sync: false`, buffer all of it to do so).
121
+ # It says nothing about caching, so add your own directives to it if you need them, and keep `no-transform` in there
122
+ # * `content-encoding` will be set to `identity`. This tells the receiving browsers (or downstream proxies) that they should not attempt to
123
+ # open or uncompress the response before saving it or passing it onwards. Note that it does _not_ stop `Rack::Deflater`, which compresses
124
+ # responses with `content-encoding: identity` just the same
122
125
  # * `x-accel-buffering` will be set to `no` - this tells both nginx and the Google Cloud load balancer that the response should not be buffered
123
126
  #
124
127
  # These header values are known to get as close as possible to guaranteeing streaming on most environments where Ruby web applications may be hosted.
@@ -134,8 +137,9 @@ class ZipKit::OutputEnumerator
134
137
  # Set this even when not streaming for consistency. The fact that there would be
135
138
  # a weak ETag generated would mean that the middleware buffers, so we have tests for that.
136
139
  "last-modified" => Time.now.httpdate,
137
- # Make sure Rack::Deflater does not touch our response body either, see
138
- # https://github.com/felixbuenemann/xlsxtream/issues/14#issuecomment-529569548
140
+ # Make sure Rack::Deflater does not touch our response body either. It only skips
141
+ # responses which have `no-transform` or a content-encoding other than `identity`
142
+ "cache-control" => "no-transform",
139
143
  "content-encoding" => "identity",
140
144
  # Disable buffering for both nginx and Google Load Balancer, see
141
145
  # https://cloud.google.com/appengine/docs/flexible/how-requests-are-handled?tab=python#x-accel-buffering
@@ -45,9 +45,16 @@ module ZipKit::RailsStreaming
45
45
  # names would send some headers twice
46
46
  present_header_names = response.headers.keys.map(&:downcase)
47
47
  ZipKit::OutputEnumerator.streaming_http_headers.each_pair do |name, value|
48
+ next if name == "cache-control"
48
49
  response.headers[name] = value unless present_header_names.include?(name)
49
50
  end
50
51
 
52
+ # Rails rewrites Cache-Control when the response gets committed, and before 7.1 it only
53
+ # looks for it under that name - a lowercase one would go out next to its own. no-transform
54
+ # is what keeps Rack::Deflater away, so it goes on top of whatever the controller has set
55
+ cache_control = response.headers["Cache-Control"].to_s.split(/,\s*/)
56
+ response.headers["Cache-Control"] = (cache_control | ["no-transform"]).join(", ")
57
+
51
58
  # The output enumerator yields chunks of bytes generated from the Streamer,
52
59
  # with some buffering. See OutputEnumerator docs for more.
53
60
  rack_zip_body = ZipKit::OutputEnumerator.new(**output_enumerator_options, &zip_streaming_blk)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ZipKit
4
- VERSION = "6.4.1"
4
+ VERSION = "6.4.2"
5
5
  end