ucf 0.0.2 → 0.1.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.
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