webloc 0.3.0 → 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 +98 -4
- data/lib/webloc/binary_plist.rb +159 -0
- data/lib/webloc/version.rb +1 -1
- data/lib/webloc.rb +58 -53
- metadata +9 -19
- 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 -50
- 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
|
|
|
@@ -10,14 +10,108 @@ It works on Ruby 2.7 and up, including Ruby 3.x, and supports URLs of up to 2048
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
+
### Basic Usage
|
|
14
|
+
|
|
13
15
|
Reading a .webloc file:
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
require 'webloc'
|
|
18
|
+
webloc = Webloc.load('bookmark.webloc')
|
|
19
|
+
puts webloc.url
|
|
20
|
+
# => "https://example.com"
|
|
16
21
|
|
|
17
22
|
Writing to a .webloc file:
|
|
18
23
|
|
|
24
|
+
require 'webloc'
|
|
19
25
|
Webloc.new('https://rubyweekly.com/').save('rubyweekly.webloc')
|
|
20
26
|
|
|
27
|
+
### Advanced Examples
|
|
28
|
+
|
|
29
|
+
#### Error handling
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require 'webloc'
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
webloc = Webloc.load('suspicious.webloc')
|
|
36
|
+
puts webloc.url
|
|
37
|
+
rescue Webloc::FileNotFoundError => e
|
|
38
|
+
puts "File not found: #{e.message}"
|
|
39
|
+
rescue Webloc::CorruptedFileError => e
|
|
40
|
+
puts "File is corrupted: #{e.message}"
|
|
41
|
+
rescue Webloc::InvalidFormatError => e
|
|
42
|
+
puts "Invalid file format: #{e.message}"
|
|
43
|
+
rescue Webloc::WeblocError => e
|
|
44
|
+
puts "General webloc error: #{e.message}"
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### Validating URLs before creating webloc files
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
require 'webloc'
|
|
52
|
+
require 'uri'
|
|
53
|
+
|
|
54
|
+
def create_webloc_safely(url, filename)
|
|
55
|
+
# Basic URL validation
|
|
56
|
+
uri = URI.parse(url)
|
|
57
|
+
unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
58
|
+
puts "Invalid URL scheme: #{url}"
|
|
59
|
+
return false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Create the webloc file
|
|
63
|
+
Webloc.new(url).save(filename)
|
|
64
|
+
puts "Created #{filename} for #{url}"
|
|
65
|
+
true
|
|
66
|
+
rescue URI::InvalidURIError
|
|
67
|
+
puts "Invalid URL format: #{url}"
|
|
68
|
+
false
|
|
69
|
+
rescue Webloc::WeblocError => e
|
|
70
|
+
puts "Failed to create webloc: #{e.message}"
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
create_webloc_safely('https://example.com', 'example.webloc')
|
|
75
|
+
create_webloc_safely('invalid-url', 'invalid.webloc')
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### Converting between formats
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
require 'webloc'
|
|
82
|
+
require 'json'
|
|
83
|
+
|
|
84
|
+
# Convert webloc to JSON
|
|
85
|
+
webloc = Webloc.load('bookmark.webloc')
|
|
86
|
+
json_data = { url: webloc.url, title: File.basename('bookmark.webloc', '.webloc') }
|
|
87
|
+
File.write('bookmark.json', JSON.pretty_generate(json_data))
|
|
88
|
+
|
|
89
|
+
# Convert JSON back to webloc
|
|
90
|
+
json_content = JSON.parse(File.read('bookmark.json'))
|
|
91
|
+
Webloc.new(json_content['url']).save('restored.webloc')
|
|
92
|
+
```
|
|
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
|
+
|
|
21
115
|
## Thanks
|
|
22
116
|
|
|
23
117
|
Thanks is due to Christos Karaiskos for [this article](https://medium.com/@karaiskc/understanding-apples-binary-property-list-format-281e6da00dbd
|
|
@@ -25,6 +119,6 @@ Thanks is due to Christos Karaiskos for [this article](https://medium.com/@karai
|
|
|
25
119
|
|
|
26
120
|
## License
|
|
27
121
|
|
|
28
|
-
Copyright (C) 2011-
|
|
122
|
+
Copyright (C) 2011-2026 Peter Cooper
|
|
29
123
|
|
|
30
|
-
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,78 +1,83 @@
|
|
|
1
1
|
require 'plist'
|
|
2
|
+
require_relative 'webloc/binary_plist'
|
|
2
3
|
|
|
3
4
|
class Webloc
|
|
5
|
+
class WeblocError < StandardError; end
|
|
6
|
+
class FileNotFoundError < WeblocError; end
|
|
7
|
+
class CorruptedFileError < WeblocError; end
|
|
8
|
+
class InvalidFormatError < WeblocError; end
|
|
9
|
+
class EmptyFileError < WeblocError; end
|
|
10
|
+
|
|
4
11
|
attr_accessor :url
|
|
5
12
|
|
|
6
13
|
def initialize(url)
|
|
14
|
+
raise ArgumentError, "URL cannot be nil or empty" if url.nil? || url.empty?
|
|
7
15
|
@url = url
|
|
8
16
|
end
|
|
9
17
|
|
|
10
18
|
def self.load(filename)
|
|
11
|
-
|
|
12
|
-
data = data.force_encoding('binary') rescue data
|
|
19
|
+
raise FileNotFoundError, "File not found: #{filename}" unless File.exist?(filename)
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
begin
|
|
22
|
+
data = File.binread(filename)
|
|
23
|
+
rescue => e
|
|
24
|
+
raise FileNotFoundError, "Unable to read file '#{filename}': #{e.message}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
raise EmptyFileError, "File is empty: #{filename}" if data.empty?
|
|
28
|
+
|
|
29
|
+
url = nil
|
|
30
|
+
|
|
31
|
+
if data.start_with?('bplist00') || !data.include?('<plist')
|
|
32
|
+
# Handle binary plist format
|
|
33
|
+
url = parse_binary_format(data, filename)
|
|
26
34
|
else
|
|
27
|
-
|
|
35
|
+
# Handle XML plist format
|
|
36
|
+
url = parse_xml_format(filename)
|
|
28
37
|
end
|
|
29
38
|
|
|
30
|
-
raise
|
|
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)
|
|
31
41
|
new(url)
|
|
32
42
|
end
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
# PLIST HEADER
|
|
36
|
-
@data = "bplist\x30\x30".bytes
|
|
44
|
+
private
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
def self.parse_binary_format(data, filename)
|
|
47
|
+
BinaryPlist.new(data.b, filename).url
|
|
48
|
+
end
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
def self.parse_xml_format(filename)
|
|
51
|
+
begin
|
|
52
|
+
plist_data = Plist::parse_xml(filename)
|
|
53
|
+
raise InvalidFormatError, "Invalid XML plist format - could not parse file: #{filename}" unless plist_data.is_a?(Hash)
|
|
54
|
+
|
|
55
|
+
url = plist_data['URL']
|
|
56
|
+
raise CorruptedFileError, "No 'URL' key found in plist file: #{filename}" unless url
|
|
57
|
+
|
|
58
|
+
url
|
|
59
|
+
rescue => e
|
|
60
|
+
if e.message.include?('parse') || e.message.include?('XML') || e.message.include?('plist')
|
|
61
|
+
raise InvalidFormatError, "Invalid XML plist format in file '#{filename}': #{e.message}"
|
|
62
|
+
else
|
|
63
|
+
raise CorruptedFileError, "Failed to parse XML webloc format in file '#{filename}': #{e.message}"
|
|
64
|
+
end
|
|
51
65
|
end
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# This is the offset table
|
|
55
|
-
@data += "\x08\x0B\x0F".bytes # so objects at 0x08, 0x0b and 0x0f
|
|
66
|
+
end
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
# Bytes 0-4 are unused
|
|
59
|
-
@data += "\x00\x00\x00\x00\x00".bytes
|
|
60
|
-
# Byte 5 is the sort version
|
|
61
|
-
@data += "\x00".bytes
|
|
62
|
-
# Byte 6 is how many bytes are needed for each offset table offset
|
|
63
|
-
@data += "\x01".bytes
|
|
64
|
-
@data += "\x01".bytes
|
|
65
|
-
# Bytes 8-15 are how many objects are contained in the plist
|
|
66
|
-
@data += "\x00\x00\x00\x00\x00\x00\x00\x03".bytes
|
|
67
|
-
# Bytes 16-23 are for an offset from the offset table
|
|
68
|
-
@data += "\x00\x00\x00\x00\x00\x00\x00\x00".bytes
|
|
69
|
-
# Bytes 24-31 denote the position of the offset table from the start of the file
|
|
70
|
-
@data += "\x00\x00\x00\x00\x00\x00".bytes + [@url.length + 18].pack('S>').bytes
|
|
68
|
+
public
|
|
71
69
|
|
|
72
|
-
|
|
70
|
+
def data
|
|
71
|
+
BinaryPlist.dump(@url)
|
|
73
72
|
end
|
|
74
|
-
|
|
73
|
+
|
|
75
74
|
def save(filename)
|
|
76
|
-
|
|
75
|
+
raise ArgumentError, "Filename cannot be nil or empty" if filename.nil? || filename.empty?
|
|
76
|
+
|
|
77
|
+
begin
|
|
78
|
+
File.open(filename, 'wb') { |f| f.write data }
|
|
79
|
+
rescue => e
|
|
80
|
+
raise WeblocError, "Failed to save webloc file '#{filename}': #{e.message}"
|
|
81
|
+
end
|
|
77
82
|
end
|
|
78
|
-
end
|
|
83
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
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
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: plist
|
|
@@ -31,21 +30,16 @@ executables: []
|
|
|
31
30
|
extensions: []
|
|
32
31
|
extra_rdoc_files: []
|
|
33
32
|
files:
|
|
34
|
-
-
|
|
35
|
-
- Gemfile
|
|
33
|
+
- CHANGELOG.md
|
|
36
34
|
- LICENSE
|
|
37
35
|
- README.md
|
|
38
|
-
- Rakefile
|
|
39
36
|
- lib/webloc.rb
|
|
37
|
+
- lib/webloc/binary_plist.rb
|
|
40
38
|
- lib/webloc/version.rb
|
|
41
|
-
- test/oldstyle.webloc
|
|
42
|
-
- test/pliststyle.webloc
|
|
43
|
-
- test/webloc_test.rb
|
|
44
|
-
- webloc.gemspec
|
|
45
39
|
homepage: https://github.com/peterc/webloc
|
|
46
|
-
licenses:
|
|
40
|
+
licenses:
|
|
41
|
+
- MIT
|
|
47
42
|
metadata: {}
|
|
48
|
-
post_install_message:
|
|
49
43
|
rdoc_options: []
|
|
50
44
|
require_paths:
|
|
51
45
|
- lib
|
|
@@ -53,18 +47,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
53
47
|
requirements:
|
|
54
48
|
- - ">="
|
|
55
49
|
- !ruby/object:Gem::Version
|
|
56
|
-
version: '0'
|
|
50
|
+
version: '3.0'
|
|
57
51
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
52
|
requirements:
|
|
59
53
|
- - ">="
|
|
60
54
|
- !ruby/object:Gem::Version
|
|
61
55
|
version: '0'
|
|
62
56
|
requirements: []
|
|
63
|
-
rubygems_version:
|
|
64
|
-
signing_key:
|
|
57
|
+
rubygems_version: 4.0.6
|
|
65
58
|
specification_version: 4
|
|
66
59
|
summary: Reads and writes .webloc files on macOS
|
|
67
|
-
test_files:
|
|
68
|
-
- test/oldstyle.webloc
|
|
69
|
-
- test/pliststyle.webloc
|
|
70
|
-
- 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,50 +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_created_with_url
|
|
11
|
-
assert_equal 'http://example.com', Webloc.new('http://example.com').url
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def test_webloc_object_loaded_from_old_style_file
|
|
15
|
-
assert_equal 'https://github.com/peterc/webloc', Webloc.load(File.dirname(__FILE__) + '/oldstyle.webloc').url
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def test_webloc_object_loaded_from_plist_file
|
|
19
|
-
assert_equal 'https://github.com/peterc/webloc', Webloc.load(File.dirname(__FILE__) + '/pliststyle.webloc').url
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def test_webloc_generates_valid_data
|
|
23
|
-
data = File.read(File.dirname(__FILE__) + '/oldstyle.webloc').b
|
|
24
|
-
assert_equal data, Webloc.new('https://github.com/peterc/webloc').data
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def test_webloc_can_handle_long_urls
|
|
28
|
-
url = "http://example.com/this-is-a-very-long-url-abcde" + ('a' * 2000)
|
|
29
|
-
assert_nothing_raised { Webloc.new(url).data }
|
|
30
|
-
file = Tempfile.new('test-long-webloc')
|
|
31
|
-
begin
|
|
32
|
-
Webloc.new(url).save(file.path)
|
|
33
|
-
assert_equal url, Webloc.load(file.path).url
|
|
34
|
-
ensure
|
|
35
|
-
file.close
|
|
36
|
-
file.unlink
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def test_webloc_can_write_file
|
|
41
|
-
file = Tempfile.new('test-webloc')
|
|
42
|
-
begin
|
|
43
|
-
Webloc.new('https://github.com/peterc/webloc').save(file.path)
|
|
44
|
-
assert_equal Webloc.new('https://github.com/peterc/webloc').data, File.read(file.path).b
|
|
45
|
-
ensure
|
|
46
|
-
file.close
|
|
47
|
-
file.unlink
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
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
|