ro-crate 0.5.3 → 0.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 987f4bf5543890ca23264eaa771134f6ec2ba848a84ea32424cf951429141523
4
- data.tar.gz: f05c5166d95be3cec706fbb5d550c5af916f5f4c444112575ec49d5fe961e12a
3
+ metadata.gz: eee42ff56abf6a19d3a0e3575957c338b79a87c5609566618c032ca302ccab8d
4
+ data.tar.gz: 7d36b9517a344a2d2f9559a11229e8a51e2a2e4b02c5ada951d8b86b82051900
5
5
  SHA512:
6
- metadata.gz: '0848a81d03d8cdf7b4809de8613d90b67950f4339ae20ab5ce4ba6fa8d9e9559033c4a53ca47391b99ed5287ba7a2f2be7976b87c7ea8479051b8e7dfc9da6e8'
7
- data.tar.gz: c1fc1ffdadafd04c19d81c37befec9eed9bcf9e40db9c39158b20a2a8ec8b8368841c65211d31439ba585f637de9541e2b7d58673da4964c7b8afccbfb9bf6ff
6
+ metadata.gz: 82917f083837b091d304e61188dbcb06cbe9a306676e3c17e4c56d662cb3a65d90bb7f41b5c09503caaf457a0740d30a9011a4f4edc64fe2bf78556a6e9f078e
7
+ data.tar.gz: 430857f30551fb3f4e2c82fce71babfd9574cf4a039ece9dd26318766d054ff4ac9725b3fa6bab134d8e0a5ad5eed01905324aeadbdaf0053730716b57e97eca
@@ -7,7 +7,7 @@ jobs:
7
7
  runs-on: ubuntu-latest
8
8
  steps:
9
9
  - name: Checkout
10
- uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly.
10
+ uses: actions/checkout@v6
11
11
  with:
12
12
  persist-credentials: false
13
13
  - name: Setup Ruby
@@ -5,11 +5,11 @@ jobs:
5
5
  runs-on: ubuntu-latest
6
6
  strategy:
7
7
  matrix:
8
- ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3']
8
+ ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0']
9
9
  fail-fast: false
10
10
  steps:
11
11
  - name: Checkout
12
- uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly.
12
+ uses: actions/checkout@v6
13
13
  with:
14
14
  persist-credentials: false
15
15
  - name: Setup Ruby
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-3.2.5
1
+ ruby-3.4.9
data/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  ![Tests](https://github.com/ResearchObject/ro-crate-ruby/actions/workflows/tests.yml/badge.svg)
4
4
 
5
- This is a WIP gem for creating, manipulating and reading RO-Crates (conforming to version 1.1 of the specification).
5
+ This is a WIP gem for creating, manipulating and reading RO-Crates (conforming to version 1.3 of the specification). RO-Crates produced by older versions (1.0, 1.1, 1.2) of the spec can still be read.
6
6
 
7
- * RO-Crate - https://researchobject.github.io/ro-crate/
8
- * RO-Crate spec (1.1) - https://researchobject.github.io/ro-crate/1.1/
7
+ * RO-Crate - https://www.researchobject.org/ro-crate/
8
+ * RO-Crate spec (1.3) - https://www.researchobject.org/ro-crate/specification/1.3/
9
9
 
10
10
  ## Installation
11
11
 
data/Rakefile CHANGED
@@ -1,6 +1,5 @@
1
1
  require 'bundler/gem_tasks'
2
2
  require 'rake/testtask'
3
- require 'rdoc/task'
4
3
 
5
4
  desc 'Default: run unit tests.'
6
5
  task default: :test
@@ -13,14 +12,6 @@ Rake::TestTask.new(:test) do |t|
13
12
  t.warning = false
14
13
  end
15
14
 
16
- Rake::RDocTask.new(:rdoc) do |rdoc|
17
- rdoc.rdoc_dir = 'rdoc'
18
- rdoc.title = 'Devise'
19
- rdoc.options << '--line-numbers' << '--inline-source'
20
- rdoc.rdoc_files.include('README.md')
21
- rdoc.rdoc_files.include('lib/**/*.rb')
22
- end
23
-
24
15
  task :console do
25
16
  require 'irb'
26
17
  require 'irb/completion'
@@ -21,9 +21,16 @@ module ROCrate
21
21
 
22
22
  ##
23
23
  # Initialize an empty RO-Crate.
24
- def initialize(id = IDENTIFIER, properties = {})
24
+ #
25
+ # @param id [String] The crate's identifier.
26
+ # @param properties [Hash] Initial properties for the root data entity.
27
+ # @param version [String] RO-Crate spec version to declare (default: ROCrate::Metadata::DEFAULT_VERSION).
28
+ # Must be one of ROCrate::Metadata::SUPPORTED_VERSIONS.
29
+ def initialize(id = IDENTIFIER, properties = {}, version: ROCrate::Metadata::DEFAULT_VERSION)
30
+ ROCrate::Metadata.warn_unrecognized_version(version)
25
31
  @data_entities = Set.new
26
32
  @contextual_entities = Set.new
33
+ @metadata_version = version
27
34
  super(self, nil, id, properties)
28
35
  end
29
36
 
@@ -168,7 +175,7 @@ module ROCrate
168
175
  #
169
176
  # @return [Metadata]
170
177
  def metadata
171
- @metadata ||= ROCrate::Metadata.new(self)
178
+ @metadata ||= ROCrate::Metadata.new(self, {}, version: @metadata_version || ROCrate::Metadata::DEFAULT_VERSION)
172
179
  end
173
180
 
174
181
  ##
@@ -74,9 +74,8 @@ module ROCrate
74
74
  end
75
75
 
76
76
  def list_all_files(source_directory, include_hidden: false)
77
- args = ['**/*']
78
- args << ::File::FNM_DOTMATCH if include_hidden
79
- Dir.chdir(source_directory) { Dir.glob(*args) }.reject do |path|
77
+ flags = include_hidden ? ::File::FNM_DOTMATCH : 0
78
+ Dir.glob('**/*', flags, base: source_directory).reject do |path|
80
79
  path == '.' || path == '..' || path.end_with?('/.')
81
80
  end
82
81
  end
@@ -5,13 +5,46 @@ module ROCrate
5
5
  IDENTIFIER = 'ro-crate-metadata.json'.freeze
6
6
  IDENTIFIER_1_0 = 'ro-crate-metadata.jsonld'.freeze # 1.0 spec identifier
7
7
  RO_CRATE_BASE = 'https://w3id.org/ro/crate/'
8
- CONTEXT = "#{RO_CRATE_BASE}1.1/context".freeze
9
- SPEC = "#{RO_CRATE_BASE}1.1".freeze
10
8
 
11
- def initialize(crate, properties = {})
9
+ SUPPORTED_VERSIONS = %w[1.0 1.0-DRAFT 1.1 1.1-DRAFT 1.2 1.2-DRAFT 1.3].freeze
10
+ DEFAULT_VERSION = '1.3'.freeze
11
+
12
+ CONTEXT = "#{RO_CRATE_BASE}#{DEFAULT_VERSION}/context".freeze
13
+ SPEC = "#{RO_CRATE_BASE}#{DEFAULT_VERSION}".freeze
14
+
15
+ attr_reader :version
16
+
17
+ ##
18
+ # Emit a warning if the given version is not in SUPPORTED_VERSIONS.
19
+ # Does not raise — unrecognized versions are still accepted so the library
20
+ # stays forward-compatible with future spec versions that need no changes.
21
+ def self.warn_unrecognized_version(v)
22
+ return if SUPPORTED_VERSIONS.include?(v)
23
+ warn "Unrecognized RO-Crate version: #{v.inspect}. Known versions: #{SUPPORTED_VERSIONS.join(', ')}"
24
+ end
25
+
26
+ def initialize(crate, properties = {}, version: DEFAULT_VERSION)
27
+ self.class.warn_unrecognized_version(version)
28
+ @version = version
12
29
  super(crate, nil, IDENTIFIER, properties)
13
30
  end
14
31
 
32
+ ##
33
+ # Update the spec version this metadata declares.
34
+ # Used by the Reader to preserve the version of a parsed crate.
35
+ def version=(v)
36
+ self.class.warn_unrecognized_version(v)
37
+ @version = v
38
+ end
39
+
40
+ def context_url
41
+ "#{RO_CRATE_BASE}#{@version}/context"
42
+ end
43
+
44
+ def spec_url
45
+ "#{RO_CRATE_BASE}#{@version}"
46
+ end
47
+
15
48
  ##
16
49
  # Generate the crate's `ro-crate-metadata.jsonld`.
17
50
  # @return [String] The rendered JSON-LD as a "prettified" string.
@@ -21,7 +54,7 @@ module ROCrate
21
54
  end
22
55
 
23
56
  def context
24
- @context || CONTEXT
57
+ @context || context_url
25
58
  end
26
59
 
27
60
  def context= c
@@ -39,7 +72,7 @@ module ROCrate
39
72
  '@id' => IDENTIFIER,
40
73
  '@type' => 'CreativeWork',
41
74
  'about' => { '@id' => crate.id },
42
- 'conformsTo' => { '@id' => SPEC }
75
+ 'conformsTo' => { '@id' => spec_url }
43
76
  }
44
77
  end
45
78
  end
@@ -1,7 +1,11 @@
1
+ require 'zip/version'
2
+
1
3
  module ROCrate
2
4
  ##
3
5
  # A class to handle reading of RO-Crates from Zip files or directories.
4
6
  class Reader
7
+ LEGACY_EXTRACT = Zip::VERSION.start_with?('2.').freeze
8
+
5
9
  ##
6
10
  # Reads an RO-Crate from a directory or zip file.
7
11
  #
@@ -42,15 +46,15 @@ module ROCrate
42
46
  #
43
47
  # @param source [#read] An IO-like object containing a Zip file.
44
48
  # @param target [String, ::File, Pathname] The target directory where the file should be unzipped.
45
- def self.unzip_io_to(io, target)
46
- Dir.chdir(target) do
47
- Zip::InputStream.open(io) do |input|
48
- while (entry = input.get_next_entry)
49
- unless ::File.exist?(entry.name) || entry.name_is_directory?
50
- FileUtils::mkdir_p(::File.dirname(entry.name))
51
- ::File.binwrite(entry.name, input.read)
52
- end
53
- end
49
+ def self.unzip_io_to(source, target)
50
+ target_path = Pathname(target)
51
+ Zip::InputStream.open(source) do |input|
52
+ while (entry = input.get_next_entry)
53
+ next if entry.name_is_directory?
54
+ dest = safe_join(target_path, entry.name)
55
+ next if dest.exist?
56
+ FileUtils.mkdir_p(dest.dirname)
57
+ ::File.binwrite(dest, input.read)
54
58
  end
55
59
  end
56
60
  end
@@ -60,15 +64,14 @@ module ROCrate
60
64
  #
61
65
  # @param source [String, ::File, Pathname] The location of the zip file.
62
66
  # @param target [String, ::File, Pathname] The target directory where the file should be unzipped.
63
- def self.unzip_file_to(file_or_path, target)
64
- Dir.chdir(target) do
65
- Zip::File.open(file_or_path) do |zipfile|
66
- zipfile.each do |entry|
67
- unless ::File.exist?(entry.name)
68
- FileUtils::mkdir_p(::File.dirname(entry.name))
69
- zipfile.extract(entry, entry.name)
70
- end
71
- end
67
+ def self.unzip_file_to(source, target)
68
+ target_path = Pathname(target)
69
+ Zip::File.open(source) do |zipfile|
70
+ zipfile.each do |entry|
71
+ dest = safe_join(target_path, entry.name)
72
+ next if dest.exist?
73
+ FileUtils.mkdir_p(dest.dirname)
74
+ LEGACY_EXTRACT ? entry.extract(dest) : entry.extract(entry.name, destination_directory: target_path)
72
75
  end
73
76
  end
74
77
  end
@@ -87,6 +90,7 @@ module ROCrate
87
90
 
88
91
  # Traverse the unzipped directory to try and find the crate's root
89
92
  root_dir = detect_root_directory(target_dir)
93
+ raise ROCrate::ReadException, "No metadata found!" unless root_dir
90
94
 
91
95
  read_directory(root_dir)
92
96
  end
@@ -184,7 +188,10 @@ module ROCrate
184
188
  def self.initialize_crate(entity_hash, source, crate_class: ROCrate::Crate, context:)
185
189
  crate_class.new.tap do |crate|
186
190
  crate.properties = entity_hash.delete(ROCrate::Crate::IDENTIFIER)
187
- crate.metadata.properties = entity_hash.delete(ROCrate::Metadata::IDENTIFIER)
191
+ metadata_props = entity_hash.delete(ROCrate::Metadata::IDENTIFIER)
192
+ crate.metadata.properties = metadata_props
193
+ parsed_version = extract_version(metadata_props)
194
+ crate.metadata.version = parsed_version if parsed_version
188
195
  crate.metadata.context = context
189
196
  preview_properties = entity_hash.delete(ROCrate::Preview::IDENTIFIER)
190
197
  preview_path = ::File.join(source, ROCrate::Preview::IDENTIFIER)
@@ -265,7 +272,7 @@ module ROCrate
265
272
 
266
273
  ##
267
274
  # Extract the metadata entity from the entity hash, according to the rules defined here:
268
- # https://www.researchobject.org/ro-crate/1.1/root-data-entity.html#finding-the-root-data-entity
275
+ # https://www.researchobject.org/ro-crate/specification/1.2/root-data-entity.html#finding-the-root-data-entity
269
276
  # @return [nil, Hash{String => Hash}] A Hash containing (hopefully) one value, the metadata entity's properties
270
277
  # mapped by its @id, or nil if nothing is found.
271
278
  def self.extract_metadata_entity(entities)
@@ -284,6 +291,24 @@ module ROCrate
284
291
  entities.delete(ROCrate::Metadata::IDENTIFIER_1_0))
285
292
  end
286
293
 
294
+ ##
295
+ # Extract the spec version from the metadata entity's `conformsTo`.
296
+ # Looks for an `@id` matching `https://w3id.org/ro/crate/<version>` and returns `<version>`.
297
+ # @param metadata_props [Hash, nil] The metadata entity's properties.
298
+ # @return [String, nil] The parsed version string, or nil if not found.
299
+ def self.extract_version(metadata_props)
300
+ return nil unless metadata_props
301
+ conforms = metadata_props['conformsTo']
302
+ conforms = [conforms] unless conforms.is_a?(Array)
303
+ conforms.compact.each do |c|
304
+ id = c.is_a?(Hash) ? c['@id'] : c
305
+ next unless id&.start_with?(ROCrate::Metadata::RO_CRATE_BASE)
306
+ version = id.sub(ROCrate::Metadata::RO_CRATE_BASE, '').split('/').first
307
+ return version if version && !version.empty?
308
+ end
309
+ nil
310
+ end
311
+
287
312
  ##
288
313
  # Extract the ro-crate-preview entity from the entity hash.
289
314
  # @return [Hash{String => Hash}] A Hash containing the preview entity's properties mapped by its @id, or nil if nothing is found.
@@ -293,7 +318,7 @@ module ROCrate
293
318
 
294
319
  ##
295
320
  # Extract the root entity from the entity hash, according to the rules defined here:
296
- # https://www.researchobject.org/ro-crate/1.1/root-data-entity.html#finding-the-root-data-entity
321
+ # https://www.researchobject.org/ro-crate/specification/1.2/root-data-entity.html#finding-the-root-data-entity
297
322
  # @return [Hash{String => Hash}] A Hash containing (hopefully) one value, the root entity's properties,
298
323
  # mapped by its @id.
299
324
  def self.extract_root_entity(entities)
@@ -303,7 +328,7 @@ module ROCrate
303
328
  end
304
329
 
305
330
  ##
306
- # Finds an RO-Crate's root directory (where `ro-crate-metdata.json` is located) within a given directory.
331
+ # Finds an RO-Crate's root directory (where `ro-crate-metadata.json` is located) within a given directory.
307
332
  #
308
333
  # @param source [String, ::File, Pathname] The location of the directory.
309
334
  # @return [Pathname, nil] The path to the root, or nil if not found.
@@ -323,5 +348,28 @@ module ROCrate
323
348
 
324
349
  nil
325
350
  end
351
+
352
+ ##
353
+ # Safely joins a desired file path onto a base directory, raising an exception if the path attempts to traverse
354
+ # outside it.
355
+ #
356
+ # @param base [Pathname] The base directory where the file will go.
357
+ # @param path [String] The desired file path.
358
+ #
359
+ # @raise [ROCrate::ReadException] Raised if an unsafe path is given.
360
+ #
361
+ # @return [Pathname] The safely joined base + path.
362
+ def self.safe_join(base, path)
363
+ dest = base.join(path)
364
+ # Guard against zip-slip attacks.
365
+ begin
366
+ unsafe = dest.expand_path.relative_path_from(base.expand_path).each_filename.first == '..'
367
+ rescue ArgumentError # Handle unjoinable paths, e.g. on different drives.
368
+ unsafe = true
369
+ end
370
+ raise ROCrate::ReadException, "Unsafe path in zip entry: #{path}" if unsafe
371
+
372
+ dest
373
+ end
326
374
  end
327
375
  end
@@ -44,7 +44,7 @@ module ROCrate
44
44
  # @param destination [String, ::File] The destination where to write the RO-Crate zip.
45
45
  # @param skip_preview [Boolean] Whether or not to skip generation of the RO-Crate preview HTML file.
46
46
  def write_zip(destination, skip_preview: false)
47
- Zip::File.open(destination, Zip::File::CREATE) do |zip|
47
+ Zip::File.open(destination, create: true) do |zip|
48
48
  @crate.payload.each do |path, entry|
49
49
  next if entry.directory?
50
50
  next if skip_preview && entry&.source.is_a?(ROCrate::PreviewGenerator)
data/ro_crate.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'ro-crate'
3
- s.version = '0.5.3'
3
+ s.version = '0.7.0'
4
4
  s.summary = 'Create, manipulate, read RO-Crates.'
5
5
  s.authors = ['Finn Bacall']
6
6
  s.email = 'finn.bacall@manchester.ac.uk'
@@ -8,12 +8,13 @@ Gem::Specification.new do |s|
8
8
  s.homepage = 'https://github.com/ResearchObject/ro-crate-ruby'
9
9
  s.require_paths = ['lib']
10
10
  s.licenses = ['MIT']
11
- s.add_runtime_dependency 'addressable', '>= 2.7', '< 2.9'
12
- s.add_runtime_dependency 'rubyzip', '~> 2.0.0'
13
- s.add_development_dependency 'rake', '~> 13.0.0'
11
+ s.required_ruby_version = '>= 2.7.0'
12
+ s.add_runtime_dependency 'addressable', '>= 2.7', '< 3'
13
+ s.add_runtime_dependency 'rubyzip', '>= 2.3', '< 4'
14
+ s.add_development_dependency 'rake', '~> 13.4.2'
14
15
  s.add_development_dependency 'test-unit', '~> 3.5.3'
15
16
  s.add_development_dependency 'simplecov', '~> 0.21.2'
16
17
  s.add_development_dependency 'yard', '~> 0.9.25'
17
- s.add_development_dependency 'webmock', '~> 3.8.3'
18
- s.add_development_dependency 'rexml', '~> 3.2.5'
18
+ s.add_development_dependency 'webmock', '~> 3.26.2'
19
+ s.add_development_dependency 'rexml', '~> 3.4.4'
19
20
  end
data/test/crate_test.rb CHANGED
@@ -377,4 +377,33 @@ class CrateTest < Test::Unit::TestCase
377
377
  assert_nil crate.get('#joe')
378
378
  assert crate.get('#joehouse')
379
379
  end
380
+
381
+ test 'defaults to RO-Crate spec 1.3' do
382
+ crate = ROCrate::Crate.new
383
+ assert_equal '1.3', crate.metadata.version
384
+ assert_equal 'https://w3id.org/ro/crate/1.3/context', crate.metadata.context
385
+ assert_equal 'https://w3id.org/ro/crate/1.3', crate.metadata.spec_url
386
+ assert_equal({ '@id' => 'https://w3id.org/ro/crate/1.3' }, crate.metadata.properties['conformsTo'])
387
+ end
388
+
389
+ test 'can write older spec version' do
390
+ crate = ROCrate::Crate.new(ROCrate::Crate::IDENTIFIER, {}, version: '1.1')
391
+ assert_equal '1.1', crate.metadata.version
392
+ assert_equal 'https://w3id.org/ro/crate/1.1/context', crate.metadata.context
393
+ assert_equal({ '@id' => 'https://w3id.org/ro/crate/1.1' }, crate.metadata.properties['conformsTo'])
394
+ end
395
+
396
+ test 'warns but accepts unrecognized spec version' do
397
+ original_stderr = $stderr
398
+ begin
399
+ $stderr = StringIO.new
400
+ crate = ROCrate::Crate.new(ROCrate::Crate::IDENTIFIER, {}, version: '1.5')
401
+ err = $stderr.string
402
+ assert_match(/Unrecognized RO-Crate version/, err)
403
+ assert_equal '1.5', crate.metadata.version
404
+ assert_equal 'https://w3id.org/ro/crate/1.5', crate.metadata.spec_url
405
+ ensure
406
+ $stderr = original_stderr
407
+ end
408
+ end
380
409
  end
@@ -0,0 +1 @@
1
+ I have spaces in my name
@@ -0,0 +1,42 @@
1
+ {
2
+ "@context": "https://w3id.org/ro/crate/1.2/context",
3
+ "@graph": [
4
+ {
5
+ "@id": "ro-crate-metadata.json",
6
+ "@type": "CreativeWork",
7
+ "about": {
8
+ "@id": "./"
9
+ },
10
+ "conformsTo": {
11
+ "@id": "https://w3id.org/ro/crate/1.2"
12
+ }
13
+ },
14
+ {
15
+ "@id": "ro-crate-preview.html",
16
+ "@type": "CreativeWork",
17
+ "about": {
18
+ "@id": "./"
19
+ }
20
+ },
21
+ {
22
+ "@id": "./",
23
+ "@type": "Dataset",
24
+ "hasPart": [
25
+ {
26
+ "@id": "http://example.com/external_ref.txt"
27
+ },
28
+ {
29
+ "@id": "file%20with%20spaces.txt"
30
+ }
31
+ ]
32
+ },
33
+ {
34
+ "@id": "http://example.com/external_ref.txt",
35
+ "@type": "File"
36
+ },
37
+ {
38
+ "@id": "file%20with%20spaces.txt",
39
+ "@type": "File"
40
+ }
41
+ ]
42
+ }
@@ -0,0 +1,82 @@
1
+
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <title>New RO-Crate</title>
6
+ <script type="application/ld+json">{
7
+ "@context": "https://w3id.org/ro/crate/1.2/context",
8
+ "@graph": [
9
+ {
10
+ "@id": "ro-crate-metadata.json",
11
+ "@type": "CreativeWork",
12
+ "about": {
13
+ "@id": "./"
14
+ },
15
+ "conformsTo": {
16
+ "@id": "https://w3id.org/ro/crate/1.2"
17
+ }
18
+ },
19
+ {
20
+ "@id": "ro-crate-preview.html",
21
+ "@type": "CreativeWork",
22
+ "about": {
23
+ "@id": "./"
24
+ }
25
+ },
26
+ {
27
+ "@id": "./",
28
+ "@type": "Dataset",
29
+ "hasPart": [
30
+ {
31
+ "@id": "http://example.com/external_ref.txt"
32
+ },
33
+ {
34
+ "@id": "file%20with%20spaces.txt"
35
+ }
36
+ ]
37
+ },
38
+ {
39
+ "@id": "http://example.com/external_ref.txt",
40
+ "@type": "File"
41
+ },
42
+ {
43
+ "@id": "file%20with%20spaces.txt",
44
+ "@type": "File"
45
+ }
46
+ ]
47
+ }</script>
48
+ <meta name="generator" content="https://github.com/ResearchObject/ro-crate-ruby">
49
+ <meta name="keywords" content="RO-Crate">
50
+ <meta charset="utf-8">
51
+ </head>
52
+ <body>
53
+ <h1>New RO-Crate</h1>
54
+
55
+ <p>
56
+
57
+ </p>
58
+ <dl>
59
+
60
+
61
+
62
+
63
+ </dl>
64
+
65
+ <h2>Contents</h2>
66
+ <ul>
67
+
68
+ <li>
69
+ <strong><a href="http://example.com/external_ref.txt" target="_blank">http://example.com/external_ref.txt</a></strong>
70
+
71
+
72
+ </li>
73
+
74
+ <li>
75
+ <strong>file%20with%20spaces.txt</strong>
76
+
77
+
78
+ </li>
79
+
80
+ </ul>
81
+ </body>
82
+ </html>
@@ -0,0 +1 @@
1
+ I have spaces in my name
@@ -0,0 +1,42 @@
1
+ {
2
+ "@context": "https://w3id.org/ro/crate/1.3/context",
3
+ "@graph": [
4
+ {
5
+ "@id": "ro-crate-metadata.json",
6
+ "@type": "CreativeWork",
7
+ "about": {
8
+ "@id": "./"
9
+ },
10
+ "conformsTo": {
11
+ "@id": "https://w3id.org/ro/crate/1.3"
12
+ }
13
+ },
14
+ {
15
+ "@id": "ro-crate-preview.html",
16
+ "@type": "CreativeWork",
17
+ "about": {
18
+ "@id": "./"
19
+ }
20
+ },
21
+ {
22
+ "@id": "./",
23
+ "@type": "Dataset",
24
+ "hasPart": [
25
+ {
26
+ "@id": "http://example.com/external_ref.txt"
27
+ },
28
+ {
29
+ "@id": "file%20with%20spaces.txt"
30
+ }
31
+ ]
32
+ },
33
+ {
34
+ "@id": "http://example.com/external_ref.txt",
35
+ "@type": "File"
36
+ },
37
+ {
38
+ "@id": "file%20with%20spaces.txt",
39
+ "@type": "File"
40
+ }
41
+ ]
42
+ }
@@ -0,0 +1,82 @@
1
+
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <title>New RO-Crate</title>
6
+ <script type="application/ld+json">{
7
+ "@context": "https://w3id.org/ro/crate/1.3/context",
8
+ "@graph": [
9
+ {
10
+ "@id": "ro-crate-metadata.json",
11
+ "@type": "CreativeWork",
12
+ "about": {
13
+ "@id": "./"
14
+ },
15
+ "conformsTo": {
16
+ "@id": "https://w3id.org/ro/crate/1.3"
17
+ }
18
+ },
19
+ {
20
+ "@id": "ro-crate-preview.html",
21
+ "@type": "CreativeWork",
22
+ "about": {
23
+ "@id": "./"
24
+ }
25
+ },
26
+ {
27
+ "@id": "./",
28
+ "@type": "Dataset",
29
+ "hasPart": [
30
+ {
31
+ "@id": "http://example.com/external_ref.txt"
32
+ },
33
+ {
34
+ "@id": "file%20with%20spaces.txt"
35
+ }
36
+ ]
37
+ },
38
+ {
39
+ "@id": "http://example.com/external_ref.txt",
40
+ "@type": "File"
41
+ },
42
+ {
43
+ "@id": "file%20with%20spaces.txt",
44
+ "@type": "File"
45
+ }
46
+ ]
47
+ }</script>
48
+ <meta name="generator" content="https://github.com/ResearchObject/ro-crate-ruby">
49
+ <meta name="keywords" content="RO-Crate">
50
+ <meta charset="utf-8">
51
+ </head>
52
+ <body>
53
+ <h1>New RO-Crate</h1>
54
+
55
+ <p>
56
+
57
+ </p>
58
+ <dl>
59
+
60
+
61
+
62
+
63
+ </dl>
64
+
65
+ <h2>Contents</h2>
66
+ <ul>
67
+
68
+ <li>
69
+ <strong><a href="http://example.com/external_ref.txt" target="_blank">http://example.com/external_ref.txt</a></strong>
70
+
71
+
72
+ </li>
73
+
74
+ <li>
75
+ <strong>file%20with%20spaces.txt</strong>
76
+
77
+
78
+ </li>
79
+
80
+ </ul>
81
+ </body>
82
+ </html>
Binary file
Binary file
Binary file
data/test/reader_test.rb CHANGED
@@ -188,6 +188,48 @@ class ReaderTest < Test::Unit::TestCase
188
188
  assert crate.preview.source.source.is_a?(ROCrate::PreviewGenerator)
189
189
  end
190
190
 
191
+ test 'can read a 1.2 spec crate' do
192
+ stub_request(:get, "http://example.com/external_ref.txt").to_return(status: 200, body: 'file contents')
193
+
194
+ crate = ROCrate::Reader.read_directory(fixture_file('crate-spec1.2').path)
195
+ file = crate.dereference('file with spaces.txt')
196
+ assert file
197
+ assert file.is_a?(ROCrate::File)
198
+ refute file.remote?
199
+ assert file.source.is_a?(ROCrate::Entry)
200
+ assert_equal 'file%20with%20spaces.txt', file.id
201
+
202
+ ext_file = crate.dereference('http://example.com/external_ref.txt')
203
+ assert ext_file
204
+ assert ext_file.is_a?(ROCrate::File)
205
+ assert ext_file.remote?
206
+ assert ext_file.source.is_a?(ROCrate::RemoteEntry)
207
+ assert_equal 'http://example.com/external_ref.txt', ext_file.id
208
+ assert_equal 'file contents', ext_file.source.read
209
+ assert crate.preview.source.source.is_a?(Pathname)
210
+ end
211
+
212
+ test 'can read a 1.3 spec crate' do
213
+ stub_request(:get, "http://example.com/external_ref.txt").to_return(status: 200, body: 'file contents')
214
+
215
+ crate = ROCrate::Reader.read_directory(fixture_file('crate-spec1.3').path)
216
+ file = crate.dereference('file with spaces.txt')
217
+ assert file
218
+ assert file.is_a?(ROCrate::File)
219
+ refute file.remote?
220
+ assert file.source.is_a?(ROCrate::Entry)
221
+ assert_equal 'file%20with%20spaces.txt', file.id
222
+
223
+ ext_file = crate.dereference('http://example.com/external_ref.txt')
224
+ assert ext_file
225
+ assert ext_file.is_a?(ROCrate::File)
226
+ assert ext_file.remote?
227
+ assert ext_file.source.is_a?(ROCrate::RemoteEntry)
228
+ assert_equal 'http://example.com/external_ref.txt', ext_file.id
229
+ assert_equal 'file contents', ext_file.source.read
230
+ assert crate.preview.source.source.is_a?(Pathname)
231
+ end
232
+
191
233
  test 'reading from directory with unlisted files' do
192
234
  crate = ROCrate::Reader.read_directory(fixture_file('sparse_directory_crate').path)
193
235
 
@@ -358,6 +400,11 @@ class ReaderTest < Test::Unit::TestCase
358
400
  ROCrate::Reader.read(fixture_file('broken/missing_file'))
359
401
  end
360
402
  assert_include e.message, 'not found in crate: file1.txt'
403
+
404
+ e = check_exception(ROCrate::ReadException) do
405
+ ROCrate::Reader.read(fixture_file('just_a_zip.zip').path)
406
+ end
407
+ assert_include e.message, 'No metadata found'
361
408
  end
362
409
 
363
410
  test 'tolerates arcp identifier on root data entity (and missing hasPart)' do
@@ -381,18 +428,56 @@ class ReaderTest < Test::Unit::TestCase
381
428
  assert_equal 'a_file', data.first.name
382
429
  end
383
430
 
384
- private
431
+ test 'protect against zip-slip' do
432
+ Dir.mktmpdir do |dir|
433
+ subdir = ::File.join(dir, 'subdir')
434
+ ::Dir.mkdir(subdir)
435
+
436
+ # Relative
437
+ e = check_exception(ROCrate::ReadException) do
438
+ ROCrate::Reader.unzip_file_to(fixture_file('unsafe/relative0.zip').path, subdir)
439
+ end
440
+ assert_include e.message, 'Unsafe path in zip entry: ../moo'
441
+ refute ::File.exist?(::File.join(dir, 'moo'))
442
+
443
+ e = check_exception(ROCrate::ReadException) do
444
+ ROCrate::Reader.unzip_io_to(fixture_file('unsafe/relative0.zip'), subdir)
445
+ end
446
+ assert_include e.message, 'Unsafe path in zip entry: ../moo'
447
+ refute ::File.exist?(::File.join(dir, 'moo'))
448
+
449
+ # Absolute
450
+ e = check_exception(ROCrate::ReadException) do
451
+ ROCrate::Reader.unzip_file_to(fixture_file('unsafe/absolute1.zip').path, subdir)
452
+ end
453
+ assert_include e.message, 'Unsafe path in zip entry: /tmp/moo'
385
454
 
386
- def check_exception(exception_class)
387
- e = nil
388
- assert_raise(exception_class) do
455
+ e = check_exception(ROCrate::ReadException) do
456
+ ROCrate::Reader.unzip_io_to(fixture_file('unsafe/absolute1.zip'), subdir)
457
+ end
458
+ assert_include e.message, 'Unsafe path in zip entry: /tmp/moo'
459
+
460
+ # Simulate ArgumentError in safe_join
389
461
  begin
390
- yield
391
- rescue exception_class => e
392
- raise e
462
+ original_expand_path = Pathname.instance_method(:expand_path)
463
+ Pathname.define_method(:expand_path) do |*args|
464
+ raise ArgumentError, 'Oh no'
465
+ end
466
+ e = check_exception(ROCrate::ReadException) do
467
+ ROCrate::Reader.unzip_file_to(fixture_file('unsafe/absolute1.zip').path, subdir)
468
+ end
469
+ assert_include e.message, 'Unsafe path in zip entry: /tmp/moo'
470
+ ensure
471
+ Pathname.define_method(:expand_path, original_expand_path)
393
472
  end
394
473
  end
474
+ end
475
+
476
+ test 'reads spec 1.1 RO-Crate and preserves version' do
477
+ crate = ROCrate::Reader.read(fixture_file('crate-spec1.1').path)
395
478
 
396
- e
479
+ assert_equal '1.1', crate.metadata.version
480
+ assert_equal 'https://w3id.org/ro/crate/1.1', crate.metadata.spec_url
481
+ assert_equal 'https://w3id.org/ro/crate/1.1/context', crate.metadata.context_url
397
482
  end
398
483
  end
data/test/test_helper.rb CHANGED
@@ -5,10 +5,37 @@ require 'test/unit'
5
5
  require 'ro_crate'
6
6
  require 'webmock/test_unit'
7
7
 
8
+ class Test::Unit::TestCase
9
+ def teardown
10
+ self._opened_files.each do |f|
11
+ f.close unless f.closed?
12
+ end
13
+ end
14
+
15
+ def _opened_files
16
+ @opened_files ||= []
17
+ end
18
+ end
19
+
8
20
  def fixture_file(name, *args)
9
- ::File.open(::File.join(fixture_dir, name), *args)
21
+ f = ::File.open(::File.join(fixture_dir, name), *args)
22
+ self._opened_files << f
23
+ f
10
24
  end
11
25
 
12
26
  def fixture_dir
13
27
  ::File.join(::File.dirname(__FILE__), 'fixtures')
14
28
  end
29
+
30
+ def check_exception(exception_class)
31
+ e = nil
32
+ assert_raise(exception_class) do
33
+ begin
34
+ yield
35
+ rescue exception_class => e
36
+ raise e
37
+ end
38
+ end
39
+
40
+ e
41
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ro-crate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Finn Bacall
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-01-30 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: addressable
@@ -19,7 +18,7 @@ dependencies:
19
18
  version: '2.7'
20
19
  - - "<"
21
20
  - !ruby/object:Gem::Version
22
- version: '2.9'
21
+ version: '3'
23
22
  type: :runtime
24
23
  prerelease: false
25
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,35 +28,41 @@ dependencies:
29
28
  version: '2.7'
30
29
  - - "<"
31
30
  - !ruby/object:Gem::Version
32
- version: '2.9'
31
+ version: '3'
33
32
  - !ruby/object:Gem::Dependency
34
33
  name: rubyzip
35
34
  requirement: !ruby/object:Gem::Requirement
36
35
  requirements:
37
- - - "~>"
36
+ - - ">="
38
37
  - !ruby/object:Gem::Version
39
- version: 2.0.0
38
+ version: '2.3'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '4'
40
42
  type: :runtime
41
43
  prerelease: false
42
44
  version_requirements: !ruby/object:Gem::Requirement
43
45
  requirements:
44
- - - "~>"
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '2.3'
49
+ - - "<"
45
50
  - !ruby/object:Gem::Version
46
- version: 2.0.0
51
+ version: '4'
47
52
  - !ruby/object:Gem::Dependency
48
53
  name: rake
49
54
  requirement: !ruby/object:Gem::Requirement
50
55
  requirements:
51
56
  - - "~>"
52
57
  - !ruby/object:Gem::Version
53
- version: 13.0.0
58
+ version: 13.4.2
54
59
  type: :development
55
60
  prerelease: false
56
61
  version_requirements: !ruby/object:Gem::Requirement
57
62
  requirements:
58
63
  - - "~>"
59
64
  - !ruby/object:Gem::Version
60
- version: 13.0.0
65
+ version: 13.4.2
61
66
  - !ruby/object:Gem::Dependency
62
67
  name: test-unit
63
68
  requirement: !ruby/object:Gem::Requirement
@@ -106,29 +111,28 @@ dependencies:
106
111
  requirements:
107
112
  - - "~>"
108
113
  - !ruby/object:Gem::Version
109
- version: 3.8.3
114
+ version: 3.26.2
110
115
  type: :development
111
116
  prerelease: false
112
117
  version_requirements: !ruby/object:Gem::Requirement
113
118
  requirements:
114
119
  - - "~>"
115
120
  - !ruby/object:Gem::Version
116
- version: 3.8.3
121
+ version: 3.26.2
117
122
  - !ruby/object:Gem::Dependency
118
123
  name: rexml
119
124
  requirement: !ruby/object:Gem::Requirement
120
125
  requirements:
121
126
  - - "~>"
122
127
  - !ruby/object:Gem::Version
123
- version: 3.2.5
128
+ version: 3.4.4
124
129
  type: :development
125
130
  prerelease: false
126
131
  version_requirements: !ruby/object:Gem::Requirement
127
132
  requirements:
128
133
  - - "~>"
129
134
  - !ruby/object:Gem::Version
130
- version: 3.2.5
131
- description:
135
+ version: 3.4.4
132
136
  email: finn.bacall@manchester.ac.uk
133
137
  executables: []
134
138
  extensions: []
@@ -181,6 +185,12 @@ files:
181
185
  - test/fixtures/conflicting_data_directory/nested.txt
182
186
  - test/fixtures/crate-spec1.1/file with spaces.txt
183
187
  - test/fixtures/crate-spec1.1/ro-crate-metadata.json
188
+ - test/fixtures/crate-spec1.2/file with spaces.txt
189
+ - test/fixtures/crate-spec1.2/ro-crate-metadata.json
190
+ - test/fixtures/crate-spec1.2/ro-crate-preview.html
191
+ - test/fixtures/crate-spec1.3/file with spaces.txt
192
+ - test/fixtures/crate-spec1.3/ro-crate-metadata.json
193
+ - test/fixtures/crate-spec1.3/ro-crate-preview.html
184
194
  - test/fixtures/data.csv
185
195
  - test/fixtures/dir_symlink
186
196
  - test/fixtures/directory.zip
@@ -200,6 +210,7 @@ files:
200
210
  - test/fixtures/directory_crate/ro-crate-preview.html
201
211
  - test/fixtures/file with spaces.txt
202
212
  - test/fixtures/info.txt
213
+ - test/fixtures/just_a_zip.zip
203
214
  - test/fixtures/misc_data_entity_crate/ro-crate-metadata.json
204
215
  - test/fixtures/multi_metadata_crate.crate.zip
205
216
  - test/fixtures/nested_directory.zip
@@ -232,6 +243,8 @@ files:
232
243
  - test/fixtures/unlinked_entity_crate/test/test1/input.bed
233
244
  - test/fixtures/unlinked_entity_crate/test/test1/output_exp.bed
234
245
  - test/fixtures/unlinked_entity_crate/test/test1/sort-and-change-case-test.yml
246
+ - test/fixtures/unsafe/absolute1.zip
247
+ - test/fixtures/unsafe/relative0.zip
235
248
  - test/fixtures/uri_heavy_crate/main.nf
236
249
  - test/fixtures/uri_heavy_crate/ro-crate-metadata.json
237
250
  - test/fixtures/uri_heavy_crate/ro-crate-preview.html
@@ -2473,7 +2486,6 @@ homepage: https://github.com/ResearchObject/ro-crate-ruby
2473
2486
  licenses:
2474
2487
  - MIT
2475
2488
  metadata: {}
2476
- post_install_message:
2477
2489
  rdoc_options: []
2478
2490
  require_paths:
2479
2491
  - lib
@@ -2481,15 +2493,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
2481
2493
  requirements:
2482
2494
  - - ">="
2483
2495
  - !ruby/object:Gem::Version
2484
- version: '0'
2496
+ version: 2.7.0
2485
2497
  required_rubygems_version: !ruby/object:Gem::Requirement
2486
2498
  requirements:
2487
2499
  - - ">="
2488
2500
  - !ruby/object:Gem::Version
2489
2501
  version: '0'
2490
2502
  requirements: []
2491
- rubygems_version: 3.4.19
2492
- signing_key:
2503
+ rubygems_version: 3.6.9
2493
2504
  specification_version: 4
2494
2505
  summary: Create, manipulate, read RO-Crates.
2495
2506
  test_files: []