ucf 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Changes.rdoc CHANGED
@@ -1,5 +1,24 @@
1
1
  = Changes log for the UCF Ruby Gem
2
2
 
3
+ == Version 0.1.0
4
+
5
+ * Improvements to the reserved names code to allow sub-classing.
6
+ * Move exceptions to a new source file.
7
+ * Use a base class for UCF exceptions.
8
+ * Standardize the MalformedUCFError exception messages.
9
+ * Add an exception for clashes with reserved names.
10
+ * Implement Container#add to avoid using reserved names.
11
+ * Raise an exception if renaming to a reserved name.
12
+ * Implement Container#mkdir to avoid using reserved names.
13
+ * Make sure testing reserved names copes with trailing slashes.
14
+ * Fake up the connection to ZipFileSystem.
15
+ * Implement Container#get_output_stream to respect reserved names.
16
+ * Can now set comments on the UCF document.
17
+ * Implement the close and commit methods.
18
+ * Forward the commit_required? method.
19
+ * Separate the opening and checking of a UCF document.
20
+ * Documentation improvements and cleanup.
21
+
3
22
  == Version 0.0.2
4
23
 
5
24
  * Update the main ReadMe file.
data/ReadMe.rdoc CHANGED
@@ -7,6 +7,7 @@ Source code:: https://github.com/myGrid/ruby-ucf
7
7
  Licence:: BSD (See Licence file or http://www.opensource.org/licenses/bsd-license.php)
8
8
  Copyright:: (c) 2013 The University of Manchester, UK
9
9
 
10
+ {<img src="https://codeclimate.com/github/myGrid/ruby-ucf.png" />}[https://codeclimate.com/github/myGrid/ruby-ucf]
10
11
 
11
12
  == Synopsis
12
13
 
@@ -28,12 +29,12 @@ directory. See the contents of the tests directory for even more.
28
29
  The basic requirements of a UCF document are all implemented but there are a
29
30
  number of optional features that are not yet provided.
30
31
 
31
- * In memory operation. Presently all operations are performed on documents
32
- that are resident on disk.
32
+ * Memory resident UCF documents. Presently all operations are performed on
33
+ documents that are resident on disk as the underlying
34
+ {rubyzip library}[https://github.com/aussiegeek/rubyzip] currently
35
+ {cannot do anything else}[https://github.com/aussiegeek/rubyzip/issues/74].
33
36
  * META-INF directory support. Everything within the META-INF directory is
34
37
  optional but will supported in a near future version.
35
- * Changing the mimetype. It is not certain that this is a sensible operation
36
- but is being considered for a future version.
37
38
  * Digital signatures (this feature has been deferred until a future revision
38
39
  of the UCF specification. It will be supported by this gem when it is added
39
40
  to the specification).
@@ -51,6 +51,9 @@ begin
51
51
 
52
52
  # Copy this example code in straight from a file.
53
53
  c.add("dir/code.rb", __FILE__)
54
+
55
+ # Add a explanation of this file.
56
+ c.comment = "This is an example UCF file!"
54
57
  end
55
58
  rescue UCF::MalformedUCFError, Zip::ZipError => err
56
59
  puts err.to_s
data/lib/ucf/container.rb CHANGED
@@ -35,53 +35,58 @@ require 'zip/zipfilesystem'
35
35
 
36
36
  module UCF
37
37
 
38
- # This class represents a UCF file in PK Zip format. See
39
- # https://learn.adobe.com/wiki/display/PDFNAV/Universal+Container+Format
38
+ # This class represents a UCF document in PK Zip format. See
39
+ # {the specification}[https://learn.adobe.com/wiki/display/PDFNAV/Universal+Container+Format]
40
40
  # for more details.
41
41
  #
42
- # This class mostly provides all the facilities of the <tt>Zip::ZipFile</tt>
43
- # class in the rubyzip gem. Please also consult the rubyzip documentation:
44
- # http://rubydoc.info/gems/rubyzip/0.9.9/frames
42
+ # This class provides most of the facilities of the <tt>Zip::ZipFile</tt>
43
+ # class in the rubyzip gem. Please also consult the
44
+ # {rubyzip documentation}[http://rubydoc.info/gems/rubyzip/0.9.9/frames]
45
+ # alongside these pages.
45
46
  #
46
47
  # There are code examples available with the source code of this library.
47
48
  class Container
48
49
 
49
50
  extend Forwardable
50
- def_delegators :@zipfile, :add, :close, :comment, :commit, :dir, :each,
51
- :extract, :file, :find_entry, :get_entry, :get_input_stream,
52
- :get_output_stream, :glob, :mkdir, :name, :read, :size
51
+ def_delegators :@zipfile, :comment, :comment=, :commit_required?, :each,
52
+ :extract, :find_entry, :get_entry, :get_input_stream, :glob, :name,
53
+ :read, :size
53
54
 
54
55
  private_class_method :new
55
56
 
56
- # The mime-type of this UCF file. By default this is
57
+ # The mime-type of this UCF document. By default this is
57
58
  # "application/epub+zip".
58
59
  attr_reader :mimetype
59
60
 
60
61
  # :stopdoc:
61
62
  DEFAULT_MIMETYPE = "application/epub+zip"
62
63
 
63
- # Reserved root file names. File names in UCF documents are
64
- # case-insensitive so downcase where required in the reserved list.
64
+ # Reserved file and directory names for standard UCF documents.
65
65
  MIMETYPE_FILE = "mimetype"
66
66
  META_INF_DIR = "META-INF"
67
- RESERVED_ROOT_NAMES = [MIMETYPE_FILE, META_INF_DIR.downcase]
68
67
 
69
- ERR_MT_NONE = "Not a UCF file. 'mimetype' file is missing."
70
- ERR_MT_BAD_OFF = "Not a UCF file. 'mimetype' file is not at offset 0."
71
- ERR_MT_BAD_COMP = "Not a UCF file. 'mimetype' file is compressed."
72
-
73
- def initialize(filename)
74
- @zipfile = open_and_check_ucf(filename)
68
+ def initialize(document)
69
+ @zipfile = open_document(document)
70
+ check_document!
75
71
 
76
72
  @mimetype = read_mimetype
73
+ @on_disk = true
74
+
75
+ # Here we fake up the connection to the rubyzip filesystem classes so
76
+ # that they also respect the reserved names that we define.
77
+ mapped_zip = ::Zip::ZipFileSystem::ZipFileNameMapper.new(self)
78
+ @fs_dir = ::Zip::ZipFileSystem::ZipFsDir.new(mapped_zip)
79
+ @fs_file = ::Zip::ZipFileSystem::ZipFsFile.new(mapped_zip)
80
+ @fs_dir.file = @fs_file
81
+ @fs_file.dir = @fs_dir
77
82
  end
78
83
  # :startdoc:
79
84
 
80
85
  # :call-seq:
81
- # Container.create(filename, mimetype = "application/epub+zip") -> container
82
- # Container.create(filename, mimetype = "application/epub+zip") {|container| ...}
86
+ # Container.create(filename, mimetype = "application/epub+zip") -> document
87
+ # Container.create(filename, mimetype = "application/epub+zip") {|document| ...}
83
88
  #
84
- # Create a new UCF file on disk with the specified mimetype.
89
+ # Create a new UCF document on disk with the specified mimetype.
85
90
  def Container.create(filename, mimetype = DEFAULT_MIMETYPE, &block)
86
91
  ::Zip::ZipOutputStream.open(filename) do |stream|
87
92
  stream.put_next_entry(MIMETYPE_FILE, nil, nil, ::Zip::ZipEntry::STORED)
@@ -113,11 +118,11 @@ module UCF
113
118
  end
114
119
 
115
120
  # :call-seq:
116
- # Container.open(filename) -> container
117
- # Container.open(filename) {|container| ...}
121
+ # Container.open(filename) -> document
122
+ # Container.open(filename) {|document| ...}
118
123
  #
119
- # Open an existing UCF file from disk. It will be checked for conformance
120
- # to the UCF specification upon first access.
124
+ # Open an existing UCF document from disk. It will be checked for
125
+ # conformance to the UCF specification upon first access.
121
126
  def Container.open(filename, &block)
122
127
  c = new(filename)
123
128
 
@@ -135,9 +140,9 @@ module UCF
135
140
  # :call-seq:
136
141
  # Container.verify(filename) -> boolean
137
142
  #
138
- # Verify that the specified UCF file conforms to the UCF specification.
139
- # This method returns +false+ if there are any problems at all with the
140
- # file (including if it can't be found) or +true+ if it conforms.
143
+ # Verify that the specified UCF document conforms to the UCF
144
+ # specification. This method returns +false+ if there are any problems at
145
+ # all with the file (including if it cannot be found).
141
146
  def Container.verify(filename)
142
147
  begin
143
148
  Container.verify!(filename)
@@ -151,128 +156,261 @@ module UCF
151
156
  # :call-seq:
152
157
  # Container.verify!(filename)
153
158
  #
154
- # Verify that the specified UCF file conforms to the UCF specification.
155
- # This method raises exceptions when errors are found or if there is
156
- # something fundamental wrong with the file itself (e.g. not found).
159
+ # Verify that the specified UCF document conforms to the UCF
160
+ # specification. This method raises exceptions when errors are found or if
161
+ # there is something fundamental wrong with the file itself (e.g. it
162
+ # cannot be found).
157
163
  def Container.verify!(filename)
158
164
  new(filename).close
159
165
  nil
160
166
  end
161
167
 
168
+ # :call-seq:
169
+ # add(entry, src_path, &continue_on_exists_proc)
170
+ #
171
+ # Convenience method for adding the contents of a file to the UCF
172
+ # document. If asked to add a file with a reserved name, such as the
173
+ # special mimetype header file, this method will raise a
174
+ # ReservedNameClashError.
175
+ #
176
+ # See the rubyzip documentation for details of the
177
+ # +continue_on_exists_proc+ parameter.
178
+ def add(entry, src_path, &continue_on_exists_proc)
179
+ raise ReservedNameClashError.new(entry.to_s) if reserved_entry?(entry)
180
+
181
+ @zipfile.add(entry, src_path, &continue_on_exists_proc)
182
+ end
183
+
184
+ # :call-seq:
185
+ # commit -> boolean
186
+ # close -> boolean
187
+ #
188
+ # Commits changes that have been made since the previous commit to the
189
+ # UCF document. Returns +true+ if anything was actually done, +false+
190
+ # otherwise.
191
+ def commit
192
+ return false unless commit_required?
193
+
194
+ if on_disk?
195
+ @zipfile.commit
196
+ end
197
+ end
198
+
199
+ alias :close :commit
200
+
201
+ # :call-seq:
202
+ # dir -> Zip::ZipFsDir
203
+ #
204
+ # Returns an object which can be used like ruby's built in +Dir+ (class)
205
+ # object, except that it works on the UCF document on which this method is
206
+ # invoked.
207
+ #
208
+ # See the rubyzip documentation for details.
209
+ def dir
210
+ @fs_dir
211
+ end
212
+
213
+ # :call-seq:
214
+ # file -> Zip::ZipFsFile
215
+ #
216
+ # Returns an object which can be used like ruby's built in +File+ (class)
217
+ # object, except that it works on the UCF document on which this method is
218
+ # invoked.
219
+ #
220
+ # See the rubyzip documentation for details.
221
+ def file
222
+ @fs_file
223
+ end
224
+
225
+ # :call-seq:
226
+ # get_output_stream(entry, permission = nil) -> stream
227
+ # get_output_stream(entry, permission = nil) {|stream| ...}
228
+ #
229
+ # Returns an output stream to the specified entry. If a block is passed
230
+ # the stream object is passed to the block and the stream is automatically
231
+ # closed afterwards just as with ruby's built-in +File.open+ method.
232
+ #
233
+ # See the rubyzip documentation for details of the +permission_int+
234
+ # parameter.
235
+ def get_output_stream(entry, permission = nil, &block)
236
+ raise ReservedNameClashError.new(entry.to_s) if reserved_entry?(entry)
237
+
238
+ @zipfile.get_output_stream(entry, permission, &block)
239
+ end
240
+
241
+ # :call-seq:
242
+ # in_memory? -> boolean
243
+ #
244
+ # Is this UCF document memory resident as opposed to stored on disk?
245
+ def in_memory?
246
+ !@on_disk
247
+ end
248
+
249
+ # :call-seq:
250
+ # mkdir(name, permission = 0755)
251
+ #
252
+ # Creates a directory in the UCF document. If asked to create a directory
253
+ # with a reserved name this method will raise a ReservedNameClashError.
254
+ #
255
+ # The new directory will be created with the supplied unix-style
256
+ # permissions. The default (+0755+) is owner read, write and list; group
257
+ # read and list; and world read and list.
258
+ def mkdir(name, permission = 0755)
259
+ raise ReservedNameClashError.new(name) if reserved_entry?(name)
260
+
261
+ @zipfile.mkdir(name, permission)
262
+ end
263
+
264
+ # :call-seq:
265
+ # on_disk? -> boolean
266
+ #
267
+ # Is this UCF document stored on disk as opposed to memory resident?
268
+ def on_disk?
269
+ @on_disk
270
+ end
271
+
162
272
  # :call-seq:
163
273
  # remove(entry)
164
274
  #
165
- # Removes the specified entry. If asked to remove any reserved files such
166
- # as the special mimetype header file this method will do nothing.
275
+ # Removes the specified entry from the UCF document. If asked to remove
276
+ # any reserved files such as the special mimetype header file this method
277
+ # will do nothing.
167
278
  def remove(entry)
168
279
  return if reserved_entry?(entry)
169
280
  @zipfile.remove(entry)
170
281
  end
171
282
 
172
283
  # :call-seq:
173
- # rename(entry, new_name, &continueOnExistsProc)
284
+ # rename(entry, new_name, &continue_on_exists_proc)
174
285
  #
175
- # Renames the specified entry. If asked to rename any reserved files such
176
- # as the special mimetype header file this method will do nothing. See the
177
- # rubyzip documentation for details of the +continue_on_exists_proc+
178
- # parameter.
286
+ # Renames the specified entry in the UCF document. If asked to rename any
287
+ # reserved files such as the special mimetype header file this method will
288
+ # do nothing. If asked to rename a file _to_ one of the reserved names a
289
+ # ReservedNameClashError is raised.
290
+ #
291
+ # See the rubyzip documentation for details of the
292
+ # +continue_on_exists_proc+ parameter.
179
293
  def rename(entry, new_name, &continue_on_exists_proc)
180
294
  return if reserved_entry?(entry)
181
- @zipfile.rename(entry, new_name, continue_on_exists_proc)
295
+ raise ReservedNameClashError.new(new_name) if reserved_entry?(new_name)
296
+
297
+ @zipfile.rename(entry, new_name, &continue_on_exists_proc)
182
298
  end
183
299
 
184
300
  # :call-seq:
185
301
  # replace(entry, src_path)
186
302
  #
187
- # Replaces the specified entry with the contents of +src_path+ (from the
188
- # file system). If asked to replace any reserved files such as the special
189
- # mimetype header file this method will do nothing.
303
+ # Replaces the specified entry of the UCF document with the contents of
304
+ # +src_path+ (from the file system). If asked to replace any reserved
305
+ # files such as the special mimetype header file this method will do
306
+ # nothing.
190
307
  def replace(entry, src_path)
191
308
  return if reserved_entry?(entry)
192
309
  @zipfile.replace(entry, src_path)
193
310
  end
194
311
 
312
+ # :call-seq:
313
+ # reserved_files -> Array
314
+ #
315
+ # Return a list of reserved file names for this UCF document.
316
+ #
317
+ # When creating a more specialized sub-class of this class then this
318
+ # method should be overridden to add any extra reserved file names.
319
+ def reserved_files
320
+ [MIMETYPE_FILE]
321
+ end
322
+
323
+ # :call-seq:
324
+ # reserved_directories -> Array
325
+ #
326
+ # Return a list of reserved directory names for this UCF document.
327
+ #
328
+ # When creating a more specialized sub-class of this class then this
329
+ # method should be overridden to add any extra reserved directory names.
330
+ def reserved_directories
331
+ [META_INF_DIR]
332
+ end
333
+
334
+ # :call-seq:
335
+ # reserved_entry?(entry) -> boolean
336
+ #
337
+ # Is the given entry name in the reserved list of file or directory names?
338
+ def reserved_entry?(entry)
339
+ name = entry.kind_of?(::Zip::ZipEntry) ? entry.name : entry
340
+ name.chop! if name.end_with? "/"
341
+ reserved_names.map { |n| n.downcase }.include? name.downcase
342
+ end
343
+
344
+ # :call-seq:
345
+ # reserved_names -> Array
346
+ #
347
+ # Return a list of reserved file and directory names for this UCF
348
+ # document.
349
+ #
350
+ # In practice this method simply returns the joined lists of reserved file
351
+ # and directory names.
352
+ def reserved_names
353
+ reserved_files + reserved_directories
354
+ end
355
+
195
356
  # :call-seq:
196
357
  # to_s -> String
197
358
  #
198
- # Return a String representation of this UCF file.
359
+ # Return a textual summary of this UCF document.
199
360
  def to_s
200
361
  @zipfile.to_s + " - #{@mimetype}"
201
362
  end
202
363
 
203
364
  private
204
365
 
205
- def open_and_check_ucf(filename)
206
- file = ::Zip::ZipFile.new(filename)
366
+ def open_document(document)
367
+ ::Zip::ZipFile.new(document)
368
+ end
207
369
 
370
+ def check_document!
208
371
  # Check mimetype file is present and correct.
209
- entry = file.find_entry(MIMETYPE_FILE)
210
- raise MalformedUCFError.new(ERR_MT_NONE) if entry.nil?
211
- raise MalformedUCFError.new(ERR_MT_BAD_OFF) if entry.localHeaderOffset != 0
372
+ entry = @zipfile.find_entry(MIMETYPE_FILE)
373
+
374
+ raise MalformedUCFError.new("'mimetype' file is missing.") if entry.nil?
375
+ if entry.localHeaderOffset != 0
376
+ raise MalformedUCFError.new("'mimetype' file is not at offset 0 in the archive.")
377
+ end
212
378
  if entry.compression_method != ::Zip::ZipEntry::STORED
213
- raise MalformedUCFError.new(ERR_MT_BAD_COMP)
379
+ raise MalformedUCFError.new("'mimetype' file is compressed.")
214
380
  end
215
381
 
216
- file
382
+ true
217
383
  end
218
384
 
219
385
  def read_mimetype
220
386
  @zipfile.read(MIMETYPE_FILE)
221
387
  end
222
388
 
223
- # Remember that file names in UCF documents are case-insensitive so
224
- # compare downcased versions.
225
- def reserved_entry?(entry)
226
- name = entry.kind_of?(::Zip::ZipEntry) ? entry.name : entry
227
- RESERVED_ROOT_NAMES.include? name.downcase
228
- end
229
-
230
389
  public
231
390
 
232
391
  # Lots of extra docs out of the way at the end here...
233
392
 
234
- ##
235
- # :method: add
236
- # :call-seq:
237
- # add(entry, src_path, &continue_on_exists_proc)
238
- #
239
- # Convenience method for adding the contents of a file to the UCF file.
240
- #
241
- # See the rubyzip documentation for details of the
242
- # +continue_on_exists_proc+ parameter.
243
-
244
- ##
245
- # :method: close
246
- # :call-seq:
247
- # close
248
- #
249
- # Closes the UCF file committing any changes that have been made.
250
-
251
393
  ##
252
394
  # :method: comment
253
395
  # :call-seq:
254
396
  # comment -> String
255
397
  #
256
- # Returns the UCF file comment, if it has one.
398
+ # Returns the UCF document comment, if it has one.
257
399
 
258
400
  ##
259
- # :method: commit
401
+ # :method: comment=
260
402
  # :call-seq:
261
- # commit
403
+ # comment = comment
262
404
  #
263
- # Commits changes that have been made since the previous commit to the
264
- # UCF file.
405
+ # Set the UCF document comment to the new value.
265
406
 
266
407
  ##
267
- # :method: dir
408
+ # :method: commit_required?
268
409
  # :call-seq:
269
- # dir -> Zip::ZipFsDir
410
+ # commit_required? -> boolean
270
411
  #
271
- # Returns an object which can be used like ruby's built in +Dir+ (class)
272
- # object, except that it works on the UCF file on which this method is
273
- # invoked.
274
- #
275
- # See the rubyzip documentation for details.
412
+ # Returns +true+ if any changes have been made to this UCF document since
413
+ # the last commit, +false+ otherwise.
276
414
 
277
415
  ##
278
416
  # :method: each
@@ -289,37 +427,26 @@ module UCF
289
427
  # :call-seq:
290
428
  # extract(entry, dest_path, &on_exists_proc)
291
429
  #
292
- # Extracts the specified entry to +dest_path+.
430
+ # Extracts the specified entry of the UCF document to +dest_path+.
293
431
  #
294
432
  # See the rubyzip documentation for details of the +on_exists_proc+
295
433
  # parameter.
296
434
 
297
- ##
298
- # :method: file
299
- # :call-seq:
300
- # dir -> Zip::ZipFsFile
301
- #
302
- # Returns an object which can be used like ruby's built in +File+ (class)
303
- # object, except that it works on the UCF file on which this method is
304
- # invoked.
305
- #
306
- # See the rubyzip documentation for details.
307
-
308
435
  ##
309
436
  # :method: find_entry
310
437
  # :call-seq:
311
438
  # find_entry(entry) -> Zip::ZipEntry
312
439
  #
313
- # Searches for entries with the specified name. Returns +nil+ if no entry
314
- # is found. See also +get_entry+.
440
+ # Searches for entries within the UCF document with the specified name.
441
+ # Returns +nil+ if no entry is found. See also +get_entry+.
315
442
 
316
443
  ##
317
444
  # :method: get_entry
318
445
  # :call-seq:
319
446
  # get_entry(entry) -> Zip::ZipEntry
320
447
  #
321
- # Searches for an entry like +find_entry+, but throws +Errno::ENOENT+ if
322
- # no entry is found.
448
+ # Searches for an entry within the UCF document in a similar manner to
449
+ # +find_entry+, but throws +Errno::ENOENT+ if no entry is found.
323
450
 
324
451
  ##
325
452
  # :method: get_input_stream
@@ -331,46 +458,23 @@ module UCF
331
458
  # stream object is passed to the block and the stream is automatically
332
459
  # closed afterwards just as with ruby's built in +File.open+ method.
333
460
 
334
- ##
335
- # :method: get_output_stream
336
- # :call-seq:
337
- # get_output_stream(entry, permission_int = nil) -> stream
338
- # get_output_stream(entry, permission_int = nil) {|stream| ...}
339
- #
340
- # Returns an output stream to the specified entry. If a block is passed
341
- # the stream object is passed to the block and the stream is automatically
342
- # closed afterwards just as with ruby's built-in +File.open+ method.
343
- #
344
- # See the rubyzip documentation for details of the +permission_int+
345
- # parameter.
346
-
347
461
  ##
348
462
  # :method: glob
349
463
  # :call-seq:
350
464
  # glob(*args) -> Array of Zip::ZipEntry
351
465
  # glob(*args) {|entry| ...}
352
466
  #
353
- # Searches for entries given a glob.
467
+ # Searches for entries within the UCF document that match the given glob.
354
468
  #
355
469
  # See the rubyzip documentation for details of the parameters that can be
356
470
  # passed in.
357
471
 
358
- ##
359
- # :method: mkdir
360
- # :call-seq:
361
- # mkdir(entryName, permission_int = 0755)
362
- #
363
- # Creates a directory.
364
- #
365
- # See the rubyzip documentation for details of the +permission_int+
366
- # parameter.
367
-
368
472
  ##
369
473
  # :method: name
370
474
  # :call-seq:
371
475
  # name -> String
372
476
  #
373
- # Returns the filename of this UCF file.
477
+ # Returns the filename of this UCF document.
374
478
 
375
479
  ##
376
480
  # :method: read
@@ -0,0 +1,68 @@
1
+ # Copyright (c) 2013 The University of Manchester, UK.
2
+ #
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # * Redistributions in binary form must reproduce the above copyright notice,
12
+ # this list of conditions and the following disclaimer in the documentation
13
+ # and/or other materials provided with the distribution.
14
+ #
15
+ # * Neither the names of The University of Manchester nor the names of its
16
+ # contributors may be used to endorse or promote products derived from this
17
+ # software without specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
+ # POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+ # Author: Robert Haines
32
+
33
+ module UCF
34
+
35
+ # The base class of all other exceptions raised by this library.
36
+ class UCFError < RuntimeError
37
+ end
38
+
39
+ # This exception is raised when a bad UCF is detected.
40
+ class MalformedUCFError < UCFError
41
+
42
+ # :call-seq:
43
+ # new(reason = "")
44
+ #
45
+ # Create a new MalformedUCFError with an optional reason for why the UCF
46
+ # document is malformed.
47
+ def initialize(reason = nil)
48
+ if reason.nil?
49
+ super("Malformed UCF Document.")
50
+ else
51
+ super("Malformed UCF Document: #{reason}")
52
+ end
53
+ end
54
+ end
55
+
56
+ # This exception is raised when a clash occurs with a reserved name.
57
+ class ReservedNameClashError < UCFError
58
+
59
+ # :call-seq:
60
+ # new(name)
61
+ #
62
+ # Create a new ReservedNameClashError with the name of the clash supplied.
63
+ def initialize(name)
64
+ super("'#{name}' is reserved for internal use in this UCF document.")
65
+ end
66
+ end
67
+
68
+ end
data/lib/ucf.rb CHANGED
@@ -31,13 +31,15 @@
31
31
  # Author: Robert Haines
32
32
 
33
33
  require 'yaml'
34
+ require 'ucf/exceptions'
34
35
  require 'ucf/container'
35
36
 
36
37
  # This is a ruby library to read and write UCF files in PK Zip format. See the
37
38
  # UCF::Container class for more information.
38
39
  #
39
- # See https://learn.adobe.com/wiki/display/PDFNAV/Universal+Container+Format
40
- # for more details on the UCF.
40
+ # See
41
+ # {the UCF specification}[https://learn.adobe.com/wiki/display/PDFNAV/Universal+Container+Format]
42
+ # for more details.
41
43
  module UCF
42
44
 
43
45
  # Library version information.
@@ -50,12 +52,4 @@ module UCF
50
52
  STRING = [:major, :minor, :patch].map {|d| INFO[d]}.compact.join('.')
51
53
  end
52
54
 
53
- # Exception raised when a bad UCF is detected.
54
- class MalformedUCFError < RuntimeError
55
- # :stopdoc:
56
- def initialize(message = "")
57
- super(message)
58
- end
59
- # :startdoc:
60
- end
61
55
  end
Binary file
data/test/tc_create.rb CHANGED
@@ -42,6 +42,9 @@ class TestCreation < Test::Unit::TestCase
42
42
 
43
43
  assert_nothing_raised do
44
44
  UCF::Container.create(filename) do |c|
45
+ assert(c.on_disk?)
46
+ refute(c.in_memory?)
47
+
45
48
  assert(c.find_entry("mimetype").localHeaderOffset == 0)
46
49
  end
47
50
  end
@@ -61,6 +64,9 @@ class TestCreation < Test::Unit::TestCase
61
64
 
62
65
  assert_nothing_raised do
63
66
  UCF::Container.create(filename) do |c|
67
+ assert(c.on_disk?)
68
+ refute(c.in_memory?)
69
+
64
70
  assert(c.find_entry("mimetype").localHeaderOffset == 0)
65
71
  end
66
72
  end
@@ -71,31 +77,60 @@ class TestCreation < Test::Unit::TestCase
71
77
  end
72
78
  end
73
79
 
74
- # Check creation of stuff in ucf files.
80
+ # Check creation of stuff in ucf files. Check the commit status a few times
81
+ # to ensure that what we expect to happen, happens.
75
82
  def test_create_contents_file
76
83
  Dir.mktmpdir do |dir|
77
84
  filename = File.join(dir, "test.ucf")
78
85
 
79
86
  assert_nothing_raised do
80
87
  UCF::Container.create(filename) do |ucf|
88
+ assert(ucf.on_disk?)
89
+ refute(ucf.in_memory?)
90
+
81
91
  ucf.file.open("test.txt", "w") do |f|
82
92
  f.print "testing"
83
93
  end
84
94
 
95
+ assert(ucf.commit_required?)
96
+ assert(ucf.commit)
97
+ refute(ucf.commit_required?)
98
+ refute(ucf.commit)
99
+
85
100
  ucf.dir.mkdir("dir1")
86
101
  ucf.mkdir("dir2")
102
+
103
+ assert(ucf.commit_required?)
104
+ assert(ucf.commit)
105
+ refute(ucf.commit_required?)
106
+ refute(ucf.commit)
107
+
108
+ ucf.comment = "A comment!"
109
+
110
+ assert(ucf.commit_required?)
111
+ assert(ucf.commit)
112
+ refute(ucf.commit_required?)
113
+ refute(ucf.commit)
87
114
  end
88
115
  end
89
116
 
90
117
  assert_nothing_raised(UCF::MalformedUCFError, Zip::ZipError) do
91
118
  UCF::Container.open(filename) do |ucf|
119
+ assert(ucf.on_disk?)
120
+ refute(ucf.in_memory?)
121
+
92
122
  assert(ucf.file.exists?("test.txt"))
93
123
  assert(ucf.file.exists?("dir1"))
94
124
  assert(ucf.file.exists?("dir2"))
95
- assert(!ucf.file.exists?("dir3"))
125
+ refute(ucf.file.exists?("dir3"))
96
126
 
97
127
  text = ucf.file.read("test.txt")
98
128
  assert_equal("testing", text)
129
+
130
+ assert_equal("A comment!", ucf.comment)
131
+
132
+ refute(ucf.commit_required?)
133
+ refute(ucf.commit)
99
134
  end
100
135
  end
101
136
  end
data/test/tc_read.rb CHANGED
@@ -40,7 +40,7 @@ class TestRead < Test::Unit::TestCase
40
40
  UCF::Container.verify!($file_null)
41
41
  end
42
42
 
43
- assert(!UCF::Container.verify($file_null))
43
+ refute(UCF::Container.verify($file_null))
44
44
  end
45
45
 
46
46
  # Check that the empty ucf file does verify.
@@ -58,7 +58,7 @@ class TestRead < Test::Unit::TestCase
58
58
  UCF::Container.verify!($zip_empty)
59
59
  end
60
60
 
61
- assert(!UCF::Container.verify($zip_empty))
61
+ refute(UCF::Container.verify($zip_empty))
62
62
  end
63
63
 
64
64
  # Check that a compressed mimetype file is detected.
@@ -67,7 +67,7 @@ class TestRead < Test::Unit::TestCase
67
67
  UCF::Container.verify!($ucf_compressed_mimetype)
68
68
  end
69
69
 
70
- assert(!UCF::Container.verify($ucf_compressed_mimetype))
70
+ refute(UCF::Container.verify($ucf_compressed_mimetype))
71
71
  end
72
72
 
73
73
  # Check the raw mimetype bytes
@@ -79,10 +79,13 @@ class TestRead < Test::Unit::TestCase
79
79
  assert_not_equal("application/epub+zip", compressed_mimetype[38..57])
80
80
  end
81
81
 
82
- # Check reading files out of a ucf file.
82
+ # Check reading files out of a ucf file and make sure we don't change it.
83
83
  def test_read_files_from_ucf
84
84
  assert_nothing_raised(UCF::MalformedUCFError, Zip::ZipError) do
85
85
  UCF::Container.open($ucf_example) do |ucf|
86
+ assert(ucf.on_disk?)
87
+ refute(ucf.in_memory?)
88
+
86
89
  assert(ucf.file.exists?("greeting.txt"))
87
90
 
88
91
  greeting = ucf.file.read("greeting.txt")
@@ -92,6 +95,11 @@ class TestRead < Test::Unit::TestCase
92
95
  assert(ucf.file.directory?("dir"))
93
96
 
94
97
  assert(ucf.file.exists?("dir/code.rb"))
98
+
99
+ assert_equal("This is an example UCF file!", ucf.comment)
100
+
101
+ refute(ucf.commit_required?)
102
+ refute(ucf.commit)
95
103
  end
96
104
  end
97
105
  end
@@ -32,8 +32,78 @@
32
32
 
33
33
  require 'ucf'
34
34
 
35
+ # A class to test the overriding of reserved names.
36
+ class NewUCF < UCF::Container
37
+ def reserved_files
38
+ super + ["index.html"]
39
+ end
40
+
41
+ def reserved_directories
42
+ super + ["src", "test", "lib"]
43
+ end
44
+ end
45
+
35
46
  class TestReservedNames < Test::Unit::TestCase
36
47
 
48
+ # Check the reserved names stuff all works correctly, baring in mind that
49
+ # such comparisons for UCF documents should be case sensitive.
50
+ def test_reserved_names
51
+ UCF::Container.open($ucf_example) do |ucf|
52
+ assert_equal(1, ucf.reserved_files.length)
53
+ assert_equal(["mimetype"], ucf.reserved_files)
54
+ assert(ucf.reserved_entry?("mimetype"))
55
+ assert(ucf.reserved_entry?("mimetype/"))
56
+ assert(ucf.reserved_entry?("MimeType"))
57
+
58
+ assert_equal(1, ucf.reserved_directories.length)
59
+ assert_equal(["META-INF"], ucf.reserved_directories)
60
+ assert(ucf.reserved_entry?("META-INF"))
61
+ assert(ucf.reserved_entry?("META-INF/"))
62
+ assert(ucf.reserved_entry?("MeTa-iNf"))
63
+ assert(ucf.reserved_entry?("MeTa-iNf/"))
64
+
65
+ assert_equal(2, ucf.reserved_names.length)
66
+ assert_equal(["mimetype", "META-INF"], ucf.reserved_names)
67
+
68
+ refute(ucf.reserved_entry?("This_should_fail"))
69
+ refute(ucf.reserved_entry?("META_INF"))
70
+ refute(ucf.reserved_entry?("META_INF/"))
71
+ end
72
+ end
73
+
74
+ # Check that overriding the reserved names in a sub-class works correctly
75
+ def test_subclass_reserved_names
76
+ NewUCF.open($ucf_example) do |ucf|
77
+ assert_equal(2, ucf.reserved_files.length)
78
+ assert_equal(["mimetype", "index.html"], ucf.reserved_files)
79
+ assert(ucf.reserved_entry?("mimetype"))
80
+ assert(ucf.reserved_entry?("mimetype/"))
81
+ assert(ucf.reserved_entry?("MimeType"))
82
+ assert(ucf.reserved_entry?("index.html"))
83
+ assert(ucf.reserved_entry?("Index.HTML"))
84
+
85
+ assert_equal(4, ucf.reserved_directories.length)
86
+ assert_equal(["META-INF", "src", "test", "lib"], ucf.reserved_directories)
87
+ assert(ucf.reserved_entry?("META-INF"))
88
+ assert(ucf.reserved_entry?("META-INF/"))
89
+ assert(ucf.reserved_entry?("MeTa-iNf"))
90
+ assert(ucf.reserved_entry?("src"))
91
+ assert(ucf.reserved_entry?("SRC"))
92
+ assert(ucf.reserved_entry?("test"))
93
+ assert(ucf.reserved_entry?("lib"))
94
+ assert(ucf.reserved_entry?("lIb/"))
95
+
96
+ assert_equal(6, ucf.reserved_names.length)
97
+ assert_equal(["mimetype", "index.html", "META-INF", "src", "test", "lib"],
98
+ ucf.reserved_names)
99
+
100
+ refute(ucf.reserved_entry?("This_should_fail"))
101
+ refute(ucf.reserved_entry?("META_INF"))
102
+ refute(ucf.reserved_entry?("META_INF/"))
103
+ refute(ucf.reserved_entry?("index.htm"))
104
+ end
105
+ end
106
+
37
107
  # Check that nothing happens when trying to delete the mimetype file.
38
108
  def test_delete_mimetype
39
109
  UCF::Container.open($ucf_example) do |ucf|
@@ -49,7 +119,7 @@ class TestReservedNames < Test::Unit::TestCase
49
119
  assert(ucf.file.exists?("mimetype"))
50
120
  assert_nil(ucf.rename("mimetype", "something-else"))
51
121
  assert(ucf.file.exists?("mimetype"))
52
- assert(!ucf.file.exists?("something-else"))
122
+ refute(ucf.file.exists?("something-else"))
53
123
  end
54
124
  end
55
125
 
@@ -63,6 +133,34 @@ class TestReservedNames < Test::Unit::TestCase
63
133
  end
64
134
  end
65
135
 
136
+ # Check that an exception is raised when trying to add file with a reserved
137
+ # name.
138
+ def test_add_reserved
139
+ UCF::Container.open($ucf_empty) do |ucf|
140
+ assert_raises(UCF::ReservedNameClashError) do
141
+ ucf.add("META-INF", $zip_empty)
142
+ end
143
+ end
144
+ end
145
+
146
+ # Check that an exception is raised when trying to add file with a reserved
147
+ # name to a subclassed container.
148
+ def test_subclass_add_reserved
149
+ NewUCF.open($ucf_empty) do |ucf|
150
+ assert_raises(UCF::ReservedNameClashError) do
151
+ ucf.add("META-INF", $zip_empty)
152
+ end
153
+
154
+ assert_raises(UCF::ReservedNameClashError) do
155
+ ucf.add("index.html", $zip_empty)
156
+ end
157
+
158
+ assert_raises(UCF::ReservedNameClashError) do
159
+ ucf.add("SRC", $zip_empty)
160
+ end
161
+ end
162
+ end
163
+
66
164
  # Check that nothing happens when trying to delete the META-INF directory.
67
165
  def test_delete_metainf
68
166
  UCF::Container.open($ucf_example) do |ucf|
@@ -77,4 +175,134 @@ class TestReservedNames < Test::Unit::TestCase
77
175
  end
78
176
  end
79
177
 
178
+ # Check that an exception is raised when trying to create a directory with a
179
+ # reserved name.
180
+ def test_mkdir_reserved
181
+ UCF::Container.open($ucf_empty) do |ucf|
182
+ assert_raises(UCF::ReservedNameClashError) do
183
+ ucf.mkdir("META-INF")
184
+ end
185
+ end
186
+ end
187
+
188
+ # Check that an exception is raised when trying to create a directory with a
189
+ # reserved name in a subclassed container.
190
+ def test_subclass_mkdir_reserved
191
+ NewUCF.open($ucf_empty) do |ucf|
192
+ assert_raises(UCF::ReservedNameClashError) do
193
+ ucf.mkdir("META-INF")
194
+ end
195
+
196
+ assert_raises(UCF::ReservedNameClashError) do
197
+ ucf.mkdir("index.html")
198
+ end
199
+
200
+ assert_raises(UCF::ReservedNameClashError) do
201
+ ucf.mkdir("Lib")
202
+ end
203
+ end
204
+ end
205
+
206
+ # Check that a file cannot be renamed to one of the reserved names.
207
+ def test_rename_to_reserved
208
+ UCF::Container.open($ucf_example) do |ucf|
209
+ assert_raises(UCF::ReservedNameClashError) do
210
+ ucf.rename("dir/code.rb", "mimetype")
211
+ end
212
+
213
+ assert_raises(UCF::ReservedNameClashError) do
214
+ ucf.rename("dir", "META-INF")
215
+ end
216
+ end
217
+ end
218
+
219
+ # Check that a file cannot be renamed to one of the reserved names in a
220
+ # subclassed container.
221
+ def test_subclass_rename_to_reserved
222
+ NewUCF.open($ucf_example) do |ucf|
223
+ assert_raises(UCF::ReservedNameClashError) do
224
+ ucf.rename("dir/code.rb", "mimetype")
225
+ end
226
+
227
+ assert_raises(UCF::ReservedNameClashError) do
228
+ ucf.rename("dir", "META-INF")
229
+ end
230
+
231
+ assert_raises(UCF::ReservedNameClashError) do
232
+ ucf.rename("dir/code.rb", "index.html")
233
+ end
234
+
235
+ assert_raises(UCF::ReservedNameClashError) do
236
+ ucf.rename("dir", "Test")
237
+ end
238
+ end
239
+ end
240
+
241
+ # Check that the ruby-like File and Dir classes respect reserved names.
242
+ def test_file_dir_ops_reserved
243
+ UCF::Container.open($ucf_empty) do |ucf|
244
+ assert_raises(UCF::ReservedNameClashError) do
245
+ ucf.file.open("META-INF", "w") do |f|
246
+ f.puts "TESTING"
247
+ end
248
+ end
249
+
250
+ assert_nothing_raised(UCF::ReservedNameClashError) do
251
+ ucf.file.open("mimetype") do |f|
252
+ assert_equal("application/epub+zip", f.read)
253
+ end
254
+ end
255
+
256
+ assert_nothing_raised(UCF::ReservedNameClashError) do
257
+ ucf.file.delete("mimetype")
258
+ assert(ucf.file.exists?("mimetype"))
259
+ end
260
+
261
+ assert_raises(UCF::ReservedNameClashError) do
262
+ ucf.dir.mkdir("meta-inf")
263
+ end
264
+ end
265
+ end
266
+
267
+ # Check that the ruby-like File and Dir classes respect reserved names in a
268
+ # subclassed container.
269
+ def test_subclass_file_dir_ops_reserved
270
+ NewUCF.open($ucf_empty) do |ucf|
271
+ assert_raises(UCF::ReservedNameClashError) do
272
+ ucf.file.open("META-INF", "w") do |f|
273
+ f.puts "TESTING"
274
+ end
275
+ end
276
+
277
+ assert_raises(UCF::ReservedNameClashError) do
278
+ ucf.file.open("INDEX.HTML", "w") do |f|
279
+ f.puts "TESTING"
280
+ end
281
+ end
282
+
283
+ assert_nothing_raised(UCF::ReservedNameClashError) do
284
+ ucf.file.open("mimetype") do |f|
285
+ assert_equal("application/epub+zip", f.read)
286
+ end
287
+ end
288
+
289
+ assert_nothing_raised(UCF::ReservedNameClashError) do
290
+ ucf.file.delete("mimetype")
291
+ assert(ucf.file.exists?("mimetype"))
292
+ end
293
+
294
+ assert_raises(UCF::ReservedNameClashError) do
295
+ ucf.dir.mkdir("meta-inf")
296
+ end
297
+
298
+ assert_raises(UCF::ReservedNameClashError) do
299
+ ucf.dir.mkdir("TEST")
300
+ end
301
+
302
+ assert_raises(UCF::ReservedNameClashError) do
303
+ ucf.dir.mkdir("index.html")
304
+ end
305
+ end
306
+ end
307
+
80
308
  end
data/ucf.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "ucf"
8
- s.version = "0.0.2"
8
+ s.version = "0.1.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Robert Haines"]
12
- s.date = "2013-05-29"
12
+ s.date = "2013-06-03"
13
13
  s.description = "A Ruby library for working with Universal Container Format files. See https://learn.adobe.com/wiki/display/PDFNAV/Universal+Container+Format for the specification."
14
14
  s.email = ["support@mygrid.org.uk"]
15
15
  s.extra_rdoc_files = [
@@ -27,6 +27,7 @@ Gem::Specification.new do |s|
27
27
  "examples/verify_ucf.rb",
28
28
  "lib/ucf.rb",
29
29
  "lib/ucf/container.rb",
30
+ "lib/ucf/exceptions.rb",
30
31
  "test/data/compressed_mimetype.ucf",
31
32
  "test/data/empty.ucf",
32
33
  "test/data/empty.zip",
data/version.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :major: 0
3
- :minor: 0
4
- :patch: 2
3
+ :minor: 1
4
+ :patch: 0
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ucf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-29 00:00:00.000000000 Z
12
+ date: 2013-06-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -95,6 +95,7 @@ files:
95
95
  - examples/verify_ucf.rb
96
96
  - lib/ucf.rb
97
97
  - lib/ucf/container.rb
98
+ - lib/ucf/exceptions.rb
98
99
  - test/data/compressed_mimetype.ucf
99
100
  - test/data/empty.ucf
100
101
  - test/data/empty.zip