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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 498d11e48961e5843a3a25fafb71f4738ebc10733037145ea30a17f176abcaa2
4
- data.tar.gz: 78beeadfdafd1a7f414fc6f7b69c6fd24819ed79bb39a9148e72e1bc38089416
3
+ metadata.gz: c818dbf002be73a6bcf36920bfae42c98bd5185d8076e5835773f3b08f41fc69
4
+ data.tar.gz: 78982a897fce013b29b15e66a64cdd003de478040d6399245942fd49858383e0
5
5
  SHA512:
6
- metadata.gz: 99b273c4af784bdafe2cde8166da2c3672a263d2e412f0f1547e70ef4b8bf8de74185a13b1050e5c16b713e3f7b8c67baabe9f25b1366ee4e86876a95a368740
7
- data.tar.gz: fdbd68bb1239b9b35b1eb8c3d3ef8302d69d8a48eca6830bf636eb693f6e8c9a35e19af02148fda21a2d5a2ce3be1e3b7bef8186d7917dd8c40f73a699557e60
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(ARGV.first).url
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-2024 Peter Cooper
128
+ Copyright (C) 2011-2025 Peter Cooper
29
129
 
30
130
  webloc is licensed under the terms of the MIT License
@@ -1,3 +1,3 @@
1
1
  class Webloc
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
3
3
  end
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
- data = File.read(filename)
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
- offset = (data =~ /SURL_/)
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
- url = data[offset + length_offset,length]
26
- else
27
- url = Plist::parse_xml(filename)['URL'] rescue nil
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
- File.open(filename, 'wb') { |f| f.write data }
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.0
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: 2024-05-01 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: 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.5.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: