async-caldav 1.2.4 → 1.3.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/lib/async/caldav/client/addressbook.rb +21 -9
- data/lib/async/caldav/client/calendar.rb +39 -15
- data/lib/async/caldav/client.rb +423 -371
- data/lib/async/caldav/forward_auth.rb +61 -54
- data/lib/async/caldav/handlers/delete.rb +28 -30
- data/lib/async/caldav/handlers/get.rb +45 -47
- data/lib/async/caldav/handlers/head.rb +16 -18
- data/lib/async/caldav/handlers/mkcol.rb +64 -68
- data/lib/async/caldav/handlers/move.rb +99 -90
- data/lib/async/caldav/handlers/options.rb +15 -17
- data/lib/async/caldav/handlers/propfind.rb +140 -139
- data/lib/async/caldav/handlers/proppatch.rb +95 -90
- data/lib/async/caldav/handlers/put.rb +139 -119
- data/lib/async/caldav/handlers/report.rb +192 -161
- data/lib/async/caldav/server.rb +1029 -994
- data/lib/async/caldav/storage/filesystem.rb +247 -220
- data/lib/async/caldav/storage/mock.rb +254 -246
- data/lib/async/caldav/version.rb +1 -1
- metadata +19 -5
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "bundler/setup"
|
|
4
|
-
require "scampi"
|
|
5
|
-
require "async/caldav"
|
|
6
3
|
require 'json'
|
|
7
4
|
require 'fileutils'
|
|
8
5
|
|
|
@@ -15,7 +12,11 @@ module Async
|
|
|
15
12
|
@sync_snapshots = {}
|
|
16
13
|
FileUtils.mkdir_p(@root)
|
|
17
14
|
@root_normalized = ::File.expand_path(@root)
|
|
18
|
-
|
|
15
|
+
if @root_normalized.end_with?(::File::SEPARATOR)
|
|
16
|
+
@root_with_separator = @root_normalized
|
|
17
|
+
else
|
|
18
|
+
@root_with_separator = "#{@root_normalized}#{::File::SEPARATOR}"
|
|
19
|
+
end
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
# --- Collections ---
|
|
@@ -24,11 +25,11 @@ module Async
|
|
|
24
25
|
dir = full_path(path)
|
|
25
26
|
FileUtils.mkdir_p(dir)
|
|
26
27
|
meta = {
|
|
27
|
-
"type"
|
|
28
|
+
"type" => (props[:type] || :collection).to_s,
|
|
28
29
|
"displayname" => props[:displayname],
|
|
29
30
|
"description" => props[:description],
|
|
30
|
-
"color"
|
|
31
|
-
"props"
|
|
31
|
+
"color" => props[:color],
|
|
32
|
+
"props" => props[:props] || {},
|
|
32
33
|
}
|
|
33
34
|
File.write(File.join(dir, ".collection.json"), JSON.pretty_generate(meta))
|
|
34
35
|
symbolize(meta)
|
|
@@ -36,8 +37,11 @@ module Async
|
|
|
36
37
|
|
|
37
38
|
def get_collection(path)
|
|
38
39
|
meta_file = File.join(full_path(path), ".collection.json")
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
if File.exist?(meta_file)
|
|
41
|
+
symbolize(JSON.parse(File.read(meta_file)))
|
|
42
|
+
else
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
41
45
|
end
|
|
42
46
|
|
|
43
47
|
def delete_collection(path)
|
|
@@ -52,36 +56,49 @@ module Async
|
|
|
52
56
|
|
|
53
57
|
def list_collections(parent_path)
|
|
54
58
|
dir = full_path(parent_path)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
if File.directory?(dir)
|
|
60
|
+
Dir.children(dir).filter_map do |name|
|
|
61
|
+
child_dir = File.join(dir, name)
|
|
62
|
+
meta_file = File.join(child_dir, ".collection.json")
|
|
63
|
+
unless File.directory?(child_dir) && File.exist?(meta_file)
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
child_path = File.join(parent_path, name).sub(%r{/*$}, "/")
|
|
68
|
+
[child_path, symbolize(JSON.parse(File.read(meta_file)))]
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
[]
|
|
64
72
|
end
|
|
65
73
|
end
|
|
66
74
|
|
|
67
75
|
def update_collection(path, props)
|
|
68
76
|
col = get_collection(path)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
if col
|
|
78
|
+
if props.key?(:displayname)
|
|
79
|
+
col[:displayname] = props[:displayname]
|
|
80
|
+
end
|
|
81
|
+
if props.key?(:description)
|
|
82
|
+
col[:description] = props[:description]
|
|
83
|
+
end
|
|
84
|
+
if props.key?(:color)
|
|
85
|
+
col[:color] = props[:color]
|
|
86
|
+
end
|
|
87
|
+
if props.key?(:props)
|
|
88
|
+
col[:props] = (col[:props] || {}).merge(props[:props])
|
|
89
|
+
end
|
|
90
|
+
meta = {
|
|
91
|
+
"type" => col[:type].to_s,
|
|
92
|
+
"displayname" => col[:displayname],
|
|
93
|
+
"description" => col[:description],
|
|
94
|
+
"color" => col[:color],
|
|
95
|
+
"props" => col[:props] || {},
|
|
96
|
+
}
|
|
97
|
+
File.write(File.join(full_path(path), ".collection.json"), JSON.pretty_generate(meta))
|
|
98
|
+
col
|
|
99
|
+
else
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
85
102
|
end
|
|
86
103
|
|
|
87
104
|
def collection_exists?(path)
|
|
@@ -124,18 +141,24 @@ module Async
|
|
|
124
141
|
|
|
125
142
|
def list_items(collection_path)
|
|
126
143
|
dir = full_path(collection_path)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
144
|
+
if File.directory?(dir)
|
|
145
|
+
Dir.children(dir).filter_map do |name|
|
|
146
|
+
if name.start_with?(".")
|
|
147
|
+
next
|
|
148
|
+
end
|
|
149
|
+
file = File.join(dir, name)
|
|
150
|
+
unless File.file?(file)
|
|
151
|
+
next
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
item_path = File.join(collection_path, name)
|
|
155
|
+
body = File.read(file)
|
|
156
|
+
content_type = guess_content_type(name)
|
|
157
|
+
etag = Protocol::Caldav::ETag.compute(body)
|
|
158
|
+
[item_path, { body: body, content_type: content_type, etag: etag }]
|
|
159
|
+
end
|
|
160
|
+
else
|
|
161
|
+
[]
|
|
139
162
|
end
|
|
140
163
|
end
|
|
141
164
|
|
|
@@ -180,11 +203,11 @@ module Async
|
|
|
180
203
|
col = get_collection(collection_path) || {}
|
|
181
204
|
item_etags = items.map { |_, data| data[:etag] }
|
|
182
205
|
ctag = Protocol::Caldav::CTag.compute(
|
|
183
|
-
path:
|
|
206
|
+
path: collection_path,
|
|
184
207
|
displayname: col[:displayname],
|
|
185
208
|
description: col[:description],
|
|
186
|
-
color:
|
|
187
|
-
item_etags:
|
|
209
|
+
color: col[:color],
|
|
210
|
+
item_etags: item_etags,
|
|
188
211
|
)
|
|
189
212
|
token = "http://caldav.local/sync/#{ctag}"
|
|
190
213
|
@sync_snapshots[token] = { collection_path => snapshot }
|
|
@@ -193,69 +216,71 @@ module Async
|
|
|
193
216
|
|
|
194
217
|
def sync_changes(collection_path, token)
|
|
195
218
|
old_snapshot_entry = @sync_snapshots[token]
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
changes << [path, :modified]
|
|
219
|
+
if old_snapshot_entry
|
|
220
|
+
old_snapshot = old_snapshot_entry[collection_path] || {}
|
|
221
|
+
new_token = snapshot_sync(collection_path)
|
|
222
|
+
current_items = list_items(collection_path)
|
|
223
|
+
current = {}
|
|
224
|
+
current_items.each { |path, data| current[path] = data[:etag] }
|
|
225
|
+
changes = []
|
|
226
|
+
current.each do |path, etag|
|
|
227
|
+
if !old_snapshot.key?(path) || old_snapshot[path] != etag
|
|
228
|
+
changes << [path, :modified]
|
|
229
|
+
end
|
|
208
230
|
end
|
|
231
|
+
old_snapshot.each_key do |path|
|
|
232
|
+
unless current.key?(path)
|
|
233
|
+
changes << [path, :deleted]
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
[new_token, changes]
|
|
237
|
+
else
|
|
238
|
+
nil
|
|
209
239
|
end
|
|
210
|
-
old_snapshot.each_key do |path|
|
|
211
|
-
changes << [path, :deleted] unless current.key?(path)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
[new_token, changes]
|
|
215
240
|
end
|
|
216
241
|
|
|
217
242
|
private
|
|
218
243
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
244
|
+
def full_path(path)
|
|
245
|
+
if valid_path?(path)
|
|
246
|
+
expanded = ::File.expand_path(::File.join(@root, path))
|
|
247
|
+
if expanded == @root_normalized || expanded.start_with?(@root_with_separator)
|
|
248
|
+
expanded
|
|
249
|
+
else
|
|
250
|
+
raise ArgumentError, "path escapes root: #{path.inspect}"
|
|
251
|
+
end
|
|
224
252
|
else
|
|
225
|
-
raise ArgumentError, "path
|
|
253
|
+
raise ArgumentError, "invalid path: #{path.inspect}"
|
|
226
254
|
end
|
|
227
|
-
else
|
|
228
|
-
raise ArgumentError, "invalid path: #{path.inspect}"
|
|
229
255
|
end
|
|
230
|
-
end
|
|
231
256
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
257
|
+
def valid_path?(path)
|
|
258
|
+
path.is_a?(String) && path.valid_encoding? && !path.include?("\0")
|
|
259
|
+
end
|
|
235
260
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
261
|
+
def stat(path)
|
|
262
|
+
::File.stat(full_path(path))
|
|
263
|
+
rescue Errno::ENOENT, Errno::ELOOP
|
|
264
|
+
nil
|
|
265
|
+
end
|
|
241
266
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
267
|
+
def guess_content_type(path)
|
|
268
|
+
case File.extname(path).downcase
|
|
269
|
+
when ".ics" then "text/calendar"
|
|
270
|
+
when ".vcf" then "text/vcard"
|
|
271
|
+
else "application/octet-stream"
|
|
272
|
+
end
|
|
247
273
|
end
|
|
248
|
-
end
|
|
249
274
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
275
|
+
def symbolize(hash)
|
|
276
|
+
{
|
|
277
|
+
type: hash["type"]&.to_sym || :collection,
|
|
278
|
+
displayname: hash["displayname"],
|
|
279
|
+
description: hash["description"],
|
|
280
|
+
color: hash["color"],
|
|
281
|
+
props: hash["props"] || {},
|
|
282
|
+
}
|
|
283
|
+
end
|
|
259
284
|
end
|
|
260
285
|
end
|
|
261
286
|
end
|
|
@@ -263,156 +288,158 @@ end
|
|
|
263
288
|
|
|
264
289
|
require 'tmpdir'
|
|
265
290
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
291
|
+
__END__
|
|
292
|
+
|
|
293
|
+
require_relative "../../caldav"
|
|
294
|
+
|
|
295
|
+
describe "Async::Caldav::Storage::Filesystem" do
|
|
296
|
+
it "creates and retrieves a collection" do
|
|
297
|
+
Dir.mktmpdir do |dir|
|
|
298
|
+
s = Async::Caldav::Storage::Filesystem.new(dir)
|
|
299
|
+
s.create_collection("/cal/", type: :calendar, displayname: "Cal")
|
|
300
|
+
col = s.get_collection("/cal/")
|
|
301
|
+
col[:displayname].should.equal "Cal"
|
|
302
|
+
col[:type].should.equal :calendar
|
|
276
303
|
end
|
|
304
|
+
end
|
|
277
305
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
end
|
|
306
|
+
it "deletes collection and its items" do
|
|
307
|
+
Dir.mktmpdir do |dir|
|
|
308
|
+
s = Async::Caldav::Storage::Filesystem.new(dir)
|
|
309
|
+
s.create_collection("/cal/")
|
|
310
|
+
s.put_item("/cal/ev.ics", "data", "text/calendar")
|
|
311
|
+
s.delete_collection("/cal/")
|
|
312
|
+
s.get_collection("/cal/").should.be.nil
|
|
313
|
+
s.get_item("/cal/ev.ics").should.be.nil
|
|
287
314
|
end
|
|
315
|
+
end
|
|
288
316
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
end
|
|
317
|
+
it "lists direct child collections" do
|
|
318
|
+
Dir.mktmpdir do |dir|
|
|
319
|
+
s = Async::Caldav::Storage::Filesystem.new(dir)
|
|
320
|
+
s.create_collection("/admin/a/")
|
|
321
|
+
s.create_collection("/admin/b/")
|
|
322
|
+
s.list_collections("/admin/").length.should.equal 2
|
|
296
323
|
end
|
|
324
|
+
end
|
|
297
325
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
end
|
|
326
|
+
it "updates collection properties" do
|
|
327
|
+
Dir.mktmpdir do |dir|
|
|
328
|
+
s = Async::Caldav::Storage::Filesystem.new(dir)
|
|
329
|
+
s.create_collection("/cal/", displayname: "Old")
|
|
330
|
+
s.update_collection("/cal/", displayname: "New")
|
|
331
|
+
s.get_collection("/cal/")[:displayname].should.equal "New"
|
|
305
332
|
end
|
|
333
|
+
end
|
|
306
334
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
end
|
|
335
|
+
it "puts and retrieves an item with etag" do
|
|
336
|
+
Dir.mktmpdir do |dir|
|
|
337
|
+
s = Async::Caldav::Storage::Filesystem.new(dir)
|
|
338
|
+
item, is_new = s.put_item("/cal/ev.ics", "BEGIN:VCALENDAR", "text/calendar")
|
|
339
|
+
is_new.should.equal true
|
|
340
|
+
item[:etag].should.not.be.nil
|
|
341
|
+
s.get_item("/cal/ev.ics")[:body].should.equal "BEGIN:VCALENDAR"
|
|
315
342
|
end
|
|
343
|
+
end
|
|
316
344
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
345
|
+
it "persists across instances" do
|
|
346
|
+
Dir.mktmpdir do |dir|
|
|
347
|
+
s1 = Async::Caldav::Storage::Filesystem.new(dir)
|
|
348
|
+
s1.create_collection("/cal/", type: :calendar, displayname: "Persist")
|
|
349
|
+
s1.put_item("/cal/ev.ics", "body", "text/calendar")
|
|
322
350
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
end
|
|
351
|
+
s2 = Async::Caldav::Storage::Filesystem.new(dir)
|
|
352
|
+
s2.get_collection("/cal/")[:displayname].should.equal "Persist"
|
|
353
|
+
s2.get_item("/cal/ev.ics")[:body].should.equal "body"
|
|
327
354
|
end
|
|
355
|
+
end
|
|
328
356
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
end
|
|
357
|
+
it "list_items excludes hidden files" do
|
|
358
|
+
Dir.mktmpdir do |dir|
|
|
359
|
+
s = Async::Caldav::Storage::Filesystem.new(dir)
|
|
360
|
+
s.create_collection("/cal/")
|
|
361
|
+
s.put_item("/cal/ev.ics", "EVENT", "text/calendar")
|
|
362
|
+
items = s.list_items("/cal/")
|
|
363
|
+
items.map(&:first).any? { |p| p.include?(".collection.json") }.should.equal false
|
|
364
|
+
items.map(&:first).any? { |p| p.include?("ev.ics") }.should.equal true
|
|
338
365
|
end
|
|
366
|
+
end
|
|
339
367
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
end
|
|
368
|
+
it "deletes an item" do
|
|
369
|
+
Dir.mktmpdir do |dir|
|
|
370
|
+
s = Async::Caldav::Storage::Filesystem.new(dir)
|
|
371
|
+
s.put_item("/cal/ev.ics", "data", "text/calendar")
|
|
372
|
+
s.delete_item("/cal/ev.ics").should.equal true
|
|
373
|
+
s.get_item("/cal/ev.ics").should.be.nil
|
|
374
|
+
s.delete_item("/cal/ev.ics").should.equal false
|
|
348
375
|
end
|
|
376
|
+
end
|
|
349
377
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
end
|
|
378
|
+
it "moves an item" do
|
|
379
|
+
Dir.mktmpdir do |dir|
|
|
380
|
+
s = Async::Caldav::Storage::Filesystem.new(dir)
|
|
381
|
+
s.put_item("/cal/a.ics", "data", "text/calendar")
|
|
382
|
+
s.move_item("/cal/a.ics", "/cal/b.ics")
|
|
383
|
+
s.get_item("/cal/a.ics").should.be.nil
|
|
384
|
+
s.get_item("/cal/b.ics")[:body].should.equal "data"
|
|
358
385
|
end
|
|
386
|
+
end
|
|
359
387
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
end
|
|
388
|
+
it "reports existence correctly" do
|
|
389
|
+
Dir.mktmpdir do |dir|
|
|
390
|
+
s = Async::Caldav::Storage::Filesystem.new(dir)
|
|
391
|
+
s.exists?("/nope").should.equal false
|
|
392
|
+
s.create_collection("/col/")
|
|
393
|
+
s.exists?("/col/").should.equal true
|
|
394
|
+
s.put_item("/col/x.ics", "d", "text/calendar")
|
|
395
|
+
s.exists?("/col/x.ics").should.equal true
|
|
369
396
|
end
|
|
397
|
+
end
|
|
370
398
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
end
|
|
399
|
+
it "get_multi returns items and nils" do
|
|
400
|
+
Dir.mktmpdir do |dir|
|
|
401
|
+
s = Async::Caldav::Storage::Filesystem.new(dir)
|
|
402
|
+
s.put_item("/cal/a.ics", "A", "text/calendar")
|
|
403
|
+
result = s.get_multi(["/cal/a.ics", "/cal/nope.ics"])
|
|
404
|
+
result.length.should.equal 2
|
|
405
|
+
result[0][1][:body].should.equal "A"
|
|
406
|
+
result[1][1].should.be.nil
|
|
380
407
|
end
|
|
408
|
+
end
|
|
381
409
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
end
|
|
410
|
+
it "guesses content types from extension" do
|
|
411
|
+
Dir.mktmpdir do |dir|
|
|
412
|
+
s = Async::Caldav::Storage::Filesystem.new(dir)
|
|
413
|
+
s.put_item("/cal/ev.ics", "cal", "text/calendar")
|
|
414
|
+
s.put_item("/addr/c.vcf", "card", "text/vcard")
|
|
415
|
+
s.get_item("/cal/ev.ics")[:content_type].should.equal "text/calendar"
|
|
416
|
+
s.get_item("/addr/c.vcf")[:content_type].should.equal "text/vcard"
|
|
390
417
|
end
|
|
418
|
+
end
|
|
391
419
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
end
|
|
420
|
+
it "rejects path traversal attempts" do
|
|
421
|
+
Dir.mktmpdir do |dir|
|
|
422
|
+
s = Async::Caldav::Storage::Filesystem.new(dir)
|
|
423
|
+
lambda { s.get_item("/../../etc/passwd") }.should.raise ArgumentError
|
|
424
|
+
lambda { s.put_item("/cal/../../escape.ics", "x", "text/calendar") }.should.raise ArgumentError
|
|
398
425
|
end
|
|
426
|
+
end
|
|
399
427
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
end
|
|
428
|
+
it "rejects NUL byte in path" do
|
|
429
|
+
Dir.mktmpdir do |dir|
|
|
430
|
+
s = Async::Caldav::Storage::Filesystem.new(dir)
|
|
431
|
+
lambda { s.get_item("/cal\0/x.ics") }.should.raise ArgumentError
|
|
405
432
|
end
|
|
433
|
+
end
|
|
406
434
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
end
|
|
435
|
+
it "same body produces same etag across backends" do
|
|
436
|
+
body = "BEGIN:VCALENDAR\r\nEND:VCALENDAR"
|
|
437
|
+
mock = Async::Caldav::Storage::Mock.new
|
|
438
|
+
mock.put_item("/a.ics", body, "text/calendar")
|
|
439
|
+
Dir.mktmpdir do |dir|
|
|
440
|
+
fs = Async::Caldav::Storage::Filesystem.new(dir)
|
|
441
|
+
fs.put_item("/a.ics", body, "text/calendar")
|
|
442
|
+
mock.etag("/a.ics").should.equal fs.etag("/a.ics")
|
|
416
443
|
end
|
|
417
444
|
end
|
|
418
|
-
end
|
|
445
|
+
end
|