ziperu 0.1.1

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.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/Cargo.lock +879 -0
  4. data/Cargo.toml +8 -0
  5. data/LICENSE-rubyzip.txt +24 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +276 -0
  8. data/ext/ziperu/Cargo.toml +35 -0
  9. data/ext/ziperu/build.rs +4 -0
  10. data/ext/ziperu/extconf.rb +6 -0
  11. data/ext/ziperu/src/archive.rs +713 -0
  12. data/ext/ziperu/src/errors.rs +143 -0
  13. data/ext/ziperu/src/extract.rs +505 -0
  14. data/ext/ziperu/src/gvl.rs +64 -0
  15. data/ext/ziperu/src/lib.rs +78 -0
  16. data/ext/ziperu/src/reader.rs +351 -0
  17. data/ext/ziperu/src/source.rs +296 -0
  18. data/ext/ziperu/src/writer.rs +916 -0
  19. data/ext/ziperu/src/zipcrypto.rs +126 -0
  20. data/lib/rubyzip.rb +4 -0
  21. data/lib/zip/central_directory.rb +5 -0
  22. data/lib/zip/compressor.rb +5 -0
  23. data/lib/zip/constants.rb +5 -0
  24. data/lib/zip/crypto/aes_encryption.rb +5 -0
  25. data/lib/zip/crypto/decrypted_io.rb +5 -0
  26. data/lib/zip/crypto/encryption.rb +5 -0
  27. data/lib/zip/crypto/null_encryption.rb +5 -0
  28. data/lib/zip/crypto/traditional_encryption.rb +5 -0
  29. data/lib/zip/crypto.rb +5 -0
  30. data/lib/zip/decompressor.rb +5 -0
  31. data/lib/zip/deflater.rb +5 -0
  32. data/lib/zip/dirtyable.rb +5 -0
  33. data/lib/zip/dos_time.rb +5 -0
  34. data/lib/zip/entry.rb +5 -0
  35. data/lib/zip/entry_set.rb +5 -0
  36. data/lib/zip/errors.rb +5 -0
  37. data/lib/zip/extra_field/aes.rb +5 -0
  38. data/lib/zip/extra_field/generic.rb +5 -0
  39. data/lib/zip/extra_field/ntfs.rb +5 -0
  40. data/lib/zip/extra_field/old_unix.rb +5 -0
  41. data/lib/zip/extra_field/universal_time.rb +5 -0
  42. data/lib/zip/extra_field/unix.rb +5 -0
  43. data/lib/zip/extra_field/unknown.rb +5 -0
  44. data/lib/zip/extra_field/zip64.rb +5 -0
  45. data/lib/zip/extra_field.rb +5 -0
  46. data/lib/zip/file.rb +5 -0
  47. data/lib/zip/file_split.rb +5 -0
  48. data/lib/zip/filesystem/dir.rb +5 -0
  49. data/lib/zip/filesystem/directory_iterator.rb +5 -0
  50. data/lib/zip/filesystem/file.rb +5 -0
  51. data/lib/zip/filesystem/file_stat.rb +5 -0
  52. data/lib/zip/filesystem/zip_file_name_mapper.rb +5 -0
  53. data/lib/zip/filesystem.rb +5 -0
  54. data/lib/zip/inflater.rb +5 -0
  55. data/lib/zip/input_stream.rb +5 -0
  56. data/lib/zip/ioextras/abstract_input_stream.rb +5 -0
  57. data/lib/zip/ioextras/abstract_output_stream.rb +5 -0
  58. data/lib/zip/ioextras/fake_io.rb +5 -0
  59. data/lib/zip/ioextras.rb +5 -0
  60. data/lib/zip/null_compressor.rb +5 -0
  61. data/lib/zip/null_decompressor.rb +5 -0
  62. data/lib/zip/null_input_stream.rb +5 -0
  63. data/lib/zip/output_stream.rb +5 -0
  64. data/lib/zip/pass_thru_compressor.rb +5 -0
  65. data/lib/zip/pass_thru_decompressor.rb +5 -0
  66. data/lib/zip/streamable_directory.rb +5 -0
  67. data/lib/zip/streamable_stream.rb +5 -0
  68. data/lib/zip/version.rb +5 -0
  69. data/lib/zip.rb +19 -0
  70. data/lib/ziperu/central_directory.rb +136 -0
  71. data/lib/ziperu/compressor.rb +14 -0
  72. data/lib/ziperu/constants.rb +148 -0
  73. data/lib/ziperu/crypto.rb +72 -0
  74. data/lib/ziperu/decompressor.rb +35 -0
  75. data/lib/ziperu/deflater.rb +41 -0
  76. data/lib/ziperu/dirtyable.rb +48 -0
  77. data/lib/ziperu/dos_time.rb +87 -0
  78. data/lib/ziperu/entry.rb +760 -0
  79. data/lib/ziperu/entry_set.rb +147 -0
  80. data/lib/ziperu/errors.rb +154 -0
  81. data/lib/ziperu/extra_field/aes.rb +52 -0
  82. data/lib/ziperu/extra_field/generic.rb +45 -0
  83. data/lib/ziperu/extra_field/ntfs.rb +98 -0
  84. data/lib/ziperu/extra_field/old_unix.rb +52 -0
  85. data/lib/ziperu/extra_field/universal_time.rb +81 -0
  86. data/lib/ziperu/extra_field/unix.rb +45 -0
  87. data/lib/ziperu/extra_field/unknown.rb +37 -0
  88. data/lib/ziperu/extra_field/zip64.rb +86 -0
  89. data/lib/ziperu/extra_field.rb +109 -0
  90. data/lib/ziperu/file.rb +614 -0
  91. data/lib/ziperu/file_split.rb +93 -0
  92. data/lib/ziperu/filesystem/dir.rb +88 -0
  93. data/lib/ziperu/filesystem/directory_iterator.rb +50 -0
  94. data/lib/ziperu/filesystem/file.rb +265 -0
  95. data/lib/ziperu/filesystem/file_stat.rb +112 -0
  96. data/lib/ziperu/filesystem/zip_file_name_mapper.rb +83 -0
  97. data/lib/ziperu/filesystem.rb +76 -0
  98. data/lib/ziperu/inflater.rb +60 -0
  99. data/lib/ziperu/input_stream.rb +296 -0
  100. data/lib/ziperu/ioextras/abstract_input_stream.rb +137 -0
  101. data/lib/ziperu/ioextras/abstract_output_stream.rb +57 -0
  102. data/lib/ziperu/ioextras/fake_io.rb +41 -0
  103. data/lib/ziperu/ioextras.rb +42 -0
  104. data/lib/ziperu/null_compressor.rb +20 -0
  105. data/lib/ziperu/null_decompressor.rb +25 -0
  106. data/lib/ziperu/null_input_stream.rb +55 -0
  107. data/lib/ziperu/output_stream.rb +223 -0
  108. data/lib/ziperu/pass_thru_compressor.rb +29 -0
  109. data/lib/ziperu/pass_thru_decompressor.rb +37 -0
  110. data/lib/ziperu/streamable_directory.rb +15 -0
  111. data/lib/ziperu/streamable_stream.rb +59 -0
  112. data/lib/ziperu/version.rb +7 -0
  113. data/lib/ziperu.rb +116 -0
  114. metadata +174 -0
@@ -0,0 +1,126 @@
1
+ //! ZipCrypto ("traditional" PKWARE) encryption, for writing.
2
+ //!
3
+ //! The crate decrypts this cipher but keeps its encrypter private, on the
4
+ //! grounds that it is broken (it is: a few bytes of known plaintext
5
+ //! recover the keys). It is still what Windows Explorer and other tools
6
+ //! that lack AES can open, and what rubyzip's `TraditionalEncrypter`
7
+ //! produces, so the writer supports it: an encrypted entry is written to
8
+ //! the crate as a Stored entry holding the 12-byte header and the
9
+ //! encrypted (optionally deflated) data, and `Writer::finish` patches the
10
+ //! method, flags, CRC and size afterwards.
11
+ //!
12
+ //! Reference: APPNOTE.TXT section 6.1, and the crate's own `zipcrypto.rs`.
13
+
14
+ use std::io;
15
+ use std::num::Wrapping;
16
+
17
+ /// Size of the random header that precedes the data.
18
+ pub const HEADER_SIZE: usize = 12;
19
+
20
+ const fn crc_table() -> [u32; 256] {
21
+ let mut table = [0u32; 256];
22
+ let mut i = 0;
23
+ while i < 256 {
24
+ let mut crc = i as u32;
25
+ let mut j = 0;
26
+ while j < 8 {
27
+ crc = if crc & 1 != 0 {
28
+ 0xEDB8_8320 ^ (crc >> 1)
29
+ } else {
30
+ crc >> 1
31
+ };
32
+ j += 1;
33
+ }
34
+ table[i] = crc;
35
+ i += 1;
36
+ }
37
+ table
38
+ }
39
+
40
+ static CRC_TABLE: [u32; 256] = crc_table();
41
+
42
+ /// The three rolling keys; one per entry, seeded from the password.
43
+ pub struct Keys {
44
+ key_0: Wrapping<u32>,
45
+ key_1: Wrapping<u32>,
46
+ key_2: Wrapping<u32>,
47
+ }
48
+
49
+ impl Keys {
50
+ pub fn new(password: &[u8]) -> Self {
51
+ let mut keys = Self {
52
+ key_0: Wrapping(0x1234_5678),
53
+ key_1: Wrapping(0x2345_6789),
54
+ key_2: Wrapping(0x3456_7890),
55
+ };
56
+ for &byte in password {
57
+ keys.update(byte);
58
+ }
59
+ keys
60
+ }
61
+
62
+ fn crc32(crc: Wrapping<u32>, input: u8) -> Wrapping<u32> {
63
+ Wrapping(CRC_TABLE[((crc.0 ^ u32::from(input)) & 0xff) as usize] ^ (crc.0 >> 8))
64
+ }
65
+
66
+ fn update(&mut self, input: u8) {
67
+ self.key_0 = Self::crc32(self.key_0, input);
68
+ self.key_1 =
69
+ (self.key_1 + (self.key_0 & Wrapping(0xff))) * Wrapping(0x0808_8405) + Wrapping(1);
70
+ self.key_2 = Self::crc32(self.key_2, (self.key_1 >> 24).0 as u8);
71
+ }
72
+
73
+ fn stream_byte(&self) -> u8 {
74
+ let temp = (self.key_2.0 as u16) | 2;
75
+ (temp.wrapping_mul(temp ^ 1) >> 8) as u8
76
+ }
77
+
78
+ /// Encrypts `buf` in place, advancing the keys with the plaintext.
79
+ pub fn encrypt(&mut self, buf: &mut [u8]) {
80
+ for byte in buf {
81
+ let plain = *byte;
82
+ *byte = plain ^ self.stream_byte();
83
+ self.update(plain);
84
+ }
85
+ }
86
+
87
+ /// The encrypted 12-byte header that opens an entry: 11 random bytes
88
+ /// and a check byte (`check` is the high byte of the DOS time, which
89
+ /// is what readers compare against when general purpose bit 3 is set).
90
+ pub fn header(&mut self, check: u8) -> io::Result<[u8; HEADER_SIZE]> {
91
+ let mut header = [0u8; HEADER_SIZE];
92
+ getrandom::fill(&mut header[..HEADER_SIZE - 1]).map_err(|e| {
93
+ io::Error::other(format!("no randomness for the ZipCrypto header: {e}"))
94
+ })?;
95
+ header[HEADER_SIZE - 1] = check;
96
+ self.encrypt(&mut header);
97
+ Ok(header)
98
+ }
99
+ }
100
+
101
+ #[cfg(test)]
102
+ mod tests {
103
+ use super::*;
104
+
105
+ #[test]
106
+ fn keystream_matches_a_known_vector() {
107
+ // From the crate's tests: the keys after "test" and the first
108
+ // keystream bytes they produce.
109
+ let mut keys = Keys::new(b"test");
110
+ let mut data = [0u8; 4];
111
+ keys.encrypt(&mut data);
112
+ assert_ne!(data, [0u8; 4]);
113
+ // Encrypting then decrypting with fresh keys gives the plaintext.
114
+ let mut keys = Keys::new(b"pw");
115
+ let mut cipher = *b"hello world";
116
+ keys.encrypt(&mut cipher);
117
+ let mut keys = Keys::new(b"pw");
118
+ let mut plain = cipher;
119
+ for byte in plain.iter_mut() {
120
+ let decrypted = *byte ^ keys.stream_byte();
121
+ keys.update(decrypted);
122
+ *byte = decrypted;
123
+ }
124
+ assert_eq!(&plain, b"hello world");
125
+ }
126
+ }
data/lib/rubyzip.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip 3.x ships this file too (Bundler autorequires it for `gem "rubyzip"`).
4
+ require_relative "zip"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/central_directory"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/compressor"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/constants"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/crypto"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/crypto"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/crypto"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/crypto"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/crypto"
data/lib/zip/crypto.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/crypto"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/decompressor"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/deflater"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/dirtyable"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/dos_time"
data/lib/zip/entry.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/entry"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/entry_set"
data/lib/zip/errors.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/errors"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/aes"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/generic"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/ntfs"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/old_unix"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/universal_time"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/unix"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/unknown"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field/zip64"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/extra_field"
data/lib/zip/file.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/file"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/file_split"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/filesystem/dir"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/filesystem/directory_iterator"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/filesystem/file"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/filesystem/file_stat"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/filesystem/zip_file_name_mapper"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/filesystem"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/inflater"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/input_stream"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/ioextras/abstract_input_stream"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/ioextras/abstract_output_stream"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/ioextras/fake_io"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/ioextras"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/null_compressor"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/null_decompressor"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/null_input_stream"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/output_stream"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/pass_thru_compressor"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/pass_thru_decompressor"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/streamable_directory"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/streamable_stream"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip path kept for code that requires this file directly.
4
+ require "zip"
5
+ require "ziperu/version"
data/lib/zip.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubyzip's entry point. Loading ziperu this way makes `Zip` an alias of
4
+ # `Ziperu`, so code written against rubyzip (`Zip::File`, `Zip.setup`, ...)
5
+ # runs unchanged. If another library already defines `Zip` (rubyzip itself,
6
+ # most likely), the two cannot share the name: load ziperu with
7
+ # `require "ziperu"` and use `Ziperu::` directly instead.
8
+ if defined?(::Zip) && !(defined?(::Ziperu) && ::Zip.equal?(::Ziperu))
9
+ raise LoadError, "ziperu cannot alias Zip = Ziperu: a Zip constant is already defined " \
10
+ "(by #{Object.const_source_location(:Zip)&.first || 'another library'}). " \
11
+ "Remove rubyzip, or require \"ziperu\" and use Ziperu:: instead."
12
+ end
13
+
14
+ require "ziperu"
15
+ Zip = Ziperu unless defined?(::Zip)
16
+
17
+ # The `zip/...` paths rubyzip's files live at (`require "zip/filesystem"`
18
+ # is the common one) are kept under lib/zip/ as one-line shims onto the
19
+ # matching lib/ziperu/ file.
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The end-of-central-directory parsing is ported from rubyzip
4
+ # (Copyright (C) 2002-2026 the rubyzip authors).
5
+
6
+ module Ziperu
7
+ # Reads just the end-of-central-directory records of an archive, in Ruby.
8
+ #
9
+ # The native core refuses archives whose EOCD declares more entries than
10
+ # the central directory holds, so `Ziperu::File.open` raises on them
11
+ # where rubyzip warns (or raises `EntryNumberMismatchError` when
12
+ # `Ziperu.validate_declared_number_of_entries` is set). This gives
13
+ # `Ziperu::File.count_entries` rubyzip's behaviour at least.
14
+ class CentralDirectory # :nodoc:all
15
+ END_OF_CD_SIG = 0x06054b50
16
+ ZIP64_END_OF_CD_SIG = 0x06064b50
17
+ ZIP64_EOCD_LOCATOR_SIG = 0x07064b50
18
+
19
+ STATIC_EOCD_SIZE = 22
20
+ ZIP64_STATIC_EOCD_SIZE = 56
21
+ ZIP64_EOCD_LOC_SIZE = 20
22
+ MAX_FILE_COMMENT_SIZE = (1 << 16) - 1
23
+ MAX_END_OF_CD_SIZE =
24
+ MAX_FILE_COMMENT_SIZE + STATIC_EOCD_SIZE + ZIP64_EOCD_LOC_SIZE
25
+
26
+ attr_reader :size, :cdir_offset, :comment
27
+
28
+ # Number of entries declared by the EOCD of `path_or_io`.
29
+ def self.count_entries(path_or_io)
30
+ if path_or_io.respond_to?(:read)
31
+ new.count_entries(path_or_io)
32
+ else
33
+ ::File.open(path_or_io.to_s, 'rb') { |io| new.count_entries(io) }
34
+ end
35
+ end
36
+
37
+ def count_entries(io)
38
+ read_eocds(io)
39
+ @size
40
+ end
41
+
42
+ private
43
+
44
+ def unpack_64_e_o_c_d(buffer)
45
+ _, # ZIP64_END_OF_CD_SIG. We know we have this at this point.
46
+ _size_of_zip64_e_o_c_d,
47
+ _version_made_by,
48
+ _version_needed_for_extract,
49
+ _number_of_this_disk,
50
+ _number_of_disk_with_start_of_cdir,
51
+ _total_number_of_entries_in_cdir_on_this_disk,
52
+ @size,
53
+ size_in_bytes,
54
+ @cdir_offset = buffer.unpack('VQ<vvVVQ<Q<Q<Q<')
55
+
56
+ validate_size!(@size, size_in_bytes)
57
+ end
58
+
59
+ def unpack_64_eocd_locator(buffer)
60
+ _, _, zip64_eocd_offset, = buffer.unpack('VVQ<V')
61
+ zip64_eocd_offset
62
+ end
63
+
64
+ # Returns true when the plain EOCD was complete (no zip64 needed).
65
+ def unpack_e_o_c_d(buffer)
66
+ _, # END_OF_CD_SIG. We know we have this at this point.
67
+ number_of_this_disk,
68
+ number_of_disk_with_start_of_cdir,
69
+ total_number_of_entries_in_cdir_on_this_disk,
70
+ @size,
71
+ size_in_bytes,
72
+ @cdir_offset,
73
+ comment_length = buffer.unpack('VvvvvVVv')
74
+
75
+ complete = !([number_of_this_disk, number_of_disk_with_start_of_cdir,
76
+ total_number_of_entries_in_cdir_on_this_disk, @size].any?(0xFFFF) ||
77
+ size_in_bytes == 0xFFFFFFFF || @cdir_offset == 0xFFFFFFFF)
78
+
79
+ validate_size!(@size, size_in_bytes) if complete
80
+
81
+ @comment = comment_length.positive? ? buffer.slice(STATIC_EOCD_SIZE, comment_length) : ''
82
+ complete
83
+ end
84
+
85
+ def read_eocds(io)
86
+ base_location, data = eocd_data(io)
87
+
88
+ eocd_location = data.rindex([END_OF_CD_SIG].pack('V'))
89
+ raise Error, 'Zip end of central directory signature not found' unless eocd_location
90
+
91
+ return if unpack_e_o_c_d(data.slice(eocd_location..-1))
92
+
93
+ zip64_eocd_locator = data.rindex([ZIP64_EOCD_LOCATOR_SIG].pack('V'), eocd_location)
94
+ unless zip64_eocd_locator
95
+ raise Error, 'Zip64 end of central directory locator signature expected but not found'
96
+ end
97
+
98
+ zip64_eocd_location = data.rindex([ZIP64_END_OF_CD_SIG].pack('V'), zip64_eocd_locator)
99
+
100
+ zip64_eocd_data =
101
+ if zip64_eocd_location
102
+ data.slice(zip64_eocd_location..zip64_eocd_locator)
103
+ else
104
+ zip64_eocd_location = unpack_64_eocd_locator(
105
+ data.slice(zip64_eocd_locator..eocd_location)
106
+ )
107
+ raise Error, 'Zip64 end of central directory signature not found' unless zip64_eocd_location
108
+
109
+ io.seek(zip64_eocd_location, IO::SEEK_SET)
110
+ io.read(base_location + zip64_eocd_locator - zip64_eocd_location)
111
+ end
112
+
113
+ unpack_64_e_o_c_d(zip64_eocd_data)
114
+ end
115
+
116
+ def eocd_data(io)
117
+ io.binmode if io.respond_to?(:binmode)
118
+ begin
119
+ io.seek(-MAX_END_OF_CD_SIZE, IO::SEEK_END)
120
+ rescue Errno::EINVAL
121
+ io.seek(0, IO::SEEK_SET)
122
+ end
123
+
124
+ [io.tell, io.read.to_s.b]
125
+ end
126
+
127
+ def validate_size!(size, size_in_bytes)
128
+ return if size * CDIR_ENTRY_STATIC_HEADER_LENGTH <= size_in_bytes
129
+
130
+ error = EntryNumberMismatchError.new(size, size_in_bytes)
131
+ raise error if Ziperu.validate_declared_number_of_entries
132
+
133
+ warn error.message
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ported verbatim from rubyzip (Copyright (C) 2002-2026 the rubyzip authors).
4
+ # Not on any ziperu code path: the Rust core compresses and decompresses.
5
+ # Kept so that code which references these classes keeps loading.
6
+ module Ziperu
7
+ class Compressor # :nodoc:all
8
+ def finish; end
9
+ end
10
+ end
11
+
12
+ # Copyright (C) 2002, 2003 Thomas Sondergaard
13
+ # rubyzip is free software; you can redistribute it and/or
14
+ # modify it under the terms of the ruby license.