webloc 0.3.0 → 0.3.1
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/README.md +102 -2
- data/lib/webloc/version.rb +1 -1
- data/lib/webloc.rb +76 -9
- data/test/webloc_test.rb +80 -0
- metadata +3 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c818dbf002be73a6bcf36920bfae42c98bd5185d8076e5835773f3b08f41fc69
|
4
|
+
data.tar.gz: 78982a897fce013b29b15e66a64cdd003de478040d6399245942fd49858383e0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 659e74f401a6ce48cac21e72bcf0b07e1c2ac2214579ed700d7715efbff9d68da082a043a5036193cd7e792d7f3a8a38a2ef6ac2780951129f1aa37781e74e8f
|
7
|
+
data.tar.gz: 5799dbc3570c7395545a710f2f2f8bc4bb6f5cb79a681c4a9cb862647a489070fbdc35932a474d2fdac5253f9a090087da179cfe4e6365e32a1eb5efa26d5c0f
|
data/README.md
CHANGED
@@ -10,14 +10,114 @@ 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
|
-
Webloc.load(
|
17
|
+
webloc = Webloc.load('bookmark.webloc')
|
18
|
+
puts webloc.url
|
19
|
+
# => "https://example.com"
|
16
20
|
|
17
21
|
Writing to a .webloc file:
|
18
22
|
|
19
23
|
Webloc.new('https://rubyweekly.com/').save('rubyweekly.webloc')
|
20
24
|
|
25
|
+
### Advanced Examples
|
26
|
+
|
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
|
+
#### Error handling
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
require 'webloc'
|
60
|
+
|
61
|
+
begin
|
62
|
+
webloc = Webloc.load('suspicious.webloc')
|
63
|
+
puts webloc.url
|
64
|
+
rescue Webloc::FileNotFoundError => e
|
65
|
+
puts "File not found: #{e.message}"
|
66
|
+
rescue Webloc::CorruptedFileError => e
|
67
|
+
puts "File is corrupted: #{e.message}"
|
68
|
+
rescue Webloc::InvalidFormatError => e
|
69
|
+
puts "Invalid file format: #{e.message}"
|
70
|
+
rescue Webloc::WeblocError => e
|
71
|
+
puts "General webloc error: #{e.message}"
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
#### Validating URLs before creating webloc files
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
require 'webloc'
|
79
|
+
require 'uri'
|
80
|
+
|
81
|
+
def create_webloc_safely(url, filename)
|
82
|
+
# Basic URL validation
|
83
|
+
uri = URI.parse(url)
|
84
|
+
unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
85
|
+
puts "Invalid URL scheme: #{url}"
|
86
|
+
return false
|
87
|
+
end
|
88
|
+
|
89
|
+
# Create the webloc file
|
90
|
+
Webloc.new(url).save(filename)
|
91
|
+
puts "Created #{filename} for #{url}"
|
92
|
+
true
|
93
|
+
rescue URI::InvalidURIError
|
94
|
+
puts "Invalid URL format: #{url}"
|
95
|
+
false
|
96
|
+
rescue Webloc::WeblocError => e
|
97
|
+
puts "Failed to create webloc: #{e.message}"
|
98
|
+
false
|
99
|
+
end
|
100
|
+
|
101
|
+
create_webloc_safely('https://example.com', 'example.webloc')
|
102
|
+
create_webloc_safely('invalid-url', 'invalid.webloc')
|
103
|
+
```
|
104
|
+
|
105
|
+
#### Converting between formats
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
require 'webloc'
|
109
|
+
require 'json'
|
110
|
+
|
111
|
+
# Convert webloc to JSON
|
112
|
+
webloc = Webloc.load('bookmark.webloc')
|
113
|
+
json_data = { url: webloc.url, title: File.basename('bookmark.webloc', '.webloc') }
|
114
|
+
File.write('bookmark.json', JSON.pretty_generate(json_data))
|
115
|
+
|
116
|
+
# Convert JSON back to webloc
|
117
|
+
json_content = JSON.parse(File.read('bookmark.json'))
|
118
|
+
Webloc.new(json_content['url']).save('restored.webloc')
|
119
|
+
```
|
120
|
+
|
21
121
|
## Thanks
|
22
122
|
|
23
123
|
Thanks is due to Christos Karaiskos for [this article](https://medium.com/@karaiskc/understanding-apples-binary-property-list-format-281e6da00dbd
|
@@ -25,6 +125,6 @@ Thanks is due to Christos Karaiskos for [this article](https://medium.com/@karai
|
|
25
125
|
|
26
126
|
## License
|
27
127
|
|
28
|
-
Copyright (C) 2011-
|
128
|
+
Copyright (C) 2011-2025 Peter Cooper
|
29
129
|
|
30
130
|
webloc is licensed under the terms of the MIT License
|
data/lib/webloc/version.rb
CHANGED
data/lib/webloc.rb
CHANGED
@@ -1,18 +1,52 @@
|
|
1
1
|
require 'plist'
|
2
2
|
|
3
3
|
class Webloc
|
4
|
+
class WeblocError < StandardError; end
|
5
|
+
class FileNotFoundError < WeblocError; end
|
6
|
+
class CorruptedFileError < WeblocError; end
|
7
|
+
class InvalidFormatError < WeblocError; end
|
8
|
+
class EmptyFileError < WeblocError; end
|
9
|
+
|
4
10
|
attr_accessor :url
|
5
11
|
|
6
12
|
def initialize(url)
|
13
|
+
raise ArgumentError, "URL cannot be nil or empty" if url.nil? || url.empty?
|
7
14
|
@url = url
|
8
15
|
end
|
9
16
|
|
10
17
|
def self.load(filename)
|
11
|
-
|
18
|
+
raise FileNotFoundError, "File not found: #{filename}" unless File.exist?(filename)
|
19
|
+
|
20
|
+
begin
|
21
|
+
data = File.read(filename)
|
22
|
+
rescue => e
|
23
|
+
raise FileNotFoundError, "Unable to read file '#{filename}': #{e.message}"
|
24
|
+
end
|
25
|
+
|
26
|
+
raise EmptyFileError, "File is empty: #{filename}" if data.empty?
|
27
|
+
|
12
28
|
data = data.force_encoding('binary') rescue data
|
29
|
+
url = nil
|
13
30
|
|
14
31
|
if data !~ /\<plist/
|
15
|
-
|
32
|
+
# Handle binary plist format
|
33
|
+
url = parse_binary_format(data, filename)
|
34
|
+
else
|
35
|
+
# Handle XML plist format
|
36
|
+
url = parse_xml_format(filename)
|
37
|
+
end
|
38
|
+
|
39
|
+
raise CorruptedFileError, "No URL found in webloc file: #{filename}" if url.nil? || url.empty?
|
40
|
+
new(url)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def self.parse_binary_format(data, filename)
|
46
|
+
offset = (data =~ /SURL_/)
|
47
|
+
raise InvalidFormatError, "Invalid binary webloc format - missing SURL marker in file: #{filename}" unless offset
|
48
|
+
|
49
|
+
begin
|
16
50
|
length_offset = 7
|
17
51
|
if data[offset + 5] == "\x10"
|
18
52
|
length = data[offset + 6]
|
@@ -21,16 +55,43 @@ class Webloc
|
|
21
55
|
length_offset = 8
|
22
56
|
length = data[offset + 6] + data[offset + 7]
|
23
57
|
length = length.unpack('S>')[0]
|
58
|
+
else
|
59
|
+
raise InvalidFormatError, "Unsupported length encoding in binary webloc file: #{filename}"
|
24
60
|
end
|
25
|
-
|
26
|
-
|
27
|
-
|
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}"
|
28
72
|
end
|
29
|
-
|
30
|
-
raise ArgumentError unless url
|
31
|
-
new(url)
|
32
73
|
end
|
33
74
|
|
75
|
+
def self.parse_xml_format(filename)
|
76
|
+
begin
|
77
|
+
plist_data = Plist::parse_xml(filename)
|
78
|
+
raise InvalidFormatError, "Invalid XML plist format - could not parse file: #{filename}" unless plist_data.is_a?(Hash)
|
79
|
+
|
80
|
+
url = plist_data['URL']
|
81
|
+
raise CorruptedFileError, "No 'URL' key found in plist file: #{filename}" unless url
|
82
|
+
|
83
|
+
url
|
84
|
+
rescue => e
|
85
|
+
if e.message.include?('parse') || e.message.include?('XML') || e.message.include?('plist')
|
86
|
+
raise InvalidFormatError, "Invalid XML plist format in file '#{filename}': #{e.message}"
|
87
|
+
else
|
88
|
+
raise CorruptedFileError, "Failed to parse XML webloc format in file '#{filename}': #{e.message}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
public
|
94
|
+
|
34
95
|
def data
|
35
96
|
# PLIST HEADER
|
36
97
|
@data = "bplist\x30\x30".bytes
|
@@ -73,6 +134,12 @@ class Webloc
|
|
73
134
|
end
|
74
135
|
|
75
136
|
def save(filename)
|
76
|
-
|
137
|
+
raise ArgumentError, "Filename cannot be nil or empty" if filename.nil? || filename.empty?
|
138
|
+
|
139
|
+
begin
|
140
|
+
File.open(filename, 'wb') { |f| f.write data }
|
141
|
+
rescue => e
|
142
|
+
raise WeblocError, "Failed to save webloc file '#{filename}': #{e.message}"
|
143
|
+
end
|
77
144
|
end
|
78
145
|
end
|
data/test/webloc_test.rb
CHANGED
@@ -6,6 +6,16 @@ class WeblocTest < Test::Unit::TestCase
|
|
6
6
|
def test_webloc_object_requires_url
|
7
7
|
assert_raise(ArgumentError) { Webloc.new }
|
8
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
|
9
19
|
|
10
20
|
def test_webloc_object_created_with_url
|
11
21
|
assert_equal 'http://example.com', Webloc.new('http://example.com').url
|
@@ -47,4 +57,74 @@ class WeblocTest < Test::Unit::TestCase
|
|
47
57
|
file.unlink
|
48
58
|
end
|
49
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
|
50
130
|
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.3.
|
4
|
+
version: 0.3.1
|
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
|
@@ -45,7 +44,6 @@ files:
|
|
45
44
|
homepage: https://github.com/peterc/webloc
|
46
45
|
licenses: []
|
47
46
|
metadata: {}
|
48
|
-
post_install_message:
|
49
47
|
rdoc_options: []
|
50
48
|
require_paths:
|
51
49
|
- lib
|
@@ -60,8 +58,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
58
|
- !ruby/object:Gem::Version
|
61
59
|
version: '0'
|
62
60
|
requirements: []
|
63
|
-
rubygems_version: 3.
|
64
|
-
signing_key:
|
61
|
+
rubygems_version: 3.6.7
|
65
62
|
specification_version: 4
|
66
63
|
summary: Reads and writes .webloc files on macOS
|
67
64
|
test_files:
|