sgfa 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/sgfa/error.rb ADDED
@@ -0,0 +1,95 @@
1
+ #
2
+ # Simple Group of Filing Applications
3
+ # Errors
4
+ #
5
+ # Copyright (C) 2015 by Graham A. Field.
6
+ #
7
+ # See LICENSE.txt for licensing information.
8
+ #
9
+ # This program is distributed WITHOUT ANY WARRANTY; without even the
10
+ # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
+
12
+ # This ; prevents YARD from using the header as docs for Sgfa module
13
+ ;
14
+
15
+ module Sgfa
16
+
17
+
18
+ ##########################################################################
19
+ # Errors generated by the Sgfa system
20
+ module Error
21
+
22
+
23
+ ##########################################
24
+ # Most limit checks
25
+ #
26
+ # @param str [String] String to check
27
+ # @param min [Integer] Minimum length
28
+ # @param max [Integer] max Maximum length
29
+ # @param inv [Regexp, String] Invalid characters
30
+ # @param desc [String] desc Description of string
31
+ # @raise [Error::Limits] if limits are violated
32
+ def self.limits(str, min, max, inv, desc)
33
+
34
+ # string
35
+ if !str || !str.is_a?(String)
36
+ raise Error::Limits, desc + ' is not a string.'
37
+ end
38
+
39
+ # size
40
+ size = str.size
41
+ if str.size > max
42
+ raise Error::Limits, '%s length %d is too large, limit %d' %
43
+ [desc, size, max]
44
+ end
45
+ if str.size < min
46
+ raise Error::Limits, '%s length %d is too small, limit %d' %
47
+ [desc, size, min]
48
+ end
49
+
50
+ # invalid characters
51
+ idx = str.index(inv)
52
+ if idx
53
+ raise Error::Limits,
54
+ '%s contains invalid character \'%s\'' % [desc, str[idx]]
55
+ end
56
+ end # def self.limits()
57
+
58
+
59
+ ###########################################################
60
+ # Sanity check failed
61
+ class Sanity < StandardError; end
62
+
63
+
64
+ ###########################################################
65
+ # Outside of limits
66
+ class Limits < StandardError; end
67
+
68
+
69
+ ###########################################################
70
+ # Corrupt file
71
+ class Corrupt < StandardError; end
72
+
73
+
74
+ ###########################################################
75
+ # Outside of limits
76
+ class Limits < StandardError; end
77
+
78
+
79
+ ###########################################################
80
+ # Conflict of some type
81
+ class Conflict < StandardError; end
82
+
83
+
84
+ ###########################################################
85
+ # Does not exist
86
+ class NonExistent < StandardError; end
87
+
88
+
89
+ ###########################################################
90
+ # Permission check failed
91
+ class Permission < StandardError; end
92
+
93
+ end # module Error
94
+
95
+ end # module Sgfa
@@ -0,0 +1,445 @@
1
+ #
2
+ # Simple Group of Filing Applications
3
+ # History item
4
+ #
5
+ # Copyright (C) 2015 by Graham A. Field.
6
+ #
7
+ # See LICENSE.txt for licensing information.
8
+ #
9
+ # This program is distributed WITHOUT ANY WARRANTY; without even the
10
+ # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
+
12
+ require 'digest/sha2'
13
+
14
+ require_relative 'error'
15
+
16
+ module Sgfa
17
+
18
+
19
+ #####################################################################
20
+ # The history item provides a cryptographic chain of changes made to a
21
+ # {Jacket}.
22
+ #
23
+ # The History has attributes:
24
+ # * hash - SHA256 hash of the canonical encoding
25
+ # * canonical - the canonical encoded string
26
+ # * jacket - The {Jacket} hash ID the history belongs to
27
+ # * previous - Previous history number
28
+ # * history - History number
29
+ # * entry_max - Number of entries present in the Jacket
30
+ # * time - Date and time of the change
31
+ # * user - User name who made the change
32
+ # * entries - List of entries changed \[entry_num, revision_num, hash\]
33
+ # * attachments - List of attachments made \[entry_num, attach_num, hash\]
34
+ #
35
+ class History
36
+
37
+ #########################################################
38
+ # @!group Limits checks
39
+
40
+ # Max chars in user name
41
+ LimUserMax = 64
42
+
43
+ # Invalid character in user name
44
+ LimUserInv = /[[:cntrl:]]/
45
+
46
+ #####################################
47
+ # Limit check, user name
48
+ def self.limits_user(str)
49
+ Error.limits(str, 1, LimUserMax, LimUserInv, 'User name')
50
+ end # def self.limits_user()
51
+
52
+
53
+ #########################################################
54
+ # @!group Read attributes
55
+
56
+
57
+ #####################################
58
+ # Get history item hash
59
+ #
60
+ # @return [String] The hash of the history
61
+ # @raise (see #canonical)
62
+ def hash
63
+ if !@hash
64
+ @hash = Digest::SHA256.new.update(canonical).hexdigest
65
+ end
66
+ return @hash.dup
67
+ end # def hash
68
+
69
+
70
+ #####################################
71
+ # Generate canonical encoded string
72
+ #
73
+ # @return [String] Canonical output
74
+ # @raise [Error::Sanity] if the history is not complete
75
+ def canonical
76
+ if !@canon
77
+ raise Error::Sanity, 'History not complete' if !@history
78
+
79
+ txt = "jckt %s\n" % @jacket
80
+ txt << "hist %d\n" % @history
81
+ txt << "emax %d\n" % @entry_max
82
+ txt << "time %s\n" % time_str
83
+ txt << "prev %s\n" % @previous
84
+ txt << "user %s\n" % @user
85
+ @entries.each{|ary| txt << "entr %d %d %s\n" % ary }
86
+ @attach.each{|ary| txt << "atch %d %d %s\n" % ary }
87
+ @canon = txt
88
+ end
89
+ return @canon.dup
90
+ end # def canonical
91
+
92
+
93
+ #####################################
94
+ # Get jacket
95
+ #
96
+ # @return [String, Boolean] The jacket hash ID or false if not set
97
+ def jacket
98
+ if @jacket
99
+ return @jacket.dup
100
+ else
101
+ return false
102
+ end
103
+ end # def jacket
104
+
105
+
106
+ #####################################
107
+ # Get previous history hash
108
+ #
109
+ # @return [String, Boolean] The hash of the previous history item,
110
+ # or false if not set
111
+ def previous
112
+ if @previous
113
+ return @previous.dup
114
+ else
115
+ return false
116
+ end
117
+ end # def previous
118
+
119
+
120
+ #####################################
121
+ # Get history number
122
+ #
123
+ # @return [Integer, Boolean] History number, or false if not set
124
+ def history
125
+ return @history
126
+ end # def history
127
+
128
+
129
+ #####################################
130
+ # Get maximum entry
131
+ #
132
+ # @return [Integer, Boolean] Maximum entry in a Jacket, or false if not set
133
+ def entry_max
134
+ return @entry_max
135
+ end # def entry_max
136
+
137
+
138
+ # Regex to parse time string
139
+ TimeStrReg = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/
140
+ private_constant :TimeStrReg
141
+
142
+
143
+ #####################################
144
+ # Get time
145
+ #
146
+ # @return [Time, Boolean] The time, or false if not set
147
+ def time
148
+ if !@time
149
+ return false if !@time_str
150
+ ma = TimeStrReg.match(@time_str)
151
+ ary = ma[1,6].map{|str| str.to_i}
152
+ @time = Time.utc(*ary)
153
+ end
154
+ return @time.dup
155
+ end # def time
156
+
157
+
158
+ #####################################
159
+ # Get time string
160
+ #
161
+ # @return [String, Boolean] Encoded time string of the history, or false
162
+ # if time not set
163
+ def time_str
164
+ if !@time_str
165
+ return false if !@time
166
+ @time_str = @time.strftime('%F %T')
167
+ end
168
+ return @time_str.dup
169
+ end # def time_str
170
+
171
+
172
+ #####################################
173
+ # Get user
174
+ #
175
+ # @return [String, Boolean] The user string, or false if not set
176
+ def user
177
+ if @user
178
+ return @user.dup
179
+ else
180
+ return false
181
+ end
182
+ end # def user
183
+
184
+
185
+ #####################################
186
+ # Get entries
187
+ #
188
+ # @return [Array] of Entry information \[entry_num, revision_num, hash\]
189
+ def entries
190
+ return @entries.map{|enum, rnum, hash| [enum, rnum, hash.dup] }
191
+ end # def entries
192
+
193
+
194
+ #####################################
195
+ # Get attachments
196
+ #
197
+ # @return [Array] of Attachment information \[entry_num, attach_num, hash\]
198
+ def attachments
199
+ return @attach.map{|enum, anum, hash| [enum, anum, hash.dup] }
200
+ end # def attachments
201
+
202
+
203
+
204
+ #########################################################
205
+ # @!group General interface
206
+
207
+ #####################################
208
+ # Create a new History item
209
+ #
210
+ # @param (see #jacket=)
211
+ # @raise (see #jacket=)
212
+ def initialize(jck=nil)
213
+ reset
214
+ if jck
215
+ self.jacket = jck
216
+ end
217
+ end # def initialize()
218
+
219
+
220
+ #####################################
221
+ # Reset to blank History item
222
+ #
223
+ # @return [History] self
224
+ def reset
225
+ @hash = nil
226
+ @canon = nil
227
+ @jacket = nil
228
+ @history = nil
229
+ @entry_max = 0
230
+ @time = nil
231
+ @time_str = nil
232
+ @previous = nil
233
+ @user = nil
234
+ @entries = []
235
+ @attach = []
236
+
237
+ @change_entry = nil
238
+ @change_tag = nil
239
+ @change_attach = nil
240
+ end # def reset
241
+
242
+
243
+ #####################################
244
+ # Set History using canonical encoding
245
+ #
246
+ # @param str [String] Canonical encoded History
247
+ # @raise [Error::Corrupt] if encoding does not follow canonical rules
248
+ def canonical=(str)
249
+ @hash = nil
250
+ @canon = str.dup
251
+ lines = str.lines
252
+
253
+ ma = /^jckt ([0-9a-f]{64})$/.match lines.shift
254
+ raise(Error::Corrupt, 'Canonical history jacket error') if !ma
255
+ @jacket = ma[1]
256
+
257
+ ma = /^hist (\d+)$/.match lines.shift
258
+ raise(Error::Corrupt, 'Canonical history history error') if !ma
259
+ @history = ma[1].to_i
260
+
261
+ ma = /^emax (\d+)$/.match lines.shift
262
+ raise(Error::Corrupt, 'Canonical history entry_max error') if !ma
263
+ @entry_max = ma[1].to_i
264
+
265
+ ma = /^time (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})$/.match lines.shift
266
+ raise(Error::Corrupt, 'Canonical history time error') if !ma
267
+ @time_str = ma[1]
268
+
269
+ ma = /^prev ([0-9a-f]{64})$/.match lines.shift
270
+ raise(Error::Corrupt, 'Canonical history previous error') if !ma
271
+ @previous = ma[1]
272
+
273
+ ma = /^user (.+)$/.match lines.shift
274
+ raise(Error::Corrupt, 'Canonical history user error') if !ma
275
+ @user = ma[1]
276
+
277
+ li = lines.shift
278
+ @entries = []
279
+ while(li && ma = /^entr (\d+) (\d+) (.+)$/.match(li) )
280
+ @entries.push [ma[1].to_i, ma[2].to_i, ma[3]]
281
+ li = lines.shift
282
+ end
283
+
284
+ @attach = []
285
+ while(li && ma = /^atch (\d+) (\d+) ([0-9a-f]{64})$/.match(li) )
286
+ @attach.push [ma[1].to_i, ma[2].to_i, ma[3]]
287
+ li = lines.shift
288
+ end
289
+
290
+ if li
291
+ raise Error::Corrupt, 'Canonical history error'
292
+ end
293
+
294
+ rescue
295
+ reset
296
+ raise
297
+ end # def canonical=()
298
+
299
+
300
+ #####################################
301
+ # Set jacket
302
+ #
303
+ # @param [String] jck The jacket hash ID
304
+ # @raise [Error::Limits] if jck is not a valid hash
305
+ # @raise [Error::Sanity] if changing an already set jacket hash ID
306
+ def jacket=(jck)
307
+ ma = /^([0-9a-f]{64})$/.match jck
308
+ raise(Error::Limits, 'Jacket hash not valid') if !ma
309
+ if @jacket
310
+ raise(Error::Sanity, 'Jacket already set') if @jacket != jck
311
+ else
312
+ @jacket = jck.dup
313
+ end
314
+ end # def jacket=()
315
+
316
+
317
+ # Tag added to every entry
318
+ TagAll = '_all'
319
+ private_constant :TagAll
320
+
321
+
322
+ #####################################
323
+ # Generate next history
324
+ #
325
+ # @param [String] user User making the change
326
+ # @param [Array] ents Entries to update
327
+ # @param [Time] tme Time to use for the history
328
+ # @return [Array] the next history item, and the changes hash
329
+ # @raise (see #hash)
330
+ # @raise (see #process)
331
+ def next(user, ents, tme=nil)
332
+ raise Error::Sanity, 'History not complete' if !@history
333
+ nxt = History.new(@jacket)
334
+ cng = nxt.process(@history + 1, hash, @entry_max, user, ents, tme)
335
+ return [nxt, cng]
336
+ end # def next
337
+
338
+
339
+ #####################################
340
+ # Process entries
341
+ #
342
+ # @note Entries without a jacket set are automatically set to the correct
343
+ # value
344
+ # @note If time is not supplied, current time is used
345
+ #
346
+ # The changes returned consist of:
347
+ # * :entry - Array of entries provided in ents
348
+ # * :tag - Hash of tag => Hash of Entry => time_str or nil
349
+ # * :attach - Attached files \[entry_num, attach_num, file\]
350
+ #
351
+ # @param [Integer] hnum History number
352
+ # @param [String] prev Hash of previous item
353
+ # @param [Integer] emax Maximum previous entry number
354
+ # @param [String] user User making the change
355
+ # @param [Array] ents Entries to update
356
+ # @param [Time] tme Time to use for the history
357
+ # @return [Hash] Record of changes made
358
+ # @raise [Error::Sanity] if entry does not belong to the same jacket
359
+ # @raise (see Entry#update)
360
+ def process(hnum, prev, emax, user, ents, tme=nil)
361
+
362
+ cng = {}
363
+ add = []
364
+
365
+ # initial values
366
+ @hash = nil
367
+ @canon = nil
368
+ @previous = prev
369
+ @history = hnum
370
+ @entry_max = emax
371
+ if tme
372
+ @time = tme.utc
373
+ else
374
+ @time = Time.now.utc
375
+ end
376
+ @user = user.dup
377
+ @entries = []
378
+ @attach = []
379
+
380
+ # process the entries
381
+ ents.each do |entry|
382
+ # set/check jacket
383
+ if !entry.jacket
384
+ entry.jacket = @jacket
385
+ elsif entry.jacket != @jacket
386
+ raise Error::Sanity, 'Entry belongs to different jacket'
387
+ end
388
+
389
+ # set entry for new entries
390
+ if !entry.entry
391
+ @entry_max += 1
392
+ entry.entry = @entry_max
393
+ end
394
+
395
+ # update the entry
396
+ ecng = entry.update(@history)
397
+ enum = entry.entry
398
+ ts = entry.time_str
399
+
400
+ # time changed, all tags update
401
+ if ecng[:time]
402
+ cng[TagAll] = {} if !cng[TagAll]
403
+ cng[TagAll][enum] = ts
404
+ entry.tags.each do |tag|
405
+ cng[tag] = {} if !cng[tag]
406
+ cng[tag][enum] = ts
407
+ end
408
+
409
+ # just new tags
410
+ else
411
+ ecng[:tags_add].each do |tag|
412
+ cng[tag] = {} if !cng[tag]
413
+ cng[tag][enum] = ts
414
+ end
415
+ end
416
+
417
+ # deleted tags
418
+ ecng[:tags_del].each do |tag|
419
+ cng[tag] = {} if !cng[tag]
420
+ cng[tag][enum] = nil
421
+ end
422
+
423
+ # record entry and attachments
424
+ @entries.push [enum, entry.revision, entry.hash]
425
+ ecng[:files].each do |anum, ary|
426
+ file, hash = ary
427
+ @attach.push [enum, anum, hash]
428
+ add.push [enum, anum, file]
429
+ end
430
+
431
+ end
432
+
433
+ ret = {
434
+ :entry => ents,
435
+ :tag => cng,
436
+ :attach => add,
437
+ }
438
+
439
+ return ret
440
+ end # def process()
441
+
442
+
443
+ end # class History
444
+
445
+ end # module Sgfa