combine_pdf 1.0.25 → 1.0.27
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +6 -2
- data/CHANGELOG.md +5 -1
- data/README.md +10 -10
- data/lib/combine_pdf/api.rb +1 -0
- data/lib/combine_pdf/basic_writer.rb +1 -0
- data/lib/combine_pdf/decrypt.rb +2 -1
- data/lib/combine_pdf/exceptions.rb +2 -0
- data/lib/combine_pdf/filter.rb +1 -0
- data/lib/combine_pdf/fonts.rb +1 -0
- data/lib/combine_pdf/page_methods.rb +5 -4
- data/lib/combine_pdf/parser.rb +9 -9
- data/lib/combine_pdf/pdf_protected.rb +21 -19
- data/lib/combine_pdf/pdf_public.rb +5 -4
- data/lib/combine_pdf/renderer.rb +3 -2
- data/lib/combine_pdf/version.rb +3 -1
- data/lib/combine_pdf.rb +1 -0
- data/test/combine_pdf/renderer_test.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2ccb4f95b5400a214f06d343f8b0a5b1a7a38b94ce400f2bbe80cefccd902005
|
4
|
+
data.tar.gz: a923391ccc4a37ccf77b89a5113e723d1065e09f0404f0e9c5104034d679363e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc63f8c2c83a90a23579a517314248924207b578fc713c520c0ac4596c214cc2c7df50a173638e0659f58e62ae19ae0958b72eb29b6ed29f27d6ea65b9cdf88f
|
7
|
+
data.tar.gz: 840b62aa7abda71f22c4b83b697046a666043b73d063f5f60046cb76ed9d6f97086d4e7dc9645ece7ef97184d6e38523d90376ba9d55f9138e0b68519ce8b55e
|
data/.github/workflows/main.yml
CHANGED
@@ -9,7 +9,11 @@ jobs:
|
|
9
9
|
strategy:
|
10
10
|
fail-fast: false
|
11
11
|
matrix:
|
12
|
-
ruby: ["2.7", "3.0", "3.1", "3.2"]
|
12
|
+
ruby: ["2.7", "3.0", "3.1", "3.2", "3.3"]
|
13
|
+
rubyopt: [""]
|
14
|
+
include:
|
15
|
+
- ruby: "3.3"
|
16
|
+
rubyopt: "--enable-frozen-string-literal --debug-frozen-string-literal"
|
13
17
|
|
14
18
|
steps:
|
15
19
|
- name: Checkout code
|
@@ -25,4 +29,4 @@ jobs:
|
|
25
29
|
run: bundle lock
|
26
30
|
|
27
31
|
- name: Run tests
|
28
|
-
run: bundle exec rake test
|
32
|
+
run: bundle exec rake test RUBYOPT="${{ matrix.rubyopt }}"
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
#### Change log v.1.0.26 (2023-12-22)
|
4
|
+
|
5
|
+
**Performance**: possible performance bump. Credit to @denislavski (Denislav Naydenov) for opening PR #235.
|
6
|
+
|
3
7
|
#### Change log v.1.0.25 (2023-12-19)
|
4
8
|
|
5
|
-
**Fix**: possible improve memory usage. Credit to @denislavski (Denislav Naydenov) for opening PR #
|
9
|
+
**Fix**: possible improve memory usage. Credit to @denislavski (Denislav Naydenov) for opening PR #233 and suggesting this change.
|
6
10
|
|
7
11
|
#### Change log v.1.0.24 (2023-10-19)
|
8
12
|
|
data/README.md
CHANGED
@@ -7,23 +7,23 @@
|
|
7
7
|
|
8
8
|
CombinePDF is a nifty model, written in pure Ruby, to parse PDF files and combine (merge) them with other PDF files, watermark them or stamp them (all using the PDF file format and pure Ruby code).
|
9
9
|
|
10
|
-
##
|
10
|
+
## Unmaintained - Help Wanted(!)
|
11
11
|
|
12
|
-
|
12
|
+
I decided to stop maintaining this gem and hope someone could take over the PR reviews and maintenance of this gem (or simply open a successful fork).
|
13
13
|
|
14
|
-
|
15
|
-
gem install combine_pdf
|
16
|
-
```
|
14
|
+
I wrote this gem because I needed to solve an issue with bates-numbering existing PDF documents.
|
17
15
|
|
18
|
-
|
16
|
+
However, since 2014 I have been maintaining the gem for free and for no reason at all, except that I enjoyed sharing it with the community.
|
19
17
|
|
20
|
-
I
|
18
|
+
I love this gem, but I cannot keep maintaining it as I have my own projects to focus own and I need both the time and (more importantly) the mindspace.
|
21
19
|
|
22
|
-
|
20
|
+
## Install
|
23
21
|
|
24
|
-
|
22
|
+
Install with ruby gems:
|
25
23
|
|
26
|
-
|
24
|
+
```ruby
|
25
|
+
gem install combine_pdf
|
26
|
+
```
|
27
27
|
|
28
28
|
## Known Limitations
|
29
29
|
|
data/lib/combine_pdf/api.rb
CHANGED
data/lib/combine_pdf/decrypt.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
2
3
|
########################################################
|
3
4
|
## Thoughts from reading the ISO 32000-1:2008
|
4
5
|
## this file is part of the CombinePDF library and the code
|
@@ -137,7 +138,7 @@ module CombinePDF
|
|
137
138
|
object_key = @key.dup
|
138
139
|
object_key << [encrypted_id].pack('i')[0..2]
|
139
140
|
object_key << [encrypted_generation].pack('i')[0..1]
|
140
|
-
object_key << 'sAlT'.
|
141
|
+
object_key << 'sAlT'.b
|
141
142
|
key_length = object_key.length < 16 ? object_key.length : 16
|
142
143
|
|
143
144
|
begin
|
data/lib/combine_pdf/filter.rb
CHANGED
data/lib/combine_pdf/fonts.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
2
3
|
########################################################
|
3
4
|
## Thoughts from reading the ISO 32000-1:2008
|
4
5
|
## this file is part of the CombinePDF library and the code
|
@@ -214,7 +215,7 @@ module CombinePDF
|
|
214
215
|
options[:text_padding] = 0 if options[:text_padding].to_f >= 1
|
215
216
|
|
216
217
|
# create box stream
|
217
|
-
box_stream = ''
|
218
|
+
box_stream = +''
|
218
219
|
# set graphic state for box
|
219
220
|
if options[:box_color] || (options[:border_width].to_i > 0 && options[:border_color])
|
220
221
|
# compute x and y position for text
|
@@ -290,7 +291,7 @@ module CombinePDF
|
|
290
291
|
# reset x,y by text alignment - x,y are calculated from the bottom left
|
291
292
|
# each unit (1) is 1/72 Inch
|
292
293
|
# create text stream
|
293
|
-
text_stream = ''
|
294
|
+
text_stream = +''
|
294
295
|
if !text.to_s.empty? && options[:font_size] != 0 && (options[:font_color] || options[:stroke_color])
|
295
296
|
# compute x and y position for text
|
296
297
|
x = options[:x] + (options[:width] * options[:text_padding])
|
@@ -679,7 +680,7 @@ module CombinePDF
|
|
679
680
|
insert_content 'Q'
|
680
681
|
|
681
682
|
# Prep content
|
682
|
-
@contents = ''
|
683
|
+
@contents = +''
|
683
684
|
insert_content @contents
|
684
685
|
@contents
|
685
686
|
end
|
@@ -788,7 +789,7 @@ module CombinePDF
|
|
788
789
|
# add to array
|
789
790
|
if out.last.nil? || out.last[0] != fonts[i]
|
790
791
|
out.last[1] << '>' unless out.last.nil?
|
791
|
-
out << [fonts[i], '<', 0, 0]
|
792
|
+
out << [fonts[i], (+'<'), 0, 0]
|
792
793
|
end
|
793
794
|
out.last[1] << (fonts_array[i].cmap.nil? ? (c.unpack('H*')[0]) : fonts_array[i].cmap[c])
|
794
795
|
if fonts_array[i].metrics[c]
|
data/lib/combine_pdf/parser.rb
CHANGED
@@ -262,7 +262,7 @@ module CombinePDF
|
|
262
262
|
##########################################
|
263
263
|
elsif @scanner.scan(/\(/)
|
264
264
|
# warn "Found a literal string"
|
265
|
-
str = ''.
|
265
|
+
str = ''.b
|
266
266
|
count = 1
|
267
267
|
while count > 0 && @scanner.rest?
|
268
268
|
scn = @scanner.scan_until(/[\(\)]/)
|
@@ -369,7 +369,7 @@ module CombinePDF
|
|
369
369
|
# the following was dicarded because some PDF files didn't have an EOL marker as required
|
370
370
|
# str = @scanner.scan_until(/(\r\n|\r|\n)endstream/)
|
371
371
|
# instead, a non-strict RegExp is used:
|
372
|
-
|
372
|
+
|
373
373
|
|
374
374
|
# raise error if the stream doesn't end.
|
375
375
|
unless @scanner.skip_until(/endstream/)
|
@@ -379,7 +379,7 @@ module CombinePDF
|
|
379
379
|
length = 0 if(length < 0)
|
380
380
|
length -= 1 if(@scanner.string[old_pos + length - 1] == "\n")
|
381
381
|
length -= 1 if(@scanner.string[old_pos + length - 1] == "\r")
|
382
|
-
str = (length > 0) ? @scanner.string.slice(old_pos, length) : ''
|
382
|
+
str = (length > 0) ? @scanner.string.slice(old_pos, length) : +''
|
383
383
|
|
384
384
|
# warn "CombinePDF parser: detected Stream #{str.length} bytes long #{str[0..3]}...#{str[-4..-1]}"
|
385
385
|
|
@@ -632,17 +632,17 @@ module CombinePDF
|
|
632
632
|
#
|
633
633
|
def serialize_objects_and_references
|
634
634
|
obj_dir = {}
|
635
|
-
objid_cache = {}
|
635
|
+
objid_cache = {}.compare_by_identity
|
636
636
|
# create a dictionary for referenced objects (no value resolution at this point)
|
637
637
|
# at the same time, delete duplicates and old versions when objects have multiple versions
|
638
638
|
@parsed.uniq!
|
639
639
|
@parsed.length.times do |i|
|
640
640
|
o = @parsed[i]
|
641
|
-
objid_cache[o
|
641
|
+
objid_cache[o] = i
|
642
642
|
tmp_key = [o[:indirect_reference_id], o[:indirect_generation_number]]
|
643
643
|
if tmp_found = obj_dir[tmp_key]
|
644
644
|
tmp_found.clear
|
645
|
-
@parsed[objid_cache[tmp_found
|
645
|
+
@parsed[objid_cache[tmp_found]] = nil
|
646
646
|
end
|
647
647
|
obj_dir[tmp_key] = o
|
648
648
|
end
|
@@ -765,9 +765,9 @@ module CombinePDF
|
|
765
765
|
# end
|
766
766
|
|
767
767
|
# # run block of code on evey PDF object (PDF objects are class Hash)
|
768
|
-
# def each_object(object, limit_references = true, already_visited = {}, &block)
|
768
|
+
# def each_object(object, limit_references = true, already_visited = {}.compare_by_identity, &block)
|
769
769
|
# unless limit_references
|
770
|
-
# already_visited[object
|
770
|
+
# already_visited[object] = true
|
771
771
|
# end
|
772
772
|
# case
|
773
773
|
# when object.is_a?(Array)
|
@@ -776,7 +776,7 @@ module CombinePDF
|
|
776
776
|
# yield(object)
|
777
777
|
# unless limit_references && object[:is_reference_only]
|
778
778
|
# object.each do |k,v|
|
779
|
-
# each_object(v, limit_references, already_visited, &block) unless already_visited[v
|
779
|
+
# each_object(v, limit_references, already_visited, &block) unless already_visited[v]
|
780
780
|
# end
|
781
781
|
# end
|
782
782
|
# end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
2
3
|
########################################################
|
3
4
|
## Thoughts from reading the ISO 32000-1:2008
|
4
5
|
## this file is part of the CombinePDF library and the code
|
@@ -21,19 +22,19 @@ module CombinePDF
|
|
21
22
|
# this is used for internal operations, such as injectng data using the << operator.
|
22
23
|
def add_referenced()
|
23
24
|
# an existing object map
|
24
|
-
resolved = {}.
|
25
|
-
existing = {}
|
26
|
-
should_resolve = []
|
25
|
+
resolved = {}.compare_by_identity
|
26
|
+
existing = {}
|
27
|
+
should_resolve = []
|
27
28
|
#set all existing objects as resolved and register their children for future resolution
|
28
|
-
@objects.each { |obj| existing[obj] = obj ; resolved[obj
|
29
|
+
@objects.each { |obj| existing[obj] = obj ; resolved[obj] = obj; should_resolve << obj.values}
|
29
30
|
# loop until should_resolve is empty
|
30
31
|
while should_resolve.any?
|
31
32
|
obj = should_resolve.pop
|
32
|
-
next if resolved[obj
|
33
|
+
next if resolved[obj] # the object exists
|
33
34
|
if obj.is_a?(Hash)
|
34
35
|
referenced = obj[:referenced_object]
|
35
36
|
if referenced && referenced.any?
|
36
|
-
tmp = resolved[referenced
|
37
|
+
tmp = resolved[referenced]
|
37
38
|
if !tmp && referenced[:raw_stream_content]
|
38
39
|
tmp = existing[referenced[:raw_stream_content]]
|
39
40
|
# Avoid endless recursion by limiting it to a number of layers (default == 2)
|
@@ -42,18 +43,18 @@ module CombinePDF
|
|
42
43
|
if tmp
|
43
44
|
obj[:referenced_object] = tmp
|
44
45
|
else
|
45
|
-
resolved[obj
|
46
|
+
resolved[obj] = referenced
|
46
47
|
# existing[referenced] = referenced
|
47
48
|
existing[referenced[:raw_stream_content]] = referenced
|
48
49
|
should_resolve << referenced
|
49
50
|
@objects << referenced
|
50
51
|
end
|
51
52
|
else
|
52
|
-
resolved[obj
|
53
|
-
obj.keys.each { |k| should_resolve << obj[k] unless !obj[k].is_a?(Enumerable) || resolved[obj[k]
|
53
|
+
resolved[obj] = obj
|
54
|
+
obj.keys.each { |k| should_resolve << obj[k] unless !obj[k].is_a?(Enumerable) || resolved[obj[k]] }
|
54
55
|
end
|
55
56
|
elsif obj.is_a?(Array)
|
56
|
-
resolved[obj
|
57
|
+
resolved[obj] = obj
|
57
58
|
should_resolve.concat obj
|
58
59
|
end
|
59
60
|
end
|
@@ -78,14 +79,14 @@ module CombinePDF
|
|
78
79
|
page_list.concat(with_pages) unless with_pages.empty?
|
79
80
|
|
80
81
|
# duplicate any non-unique pages - This is a special case to resolve Adobe Acrobat Reader issues (see issues #19 and #81)
|
81
|
-
uniqueness = {}.
|
82
|
-
page_list.each { |page| page = page[:referenced_object] || page; page = page.dup if uniqueness[page
|
82
|
+
uniqueness = {}.compare_by_identity
|
83
|
+
page_list.each { |page| page = page[:referenced_object] || page; page = page.dup if uniqueness[page]; uniqueness[page] = page }
|
83
84
|
page_list.clear
|
84
85
|
page_list = uniqueness.values
|
85
86
|
uniqueness.clear
|
86
87
|
|
87
88
|
# build new Pages object
|
88
|
-
page_object_kids = []
|
89
|
+
page_object_kids = []
|
89
90
|
pages_object = { Type: :Pages, Count: page_list.length, Kids: page_object_kids }
|
90
91
|
pages_object_reference = { referenced_object: pages_object, is_reference_only: true }
|
91
92
|
page_list.each { |pg| pg[:Parent] = pages_object_reference; page_object_kids << ({ referenced_object: pg, is_reference_only: true }) }
|
@@ -186,17 +187,18 @@ module CombinePDF
|
|
186
187
|
POSSIBLE_NAME_TREES = [:Dests, :AP, :Pages, :IDS, :Templates, :URLS, :JavaScript, :EmbeddedFiles, :AlternatePresentations, :Renditions].to_set.freeze
|
187
188
|
|
188
189
|
def rebuild_names(name_tree = nil, base = 'CombinePDF_0000000')
|
190
|
+
base = +base
|
189
191
|
if name_tree
|
190
192
|
return nil unless name_tree.is_a?(Hash)
|
191
193
|
name_tree = name_tree[:referenced_object] || name_tree
|
192
194
|
dic = []
|
193
195
|
# map a names tree and return a valid name tree. Do not recourse.
|
194
196
|
should_resolve = [name_tree[:Kids], name_tree[:Names]]
|
195
|
-
resolved =
|
197
|
+
resolved = Set.new.compare_by_identity
|
196
198
|
while should_resolve.any?
|
197
199
|
pos = should_resolve.pop
|
198
200
|
if pos.is_a? Array
|
199
|
-
next if resolved.include?(pos
|
201
|
+
next if resolved.include?(pos)
|
200
202
|
if pos[0].is_a? String
|
201
203
|
(pos.length / 2).times do |i|
|
202
204
|
dic << (pos[i * 2].clear << base.next!)
|
@@ -209,16 +211,16 @@ module CombinePDF
|
|
209
211
|
end
|
210
212
|
elsif pos.is_a? Hash
|
211
213
|
pos = pos[:referenced_object] || pos
|
212
|
-
next if resolved.include?(pos
|
214
|
+
next if resolved.include?(pos)
|
213
215
|
should_resolve << pos[:Kids] if pos[:Kids]
|
214
216
|
should_resolve << pos[:Names] if pos[:Names]
|
215
217
|
end
|
216
|
-
resolved << pos
|
218
|
+
resolved << pos
|
217
219
|
end
|
218
220
|
return { referenced_object: { Names: dic }, is_reference_only: true }
|
219
221
|
end
|
220
222
|
@names ||= @names[:referenced_object]
|
221
|
-
new_names = { Type: :Names }
|
223
|
+
new_names = { Type: :Names }
|
222
224
|
POSSIBLE_NAME_TREES.each do |ntree|
|
223
225
|
if @names[ntree]
|
224
226
|
new_names[ntree] = rebuild_names(@names[ntree], base)
|
@@ -373,7 +375,7 @@ module CombinePDF
|
|
373
375
|
private
|
374
376
|
|
375
377
|
def equal_layers obj1, obj2, layer = CombinePDF.eq_depth_limit
|
376
|
-
return true if obj1.
|
378
|
+
return true if obj1.equal?(obj2)
|
377
379
|
if obj1.is_a? Hash
|
378
380
|
return false unless obj2.is_a? Hash
|
379
381
|
return false unless obj1.length == obj2.length
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
|
-
|
2
|
+
## frozen_string_literal: true
|
3
|
+
#######################################################
|
3
4
|
## Thoughts from reading the ISO 32000-1:2008
|
4
5
|
## this file is part of the CombinePDF library and the code
|
5
6
|
## is subject to the same license.
|
@@ -93,7 +94,7 @@ module CombinePDF
|
|
93
94
|
@version = 0
|
94
95
|
@viewer_preferences = {}
|
95
96
|
@info = {}
|
96
|
-
parser ||= PDFParser.new('')
|
97
|
+
parser ||= PDFParser.new(+'')
|
97
98
|
raise TypeError, "initialization error, expecting CombinePDF::PDFParser or nil, but got #{parser.class.name}" unless parser.is_a? PDFParser
|
98
99
|
@objects = parser.parse
|
99
100
|
|
@@ -207,7 +208,7 @@ module CombinePDF
|
|
207
208
|
# xref_location = 0
|
208
209
|
# out.each { |line| xref_location += line.bytesize + 1}
|
209
210
|
out << "xref\n0 #{indirect_object_count}\n0000000000 65535 f "
|
210
|
-
xref.each { |offset| out << ("%010d 00000 n " % offset) }
|
211
|
+
xref.each { |offset| out << ("%010d 00000 n ".freeze % offset) }
|
211
212
|
out << 'trailer'.freeze
|
212
213
|
out << "<<\n/Root #{false || "#{catalog[:indirect_reference_id]} #{catalog[:indirect_generation_number]} R"}"
|
213
214
|
out << "/Size #{indirect_object_count}"
|
@@ -216,7 +217,7 @@ module CombinePDF
|
|
216
217
|
# when finished, remove the numbering system and keep only pointers
|
217
218
|
remove_old_ids
|
218
219
|
# output the pdf stream
|
219
|
-
out.join("\n".
|
220
|
+
out.join("\n".b).force_encoding(Encoding::ASCII_8BIT)
|
220
221
|
end
|
221
222
|
|
222
223
|
# this method returns all the pages cataloged in the catalog.
|
data/lib/combine_pdf/renderer.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module CombinePDF
|
2
3
|
################################################################
|
3
4
|
## These are common functions, used within the different classes
|
@@ -123,12 +124,12 @@ module CombinePDF
|
|
123
124
|
# if the object is not a simple object, it is a dictionary
|
124
125
|
# A dictionary shall be written as a sequence of key-value pairs enclosed in double angle brackets (<<...>>)
|
125
126
|
# (using LESS-THAN SIGNs (3Ch) and GREATER-THAN SIGNs (3Eh)).
|
126
|
-
out << "<<\n".
|
127
|
+
out << "<<\n".b
|
127
128
|
object.each do |key, value|
|
128
129
|
out << "#{object_to_pdf key} #{object_to_pdf value}\n".force_encoding(Encoding::ASCII_8BIT) unless PDF::PRIVATE_HASH_KEYS.include? key
|
129
130
|
end
|
130
131
|
object.delete :Length
|
131
|
-
out << '>>'.
|
132
|
+
out << '>>'.b
|
132
133
|
out << "\nstream\n#{object[:raw_stream_content]}\nendstream".force_encoding(Encoding::ASCII_8BIT) if object[:raw_stream_content]
|
133
134
|
out << "\nendobj\n" if object[:indirect_reference_id]
|
134
135
|
out.join.force_encoding(Encoding::ASCII_8BIT)
|
data/lib/combine_pdf/version.rb
CHANGED
data/lib/combine_pdf.rb
CHANGED
@@ -12,7 +12,7 @@ class CombinePDFRendererTest < Minitest::Test
|
|
12
12
|
|
13
13
|
def test_numeric_array_to_pdf
|
14
14
|
input = [1.234567, 0.000054, 5, -0.000099]
|
15
|
-
expected = "[1.234567 0.000054 5 -0.000099]".
|
15
|
+
expected = "[1.234567 0.000054 5 -0.000099]".b
|
16
16
|
actual = TestRenderer.new.test_object(input)
|
17
17
|
|
18
18
|
assert_equal(expected, actual)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: combine_pdf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.27
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Boaz Segev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-11-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruby-rc4
|