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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c818dbf002be73a6bcf36920bfae42c98bd5185d8076e5835773f3b08f41fc69
4
- data.tar.gz: 78982a897fce013b29b15e66a64cdd003de478040d6399245942fd49858383e0
3
+ metadata.gz: 246849e44bba8ffbcd81036265a4cc813aa963dccf5cb092cb8551268f83b276
4
+ data.tar.gz: 833a7e93bc666b75ce56cfe58619229884eea236fda992a2a660647315eb093c
5
5
  SHA512:
6
- metadata.gz: 659e74f401a6ce48cac21e72bcf0b07e1c2ac2214579ed700d7715efbff9d68da082a043a5036193cd7e792d7f3a8a38a2ef6ac2780951129f1aa37781e74e8f
7
- data.tar.gz: 5799dbc3570c7395545a710f2f2f8bc4bb6f5cb79a681c4a9cb862647a489070fbdc35932a474d2fdac5253f9a090087da179cfe4e6365e32a1eb5efa26d5c0f
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
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2011-2024 Peter Cooper
3
+ Copyright (c) 2011-2026 Peter Cooper
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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 2.7 and up, including Ruby 3.x, and supports URLs of up to 2048 characters in length (and probably longer, but this is around the de facto limit for URLs in most systems).
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-2025 Peter Cooper
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
@@ -1,3 +1,3 @@
1
1
  class Webloc
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0"
3
3
  end
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.read(filename)
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 !~ /\<plist/
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
- offset = (data =~ /SURL_/)
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
- # PLIST HEADER
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.3.1
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
- - ".gitignore"
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: 3.6.7
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
@@ -1,4 +0,0 @@
1
- pkg/*
2
- *.gem
3
- .bundle
4
- .DS_Store
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- # Specify your gem's dependencies in webloc.gemspec
4
- gemspec
data/Rakefile DELETED
@@ -1,11 +0,0 @@
1
- require 'bundler'
2
- Bundler::GemHelper.install_tasks
3
-
4
- require 'rake/testtask'
5
- Rake::TestTask.new(:test) do |test|
6
- test.libs << 'lib' << 'test'
7
- test.pattern = 'test/**/*_test.rb'
8
- test.verbose = true
9
- end
10
-
11
- task :default => :test
data/test/oldstyle.webloc DELETED
Binary file
@@ -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