ruzip 0.1.0

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.
data/ext/Cargo.toml ADDED
@@ -0,0 +1,13 @@
1
+ [package]
2
+ name = "ruzip"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+ authors = ["Kitaiti Makoto <KitaitiMakoto@gmail.com>"]
6
+ publish = false
7
+
8
+ [lib]
9
+ crate-type = ["cdylib"]
10
+
11
+ [dependencies]
12
+ magnus = { version = "0.7.1" }
13
+ zip = "2.2.0"
data/ext/src/lib.rs ADDED
@@ -0,0 +1,202 @@
1
+ use magnus::{
2
+ Error, Integer, RString, Ruby, Symbol, Value, class, exception, function, method, prelude::*,
3
+ };
4
+ use std::fs;
5
+ use std::io::{self, Read};
6
+ use std::os::fd::FromRawFd;
7
+ use std::sync::{Arc, Mutex};
8
+ use zip::ZipArchive;
9
+
10
+ type Result<T> = std::result::Result<T, Error>;
11
+
12
+ #[magnus::wrap(class = "RuZip::Archive", free_immediately, size)]
13
+ struct Archive(Arc<Mutex<ZipArchive<fs::File>>>);
14
+
15
+ struct IO(Value);
16
+
17
+ impl io::Read for IO {
18
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
19
+ let buf_len = buf.len();
20
+ let r_string: Value = self.0.funcall_public("read", (buf_len,)).unwrap(); // FIXME: unwrap(). Should use second arg?
21
+ if r_string.is_nil() {
22
+ Ok(0)
23
+ } else {
24
+ let data = r_string.to_string();
25
+ let len = data.len();
26
+ buf.clone_from_slice(data.as_bytes()); // FIXME: clone_from_slice panics when sizes of buf and data are not the same
27
+ Ok(len)
28
+ }
29
+ }
30
+ }
31
+
32
+ impl io::Seek for IO {
33
+ fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
34
+ use io::SeekFrom::*;
35
+ let value = self.0;
36
+
37
+ match pos {
38
+ Start(i) => value
39
+ .funcall_public::<&str, (u64, Symbol), u64>("seek", (i, Symbol::new("SET")))
40
+ .unwrap(), // FIXME: unwrap()
41
+ End(i) => value
42
+ .funcall_public("seek", (i, Symbol::new("END")))
43
+ .unwrap(),
44
+ Current(i) => value
45
+ .funcall_public("seek", (i, Symbol::new("CUR")))
46
+ .unwrap(),
47
+ };
48
+ Ok(value.funcall_public("pos", ()).unwrap())
49
+ }
50
+ }
51
+
52
+ impl Archive {
53
+ fn new(r_io: Value) -> Result<Self> {
54
+ let io = if r_io.is_kind_of(class::io()) {
55
+ let fileno: Integer = r_io.funcall_public("fileno", ())?;
56
+ let raw_fd = fileno.to_i32()?;
57
+ unsafe { fs::File::from_raw_fd(raw_fd) }
58
+ } else if r_io.respond_to("to_path", false)? {
59
+ fs::File::open(
60
+ r_io.funcall_public::<&str, (), RString>("to_path", ())?
61
+ .to_string()?,
62
+ )
63
+ .unwrap()
64
+ } else if r_io.respond_to("read", false)? {
65
+ // IO(r_io);
66
+ todo!("#read");
67
+ } else if r_io.is_kind_of(class::string()) {
68
+ fs::File::open(r_io.to_r_string()?.to_string()?).unwrap() // FIXME: unwrap()
69
+ } else {
70
+ return Err(Error::new(
71
+ exception::type_error(),
72
+ format!("Unsupported argument type: {}", r_io.inspect()),
73
+ ));
74
+ };
75
+ let zip: ZipArchive<fs::File> = ZipArchive::new(io)
76
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))?;
77
+ Ok(Self(Arc::new(Mutex::new(zip))))
78
+ }
79
+
80
+ fn len(&self) -> Result<usize> {
81
+ Ok(self
82
+ .0
83
+ .lock()
84
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))?
85
+ .len())
86
+ }
87
+
88
+ fn by_index(&self, index: usize) -> Result<File> {
89
+ match self
90
+ .0
91
+ .lock()
92
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))?
93
+ .by_index(index)
94
+ {
95
+ Ok(_) => Ok(File(self.0.clone(), index)),
96
+ Err(e) => Err(Error::new(exception::runtime_error(), format!("{}", e))),
97
+ }
98
+ }
99
+
100
+ fn by_name(&self, name: RString) -> Result<Option<File>> {
101
+ let name_string = name
102
+ .to_string()
103
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))?;
104
+ let mut archive = self
105
+ .0
106
+ .lock()
107
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))?;
108
+ // TODO: Cache entries
109
+ for i in 0..archive.len() {
110
+ let file = archive
111
+ .by_index(i)
112
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))?;
113
+ if file.name_raw() == name_string.clone().into_bytes() {
114
+ return Ok(Some(File(self.0.clone(), i)));
115
+ }
116
+ }
117
+ Ok(None)
118
+ }
119
+ }
120
+
121
+ #[magnus::wrap(class = "RuZip::File")]
122
+ struct File(Arc<Mutex<ZipArchive<fs::File>>>, usize);
123
+
124
+ impl File {
125
+ // FIXME: exception::runtime_error() -> ruby.exception_runtime_error()
126
+ fn name(&self) -> Result<String> {
127
+ String::from_utf8(
128
+ self.0
129
+ .lock()
130
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))?
131
+ .by_index(self.1)
132
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))?
133
+ .name_raw()
134
+ .into(),
135
+ )
136
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))
137
+ }
138
+
139
+ fn size(&self) -> Result<u64> {
140
+ let size = self
141
+ .0
142
+ .lock()
143
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))?
144
+ .by_index(self.1)
145
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))?
146
+ .size();
147
+ Ok(size)
148
+ }
149
+
150
+ // TODO: Use ExtendedTimestamp if available
151
+ fn last_modified(&self) -> Result<Option<Value>> {
152
+ let last_modified = self
153
+ .0
154
+ .lock()
155
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))?
156
+ .by_index(self.1)
157
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))?
158
+ .last_modified();
159
+ match last_modified {
160
+ Some(mtime) => Ok(Some(magnus::class::time().new_instance((
161
+ mtime.year(),
162
+ mtime.month(),
163
+ mtime.day(),
164
+ mtime.hour(),
165
+ mtime.minute(),
166
+ mtime.second(),
167
+ ))?)),
168
+ None => Ok(None),
169
+ }
170
+ }
171
+
172
+ fn read(&self) -> Result<String> {
173
+ let mut buf = String::new();
174
+ self.0
175
+ .lock()
176
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))?
177
+ .by_index(self.1)
178
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))?
179
+ .read_to_string(&mut buf)
180
+ .map_err(|e| Error::new(exception::runtime_error(), format!("{}", e)))?;
181
+ Ok(buf)
182
+ }
183
+ }
184
+
185
+ #[magnus::init]
186
+ fn init(ruby: &Ruby) -> Result<()> {
187
+ let module = ruby.define_module("RuZip")?;
188
+
189
+ let archive_class = module.define_class("Archive", ruby.class_object())?;
190
+ archive_class.define_singleton_method("new", function!(Archive::new, 1))?;
191
+ archive_class.define_method("length", method!(Archive::len, 0))?;
192
+
193
+ let file_class = module.define_class("File", ruby.class_object())?;
194
+ archive_class.define_method("by_index", method!(Archive::by_index, 1))?;
195
+ archive_class.define_method("by_name", method!(Archive::by_name, 1))?;
196
+ file_class.define_method("name", method!(File::name, 0))?;
197
+ file_class.define_method("size", method!(File::size, 0))?;
198
+ file_class.define_method("last_modified", method!(File::last_modified, 0))?;
199
+ file_class.define_method("read", method!(File::read, 0))?;
200
+
201
+ Ok(())
202
+ }
data/ruzip.gemspec ADDED
@@ -0,0 +1,40 @@
1
+ require "json"
2
+
3
+ rust_metadata = JSON.load(`cargo metadata --manifest-path=ext/Cargo.toml --no-deps --format-version=1`)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ruzip"
7
+ spec.version = rust_metadata["packages"][0]["version"]
8
+ spec.authors = ["Kitaiti Makoto"]
9
+ spec.email = ["KitaitiMakoto@gmail.com"]
10
+
11
+ spec.summary = "Library to support the reading and writing of zip files."
12
+ spec.description = "Library to support the reading and writing of zip files. A wrapper of Rust's zip crate."
13
+ spec.homepage = "https://gitlab.com/KitaitiMakoto/ruzip"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.6.0"
16
+ spec.required_rubygems_version = ">= 3.3.11"
17
+
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "https://gitlab.com/KitaitiMakoto/ruzip"
20
+ spec.metadata["changelog_uri"] = "https://gitlab.com/KitaitiMakoto/ruzip/-/blob/master/CHANGELOG.md"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(__dir__) do
25
+ `git ls-files -z`.split("\x0")
26
+ end
27
+ spec.require_paths = ["lib"]
28
+ spec.extensions = ["ext/Cargo.toml"]
29
+
30
+ spec.add_development_dependency "rake", "~> 13.0"
31
+ spec.add_development_dependency "rake-compiler"
32
+ spec.add_development_dependency "test-unit", "~> 3.0"
33
+ spec.add_development_dependency "test-unit-notify"
34
+ spec.add_development_dependency "terminal-notifier"
35
+ spec.add_development_dependency "rubygems-tasks"
36
+ spec.add_development_dependency "kar"
37
+
38
+ # For more information and examples about making a new gem, check out our
39
+ # guide at: https://bundler.io/guides/creating_gem.html
40
+ end
data/sig/ruzip.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module RuZip
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,11 @@
1
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
2
+
3
+ require "ruzip"
4
+ require "test-unit"
5
+ require "test/unit/notify"
6
+
7
+ class Test::Unit::TestCase
8
+ def fixture_path(component)
9
+ File.join(__dir__, "fixtures", component)
10
+ end
11
+ end
@@ -0,0 +1,52 @@
1
+ require_relative "helper"
2
+
3
+ class TestArchive < Test::Unit::TestCase
4
+ def setup
5
+ @fixture = fixture_path("accessible_epub_3.epub")
6
+ end
7
+
8
+ test "new with path" do
9
+ archive = RuZip::Archive.new(@fixture)
10
+ assert_kind_of RuZip::Archive, archive
11
+ end
12
+
13
+ test "new with pathname" do
14
+ archive = RuZip::Archive.new(Pathname(@fixture))
15
+ assert_kind_of RuZip::Archive, archive
16
+ end
17
+
18
+ test "new with file" do
19
+ archive = RuZip::Archive.new(File.open(@fixture))
20
+ assert_kind_of RuZip::Archive, archive
21
+ end
22
+
23
+ test "new with closed file" do
24
+ io = nil
25
+ File.open @fixture do |file|
26
+ io = file
27
+ end
28
+ assert_raise_kind_of IOError do
29
+ RuZip::Archive.new(io)
30
+ end
31
+ end
32
+
33
+ test "new with string io" do
34
+ pend
35
+
36
+ require "stringio"
37
+ io = StringIO.new("EPUB file")
38
+ archive = RuZip::Archive.new(io)
39
+ assert_kind_of RuZip::Archive, archive
40
+ end
41
+
42
+ test "new with unsupported type" do
43
+ assert_raise_kind_of TypeError do
44
+ RuZip::Archive.new(:symbol_object)
45
+ end
46
+ end
47
+
48
+ test "len" do
49
+ archive = RuZip::Archive::new(@fixture)
50
+ assert_equal 38, archive.length
51
+ end
52
+ end
data/test/test_file.rb ADDED
@@ -0,0 +1,16 @@
1
+ require_relative "helper"
2
+
3
+ class TestFile < Test::Unit::TestCase
4
+ setup do
5
+ @archive = RuZip::Archive.new(fixture_path("accessible_epub_3.epub"))
6
+ end
7
+
8
+ test "retrieve" do
9
+ file = @archive.by_name("META-INF/container.xml")
10
+ assert_instance_of RuZip::File, file
11
+ end
12
+
13
+ test "retrieve non-existent" do
14
+ assert_nil @archive.by_name("non-existent")
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ require_relative "helper"
2
+
3
+ class TestRuZip < Test::Unit::TestCase
4
+ test "version" do
5
+ assert_equal "0.1.0", Gem.loaded_specs["ruzip"].version.to_s
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruzip
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kitaiti Makoto
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '13.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '13.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake-compiler
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: test-unit
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: test-unit-notify
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: terminal-notifier
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rubygems-tasks
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: kar
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: Library to support the reading and writing of zip files. A wrapper of
111
+ Rust's zip crate.
112
+ email:
113
+ - KitaitiMakoto@gmail.com
114
+ executables: []
115
+ extensions:
116
+ - ext/Cargo.toml
117
+ extra_rdoc_files: []
118
+ files:
119
+ - ".gitignore"
120
+ - ".gitlab-ci.yml"
121
+ - CHANGELOG.md
122
+ - CODE_OF_CONDUCT.md
123
+ - Gemfile
124
+ - MIT-LICENSE
125
+ - README.md
126
+ - Rakefile
127
+ - ext/Cargo.lock
128
+ - ext/Cargo.toml
129
+ - ext/src/lib.rs
130
+ - ruzip.gemspec
131
+ - sig/ruzip.rbs
132
+ - test/helper.rb
133
+ - test/test_archive.rb
134
+ - test/test_file.rb
135
+ - test/test_ruzip.rb
136
+ homepage: https://gitlab.com/KitaitiMakoto/ruzip
137
+ licenses:
138
+ - MIT
139
+ metadata:
140
+ homepage_uri: https://gitlab.com/KitaitiMakoto/ruzip
141
+ source_code_uri: https://gitlab.com/KitaitiMakoto/ruzip
142
+ changelog_uri: https://gitlab.com/KitaitiMakoto/ruzip/-/blob/master/CHANGELOG.md
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: 2.6.0
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: 3.3.11
156
+ requirements: []
157
+ rubygems_version: 3.7.2
158
+ specification_version: 4
159
+ summary: Library to support the reading and writing of zip files.
160
+ test_files: []