webloc 0.3.1 → 0.4.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 +4 -4
- data/CHANGELOG.md +15 -0
- data/LICENSE +1 -1
- data/README.md +26 -32
- data/lib/webloc/binary_plist.rb +159 -0
- data/lib/webloc/version.rb +1 -1
- data/lib/webloc.rb +9 -71
- metadata +8 -15
- data/.gitignore +0 -4
- data/Gemfile +0 -4
- data/Rakefile +0 -11
- data/test/oldstyle.webloc +0 -0
- data/test/pliststyle.webloc +0 -8
- data/test/webloc_test.rb +0 -130
- data/webloc.gemspec +0 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 246849e44bba8ffbcd81036265a4cc813aa963dccf5cb092cb8551268f83b276
|
|
4
|
+
data.tar.gz: 833a7e93bc666b75ce56cfe58619229884eea236fda992a2a660647315eb093c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37d9c62d25c198f4e11b56f826c229def950817d1d28733779093ed97e9e0c389db4985abebbc4712b49171f7df9e94c3748a399c0783c5b60249f135356a649
|
|
7
|
+
data.tar.gz: 949d852b749d043290c2462b33aeebe18737d9845a06d588d621d4f2260c3e54dcf02da9aa21502cdacf32ce2fc341fdbaf7cc673862e308056cba9b50228752
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- Rewrite binary plist reading to follow object references and offsets, supporting
|
|
6
|
+
short URLs, additional metadata, and different object orders.
|
|
7
|
+
- Fix binary output for long URLs and support lengths beyond 65,535 characters.
|
|
8
|
+
- Add Unicode URL support, writing UTF-16 strings and reading them as UTF-8.
|
|
9
|
+
- Recover historical long-URL files affected by earlier versions' offset bugs.
|
|
10
|
+
- Reject malformed files with a `WeblocError` subclass instead of returning
|
|
11
|
+
partial URLs. Public methods and error classes remain unchanged.
|
|
12
|
+
- Require Ruby 3.0 or newer.
|
|
13
|
+
- Retain XML support, the original tests and fixtures, and the existing binary
|
|
14
|
+
encoding for short ASCII URLs. Add README example tests, independent plist
|
|
15
|
+
interoperability checks, and CI coverage.
|
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
*webloc* is a Ruby library that can read from and write to <tt>.webloc</tt> files as used on macOS. These are a variant of 'plist' format files, specifically used for storing links to URLs.
|
|
4
4
|
|
|
5
|
-
It works on Ruby
|
|
5
|
+
It works on Ruby 3.0 and up (including Ruby 4.0.2 and TruffleRuby). It reads XML and binary plist files and writes binary plist files, supporting Unicode and URLs longer than 65,535 characters.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -14,45 +14,18 @@ It works on Ruby 2.7 and up, including Ruby 3.x, and supports URLs of up to 2048
|
|
|
14
14
|
|
|
15
15
|
Reading a .webloc file:
|
|
16
16
|
|
|
17
|
+
require 'webloc'
|
|
17
18
|
webloc = Webloc.load('bookmark.webloc')
|
|
18
19
|
puts webloc.url
|
|
19
20
|
# => "https://example.com"
|
|
20
21
|
|
|
21
22
|
Writing to a .webloc file:
|
|
22
23
|
|
|
24
|
+
require 'webloc'
|
|
23
25
|
Webloc.new('https://rubyweekly.com/').save('rubyweekly.webloc')
|
|
24
26
|
|
|
25
27
|
### Advanced Examples
|
|
26
28
|
|
|
27
|
-
#### Processing multiple .webloc files
|
|
28
|
-
|
|
29
|
-
```ruby
|
|
30
|
-
require 'webloc'
|
|
31
|
-
|
|
32
|
-
Dir.glob('*.webloc').each do |file|
|
|
33
|
-
webloc = Webloc.load(file)
|
|
34
|
-
puts "#{file}: #{webloc.url}"
|
|
35
|
-
end
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
#### Creating webloc files from a list of URLs
|
|
39
|
-
|
|
40
|
-
```ruby
|
|
41
|
-
require 'webloc'
|
|
42
|
-
|
|
43
|
-
urls = [
|
|
44
|
-
'https://github.com',
|
|
45
|
-
'https://stackoverflow.com',
|
|
46
|
-
'https://ruby-lang.org'
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
urls.each_with_index do |url, index|
|
|
50
|
-
filename = "bookmark_#{index + 1}.webloc"
|
|
51
|
-
Webloc.new(url).save(filename)
|
|
52
|
-
puts "Created #{filename}"
|
|
53
|
-
end
|
|
54
|
-
```
|
|
55
|
-
|
|
56
29
|
#### Error handling
|
|
57
30
|
|
|
58
31
|
```ruby
|
|
@@ -118,6 +91,27 @@ json_content = JSON.parse(File.read('bookmark.json'))
|
|
|
118
91
|
Webloc.new(json_content['url']).save('restored.webloc')
|
|
119
92
|
```
|
|
120
93
|
|
|
94
|
+
## Compatibility
|
|
95
|
+
|
|
96
|
+
The public methods and error classes are unchanged. The reader also recovers
|
|
97
|
+
historical long-URL files with the known offset bugs. New writes correct those
|
|
98
|
+
offsets; short ASCII URLs retain their original binary encoding.
|
|
99
|
+
Binary reading supports short URLs, additional metadata, and different object orders.
|
|
100
|
+
URLs are stored as supplied, without escaping or scheme validation. Unicode
|
|
101
|
+
strings use UTF-16 in binary files and are returned as UTF-8 when read.
|
|
102
|
+
|
|
103
|
+
Malformed files without a recoverable historical layout raise a `WeblocError`
|
|
104
|
+
subclass instead of returning a partial URL.
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
Run `bundle install` followed by `bundle exec rake test`.
|
|
109
|
+
|
|
110
|
+
The suite retains the original tests and fixtures and exercises the Ruby examples
|
|
111
|
+
in this README. Additional fixtures cover other binary layouts and files written
|
|
112
|
+
by older releases. Interoperability tests use Python 3's `plistlib` when available
|
|
113
|
+
and Apple's `plutil` on macOS; neither is a runtime dependency of the gem.
|
|
114
|
+
|
|
121
115
|
## Thanks
|
|
122
116
|
|
|
123
117
|
Thanks is due to Christos Karaiskos for [this article](https://medium.com/@karaiskc/understanding-apples-binary-property-list-format-281e6da00dbd
|
|
@@ -125,6 +119,6 @@ Thanks is due to Christos Karaiskos for [this article](https://medium.com/@karai
|
|
|
125
119
|
|
|
126
120
|
## License
|
|
127
121
|
|
|
128
|
-
Copyright (C) 2011-
|
|
122
|
+
Copyright (C) 2011-2026 Peter Cooper
|
|
129
123
|
|
|
130
|
-
webloc is licensed under the terms of the MIT License
|
|
124
|
+
webloc is licensed under the terms of the MIT License
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
class Webloc
|
|
2
|
+
# Read the root dictionary's URL without recursively decoding unrelated metadata.
|
|
3
|
+
class BinaryPlist
|
|
4
|
+
HEADER = 'bplist00'.b.freeze
|
|
5
|
+
PREFIX = "bplist00\xD1\x01\x02SURL".b.freeze
|
|
6
|
+
INTEGER_FORMATS = { 1 => 'C', 2 => 'n', 4 => 'N', 8 => 'Q>' }.freeze
|
|
7
|
+
|
|
8
|
+
def self.integer_width(value)
|
|
9
|
+
INTEGER_FORMATS.each_key.find { |width| value < (1 << (8 * width)) } ||
|
|
10
|
+
raise(ArgumentError, 'URL is too long to encode')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.dump(url)
|
|
14
|
+
if url.ascii_only?
|
|
15
|
+
payload = url.b
|
|
16
|
+
length = payload.bytesize
|
|
17
|
+
marker = 0x5f
|
|
18
|
+
else
|
|
19
|
+
payload = url.encode(Encoding::UTF_16BE).b
|
|
20
|
+
length = payload.bytesize / 2 # UTF-16 code units, including surrogate pairs.
|
|
21
|
+
marker = 0x6f
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Keep the historical object order and extended string length encoding,
|
|
25
|
+
# so existing valid ASCII output (including the original fixture) is stable.
|
|
26
|
+
width = integer_width(length)
|
|
27
|
+
output = PREFIX + [marker, 0x10 + width.bit_length - 1].pack('CC')
|
|
28
|
+
output << [length].pack(INTEGER_FORMATS.fetch(width)) << payload
|
|
29
|
+
table_offset = output.bytesize
|
|
30
|
+
offset_width = integer_width(table_offset)
|
|
31
|
+
output << [8, 11, 15].pack("#{INTEGER_FORMATS.fetch(offset_width)}*")
|
|
32
|
+
output << [offset_width, 1, 3, 0, table_offset].pack('x6CCQ>Q>Q>')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(data, filename)
|
|
36
|
+
@data = data
|
|
37
|
+
@filename = filename
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def url
|
|
41
|
+
unless @data.start_with?(HEADER)
|
|
42
|
+
detail = @data.include?('SURL_') ? 'missing bplist00 header' : 'missing SURL marker'
|
|
43
|
+
raise InvalidFormatError, "Invalid binary webloc format - #{detail} in file: #{@filename}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Recognize the complete historical layout, never just a URL marker.
|
|
47
|
+
historical_url = legacy_url
|
|
48
|
+
return historical_url if historical_url
|
|
49
|
+
|
|
50
|
+
read_table
|
|
51
|
+
type, count, cursor, limit = object(@root)
|
|
52
|
+
corrupt('Root object is not a dictionary') unless type == 0xd
|
|
53
|
+
read(cursor, count * 2 * @reference_width, limit)
|
|
54
|
+
|
|
55
|
+
count.times do |index|
|
|
56
|
+
key_ref = integer(cursor + index * @reference_width, @reference_width, limit)
|
|
57
|
+
next unless string(key_ref) == 'URL'
|
|
58
|
+
|
|
59
|
+
value_ref = integer(cursor + (count + index) * @reference_width, @reference_width, limit)
|
|
60
|
+
return string(value_ref)
|
|
61
|
+
end
|
|
62
|
+
corrupt("No 'URL' key found")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def corrupt(message)
|
|
68
|
+
raise CorruptedFileError, "#{message} in binary webloc file: #{@filename}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def read(offset, length, limit)
|
|
72
|
+
corrupt('Truncated or invalid object') if offset < 8 || length < 0 || offset > limit || length > limit - offset
|
|
73
|
+
@data.byteslice(offset, length)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def integer(offset, width, limit)
|
|
77
|
+
read(offset, width, limit).each_byte.reduce(0) { |value, byte| (value << 8) | byte }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def read_table
|
|
81
|
+
corrupt('Truncated plist trailer') if @data.bytesize < 40
|
|
82
|
+
offset_width, @reference_width, count, @root, table_offset =
|
|
83
|
+
@data.byteslice(-32, 32).unpack('x6CCQ>Q>Q>')
|
|
84
|
+
unless (1..8).cover?(offset_width) && (1..8).cover?(@reference_width)
|
|
85
|
+
corrupt('Invalid offset or reference width')
|
|
86
|
+
end
|
|
87
|
+
trailer_offset = @data.bytesize - 32
|
|
88
|
+
unless count > 0 && @root < count && table_offset >= 8 &&
|
|
89
|
+
table_offset + count * offset_width == trailer_offset
|
|
90
|
+
corrupt('Invalid offset table')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
@offsets = Array.new(count) do |index|
|
|
94
|
+
offset = integer(table_offset + index * offset_width, offset_width, trailer_offset)
|
|
95
|
+
corrupt('Object offset is outside the object table') unless offset >= 8 && offset < table_offset
|
|
96
|
+
offset
|
|
97
|
+
end
|
|
98
|
+
# Objects need not be stored in reference order. Bound each read by the
|
|
99
|
+
# following physical object, so a truncated URL cannot consume the table.
|
|
100
|
+
boundaries = @offsets.uniq.sort + [table_offset]
|
|
101
|
+
@limits = boundaries.each_cons(2).to_h
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def object(reference)
|
|
105
|
+
corrupt('Invalid object reference') unless reference < @offsets.length
|
|
106
|
+
offset = @offsets.fetch(reference)
|
|
107
|
+
limit = @limits.fetch(offset)
|
|
108
|
+
marker = integer(offset, 1, limit)
|
|
109
|
+
cursor = offset + 1
|
|
110
|
+
length = marker & 0xf
|
|
111
|
+
if length == 0xf
|
|
112
|
+
size_marker = integer(cursor, 1, limit)
|
|
113
|
+
unless (0x10..0x13).cover?(size_marker)
|
|
114
|
+
raise InvalidFormatError, "Unsupported length encoding in binary webloc file: #{@filename}"
|
|
115
|
+
end
|
|
116
|
+
width = 1 << (size_marker & 0xf)
|
|
117
|
+
length = integer(cursor + 1, width, limit)
|
|
118
|
+
cursor += width + 1
|
|
119
|
+
end
|
|
120
|
+
[marker >> 4, length, cursor, limit]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def string(reference)
|
|
124
|
+
type, length, cursor, limit = object(reference)
|
|
125
|
+
case type
|
|
126
|
+
when 0x5
|
|
127
|
+
value = read(cursor, length, limit)
|
|
128
|
+
corrupt('Invalid ASCII string') unless value.ascii_only?
|
|
129
|
+
value
|
|
130
|
+
when 0x6
|
|
131
|
+
read(cursor, length * 2, limit).force_encoding(Encoding::UTF_16BE).encode(Encoding::UTF_8)
|
|
132
|
+
else
|
|
133
|
+
corrupt('Expected a string')
|
|
134
|
+
end
|
|
135
|
+
rescue EncodingError
|
|
136
|
+
corrupt('Invalid Unicode string')
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def legacy_url
|
|
140
|
+
return unless @data.start_with?(PREFIX + "\x5f".b)
|
|
141
|
+
width = { 0x10 => 1, 0x11 => 2 }[@data.getbyte(16)]
|
|
142
|
+
return unless width && @data.bytesize >= 18 + width + 35
|
|
143
|
+
|
|
144
|
+
length = @data.byteslice(17, width).unpack1(INTEGER_FORMATS.fetch(width))
|
|
145
|
+
return unless length >= 238 && width == (length > 255 ? 2 : 1)
|
|
146
|
+
table_offset = 17 + width + length
|
|
147
|
+
return unless @data.bytesize == table_offset + 35
|
|
148
|
+
|
|
149
|
+
# Match even the two-byte overflow in the old trailer, but require every
|
|
150
|
+
# other byte of its structure and the full declared URL to be present.
|
|
151
|
+
trailer = [1, 1, 3, 0, (length + 18) & 0xffff].pack('x6CCQ>Q>Q>')
|
|
152
|
+
return unless @data.byteslice(table_offset, 35) == "\x08\x0b\x0f".b + trailer
|
|
153
|
+
value = @data.byteslice(17 + width, length)
|
|
154
|
+
value if value.ascii_only?
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private_constant :BinaryPlist
|
|
159
|
+
end
|
data/lib/webloc/version.rb
CHANGED
data/lib/webloc.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'plist'
|
|
2
|
+
require_relative 'webloc/binary_plist'
|
|
2
3
|
|
|
3
4
|
class Webloc
|
|
4
5
|
class WeblocError < StandardError; end
|
|
@@ -18,17 +19,16 @@ class Webloc
|
|
|
18
19
|
raise FileNotFoundError, "File not found: #{filename}" unless File.exist?(filename)
|
|
19
20
|
|
|
20
21
|
begin
|
|
21
|
-
data = File.
|
|
22
|
+
data = File.binread(filename)
|
|
22
23
|
rescue => e
|
|
23
24
|
raise FileNotFoundError, "Unable to read file '#{filename}': #{e.message}"
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
raise EmptyFileError, "File is empty: #{filename}" if data.empty?
|
|
27
28
|
|
|
28
|
-
data = data.force_encoding('binary') rescue data
|
|
29
29
|
url = nil
|
|
30
30
|
|
|
31
|
-
if data
|
|
31
|
+
if data.start_with?('bplist00') || !data.include?('<plist')
|
|
32
32
|
# Handle binary plist format
|
|
33
33
|
url = parse_binary_format(data, filename)
|
|
34
34
|
else
|
|
@@ -36,40 +36,15 @@ class Webloc
|
|
|
36
36
|
url = parse_xml_format(filename)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
raise CorruptedFileError, "No URL found in webloc file: #{filename}" if url.nil? || url.empty?
|
|
39
|
+
raise CorruptedFileError, "No URL found in webloc file: #{filename}" if url.nil? || (url.is_a?(String) && url.empty?)
|
|
40
|
+
raise CorruptedFileError, "URL must be a string in webloc file: #{filename}" unless url.is_a?(String)
|
|
40
41
|
new(url)
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
private
|
|
44
45
|
|
|
45
46
|
def self.parse_binary_format(data, filename)
|
|
46
|
-
|
|
47
|
-
raise InvalidFormatError, "Invalid binary webloc format - missing SURL marker in file: #{filename}" unless offset
|
|
48
|
-
|
|
49
|
-
begin
|
|
50
|
-
length_offset = 7
|
|
51
|
-
if data[offset + 5] == "\x10"
|
|
52
|
-
length = data[offset + 6]
|
|
53
|
-
length = length.unpack('C')[0]
|
|
54
|
-
elsif data[offset + 5] == "\x11"
|
|
55
|
-
length_offset = 8
|
|
56
|
-
length = data[offset + 6] + data[offset + 7]
|
|
57
|
-
length = length.unpack('S>')[0]
|
|
58
|
-
else
|
|
59
|
-
raise InvalidFormatError, "Unsupported length encoding in binary webloc file: #{filename}"
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
raise CorruptedFileError, "Invalid URL length (#{length}) in file: #{filename}" if length <= 0 || length > data.length
|
|
63
|
-
|
|
64
|
-
url = data[offset + length_offset, length]
|
|
65
|
-
raise CorruptedFileError, "Extracted URL is empty from file: #{filename}" if url.nil? || url.empty?
|
|
66
|
-
|
|
67
|
-
url
|
|
68
|
-
rescue CorruptedFileError, InvalidFormatError => e
|
|
69
|
-
raise e
|
|
70
|
-
rescue => e
|
|
71
|
-
raise CorruptedFileError, "Failed to parse binary webloc format in file '#{filename}': #{e.message}"
|
|
72
|
-
end
|
|
47
|
+
BinaryPlist.new(data.b, filename).url
|
|
73
48
|
end
|
|
74
49
|
|
|
75
50
|
def self.parse_xml_format(filename)
|
|
@@ -93,46 +68,9 @@ class Webloc
|
|
|
93
68
|
public
|
|
94
69
|
|
|
95
70
|
def data
|
|
96
|
-
|
|
97
|
-
@data = "bplist\x30\x30".bytes
|
|
98
|
-
|
|
99
|
-
# PLIST OBJECT TABLE
|
|
100
|
-
@data += "\xD1\x01\x02".bytes # object 1 is a dictionary
|
|
101
|
-
@data += "SURL".bytes # object 2
|
|
102
|
-
|
|
103
|
-
length_suffix = @url.length > 255 ? "\x11" : "\x10"
|
|
104
|
-
@data += ("\x5f" + length_suffix).bytes # object 3 is an ASCII string with a variable length length encoding (I know..)
|
|
105
|
-
# .. the '0' in \x10 denotes the length can be encoded within 2**0 bytes (i.e. 1)
|
|
106
|
-
# .. the '1' in \x11 denotes the length can be encoded within 2**1 bytes (i.e. 2)
|
|
107
|
-
|
|
108
|
-
if @url.length > 255
|
|
109
|
-
@data += [@url.length].pack('S>').bytes
|
|
110
|
-
else
|
|
111
|
-
@data += [@url.length].pack('C').bytes
|
|
112
|
-
end
|
|
113
|
-
@data += @url.bytes # and finally the URL itself
|
|
114
|
-
|
|
115
|
-
# This is the offset table
|
|
116
|
-
@data += "\x08\x0B\x0F".bytes # so objects at 0x08, 0x0b and 0x0f
|
|
117
|
-
|
|
118
|
-
# PLIST TRAILER
|
|
119
|
-
# Bytes 0-4 are unused
|
|
120
|
-
@data += "\x00\x00\x00\x00\x00".bytes
|
|
121
|
-
# Byte 5 is the sort version
|
|
122
|
-
@data += "\x00".bytes
|
|
123
|
-
# Byte 6 is how many bytes are needed for each offset table offset
|
|
124
|
-
@data += "\x01".bytes
|
|
125
|
-
@data += "\x01".bytes
|
|
126
|
-
# Bytes 8-15 are how many objects are contained in the plist
|
|
127
|
-
@data += "\x00\x00\x00\x00\x00\x00\x00\x03".bytes
|
|
128
|
-
# Bytes 16-23 are for an offset from the offset table
|
|
129
|
-
@data += "\x00\x00\x00\x00\x00\x00\x00\x00".bytes
|
|
130
|
-
# Bytes 24-31 denote the position of the offset table from the start of the file
|
|
131
|
-
@data += "\x00\x00\x00\x00\x00\x00".bytes + [@url.length + 18].pack('S>').bytes
|
|
132
|
-
|
|
133
|
-
@data = @data.pack('C*')
|
|
71
|
+
BinaryPlist.dump(@url)
|
|
134
72
|
end
|
|
135
|
-
|
|
73
|
+
|
|
136
74
|
def save(filename)
|
|
137
75
|
raise ArgumentError, "Filename cannot be nil or empty" if filename.nil? || filename.empty?
|
|
138
76
|
|
|
@@ -142,4 +80,4 @@ class Webloc
|
|
|
142
80
|
raise WeblocError, "Failed to save webloc file '#{filename}': #{e.message}"
|
|
143
81
|
end
|
|
144
82
|
end
|
|
145
|
-
end
|
|
83
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: webloc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter Cooper
|
|
@@ -30,19 +30,15 @@ executables: []
|
|
|
30
30
|
extensions: []
|
|
31
31
|
extra_rdoc_files: []
|
|
32
32
|
files:
|
|
33
|
-
-
|
|
34
|
-
- Gemfile
|
|
33
|
+
- CHANGELOG.md
|
|
35
34
|
- LICENSE
|
|
36
35
|
- README.md
|
|
37
|
-
- Rakefile
|
|
38
36
|
- lib/webloc.rb
|
|
37
|
+
- lib/webloc/binary_plist.rb
|
|
39
38
|
- lib/webloc/version.rb
|
|
40
|
-
- test/oldstyle.webloc
|
|
41
|
-
- test/pliststyle.webloc
|
|
42
|
-
- test/webloc_test.rb
|
|
43
|
-
- webloc.gemspec
|
|
44
39
|
homepage: https://github.com/peterc/webloc
|
|
45
|
-
licenses:
|
|
40
|
+
licenses:
|
|
41
|
+
- MIT
|
|
46
42
|
metadata: {}
|
|
47
43
|
rdoc_options: []
|
|
48
44
|
require_paths:
|
|
@@ -51,17 +47,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
51
47
|
requirements:
|
|
52
48
|
- - ">="
|
|
53
49
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '0'
|
|
50
|
+
version: '3.0'
|
|
55
51
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
52
|
requirements:
|
|
57
53
|
- - ">="
|
|
58
54
|
- !ruby/object:Gem::Version
|
|
59
55
|
version: '0'
|
|
60
56
|
requirements: []
|
|
61
|
-
rubygems_version:
|
|
57
|
+
rubygems_version: 4.0.6
|
|
62
58
|
specification_version: 4
|
|
63
59
|
summary: Reads and writes .webloc files on macOS
|
|
64
|
-
test_files:
|
|
65
|
-
- test/oldstyle.webloc
|
|
66
|
-
- test/pliststyle.webloc
|
|
67
|
-
- test/webloc_test.rb
|
|
60
|
+
test_files: []
|
data/.gitignore
DELETED
data/Gemfile
DELETED
data/Rakefile
DELETED
data/test/oldstyle.webloc
DELETED
|
Binary file
|
data/test/pliststyle.webloc
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>URL</key>
|
|
6
|
-
<string>https://github.com/peterc/webloc</string>
|
|
7
|
-
</dict>
|
|
8
|
-
</plist>
|
data/test/webloc_test.rb
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
require 'test/unit'
|
|
2
|
-
require 'webloc'
|
|
3
|
-
require 'tempfile'
|
|
4
|
-
|
|
5
|
-
class WeblocTest < Test::Unit::TestCase
|
|
6
|
-
def test_webloc_object_requires_url
|
|
7
|
-
assert_raise(ArgumentError) { Webloc.new }
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def test_webloc_object_rejects_nil_url
|
|
11
|
-
error = assert_raise(ArgumentError) { Webloc.new(nil) }
|
|
12
|
-
assert_equal "URL cannot be nil or empty", error.message
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def test_webloc_object_rejects_empty_url
|
|
16
|
-
error = assert_raise(ArgumentError) { Webloc.new("") }
|
|
17
|
-
assert_equal "URL cannot be nil or empty", error.message
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def test_webloc_object_created_with_url
|
|
21
|
-
assert_equal 'http://example.com', Webloc.new('http://example.com').url
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def test_webloc_object_loaded_from_old_style_file
|
|
25
|
-
assert_equal 'https://github.com/peterc/webloc', Webloc.load(File.dirname(__FILE__) + '/oldstyle.webloc').url
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def test_webloc_object_loaded_from_plist_file
|
|
29
|
-
assert_equal 'https://github.com/peterc/webloc', Webloc.load(File.dirname(__FILE__) + '/pliststyle.webloc').url
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def test_webloc_generates_valid_data
|
|
33
|
-
data = File.read(File.dirname(__FILE__) + '/oldstyle.webloc').b
|
|
34
|
-
assert_equal data, Webloc.new('https://github.com/peterc/webloc').data
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def test_webloc_can_handle_long_urls
|
|
38
|
-
url = "http://example.com/this-is-a-very-long-url-abcde" + ('a' * 2000)
|
|
39
|
-
assert_nothing_raised { Webloc.new(url).data }
|
|
40
|
-
file = Tempfile.new('test-long-webloc')
|
|
41
|
-
begin
|
|
42
|
-
Webloc.new(url).save(file.path)
|
|
43
|
-
assert_equal url, Webloc.load(file.path).url
|
|
44
|
-
ensure
|
|
45
|
-
file.close
|
|
46
|
-
file.unlink
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def test_webloc_can_write_file
|
|
51
|
-
file = Tempfile.new('test-webloc')
|
|
52
|
-
begin
|
|
53
|
-
Webloc.new('https://github.com/peterc/webloc').save(file.path)
|
|
54
|
-
assert_equal Webloc.new('https://github.com/peterc/webloc').data, File.read(file.path).b
|
|
55
|
-
ensure
|
|
56
|
-
file.close
|
|
57
|
-
file.unlink
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def test_load_nonexistent_file_raises_file_not_found_error
|
|
62
|
-
error = assert_raise(Webloc::FileNotFoundError) { Webloc.load('nonexistent.webloc') }
|
|
63
|
-
assert_match(/File not found: nonexistent\.webloc/, error.message)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def test_load_empty_file_raises_empty_file_error
|
|
67
|
-
file = Tempfile.new('empty-webloc')
|
|
68
|
-
begin
|
|
69
|
-
file.close
|
|
70
|
-
error = assert_raise(Webloc::EmptyFileError) { Webloc.load(file.path) }
|
|
71
|
-
assert_match(/File is empty:/, error.message)
|
|
72
|
-
ensure
|
|
73
|
-
file.unlink
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def test_load_corrupted_binary_file_raises_invalid_format_error
|
|
78
|
-
file = Tempfile.new('corrupted-webloc')
|
|
79
|
-
begin
|
|
80
|
-
file.write("corrupted binary data without SURL marker")
|
|
81
|
-
file.close
|
|
82
|
-
error = assert_raise(Webloc::InvalidFormatError) { Webloc.load(file.path) }
|
|
83
|
-
assert_match(/Invalid binary webloc format - missing SURL marker/, error.message)
|
|
84
|
-
ensure
|
|
85
|
-
file.unlink
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def test_load_invalid_xml_file_raises_invalid_format_error
|
|
90
|
-
file = Tempfile.new('invalid-xml-webloc')
|
|
91
|
-
begin
|
|
92
|
-
file.write("<plist><invalid>xml</invalid>")
|
|
93
|
-
file.close
|
|
94
|
-
error = assert_raise(Webloc::InvalidFormatError) { Webloc.load(file.path) }
|
|
95
|
-
assert_match(/Invalid XML plist format/, error.message)
|
|
96
|
-
ensure
|
|
97
|
-
file.unlink
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def test_load_xml_without_url_key_raises_invalid_format_error
|
|
102
|
-
file = Tempfile.new('no-url-xml-webloc')
|
|
103
|
-
begin
|
|
104
|
-
file.write('<?xml version="1.0"?><plist><dict><key>NotURL</key><string>value</string></dict></plist>')
|
|
105
|
-
file.close
|
|
106
|
-
error = assert_raise(Webloc::InvalidFormatError) { Webloc.load(file.path) }
|
|
107
|
-
assert_match(/No 'URL' key found in plist file/, error.message)
|
|
108
|
-
ensure
|
|
109
|
-
file.unlink
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def test_save_with_nil_filename_raises_argument_error
|
|
114
|
-
webloc = Webloc.new('http://example.com')
|
|
115
|
-
error = assert_raise(ArgumentError) { webloc.save(nil) }
|
|
116
|
-
assert_equal "Filename cannot be nil or empty", error.message
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def test_save_with_empty_filename_raises_argument_error
|
|
120
|
-
webloc = Webloc.new('http://example.com')
|
|
121
|
-
error = assert_raise(ArgumentError) { webloc.save("") }
|
|
122
|
-
assert_equal "Filename cannot be nil or empty", error.message
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def test_save_to_invalid_path_raises_webloc_error
|
|
126
|
-
webloc = Webloc.new('http://example.com')
|
|
127
|
-
error = assert_raise(Webloc::WeblocError) { webloc.save('/invalid/path/that/does/not/exist/file.webloc') }
|
|
128
|
-
assert_match(/Failed to save webloc file/, error.message)
|
|
129
|
-
end
|
|
130
|
-
end
|
data/webloc.gemspec
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
|
2
|
-
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
-
require "webloc/version"
|
|
4
|
-
|
|
5
|
-
Gem::Specification.new do |s|
|
|
6
|
-
s.name = "webloc"
|
|
7
|
-
s.version = Webloc::VERSION
|
|
8
|
-
s.platform = Gem::Platform::RUBY
|
|
9
|
-
s.authors = ["Peter Cooper"]
|
|
10
|
-
s.email = ["git@peterc.org"]
|
|
11
|
-
s.homepage = "https://github.com/peterc/webloc"
|
|
12
|
-
s.summary = %q{Reads and writes .webloc files on macOS}
|
|
13
|
-
s.description = %q{Webloc reads and writes .webloc files on macOS}
|
|
14
|
-
|
|
15
|
-
s.rubyforge_project = "webloc"
|
|
16
|
-
|
|
17
|
-
s.files = `git ls-files`.split("\n")
|
|
18
|
-
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
19
|
-
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
20
|
-
s.require_paths = ["lib"]
|
|
21
|
-
|
|
22
|
-
s.add_dependency 'plist'
|
|
23
|
-
end
|