manageiq-smartstate 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2eed9833b4418d9405995117c01ae36ec8d9dbd8f59c2a71a3b1695fd1845eca
4
- data.tar.gz: 014abce3c3c7a22594ffe9877046b1a390b0941e58e4052b3eb29525c80bd6fb
3
+ metadata.gz: 3547ea48f2af9d840bfceb8c0c3ae20b6e57bb7b1494e705a940e0998f28d9a6
4
+ data.tar.gz: dbbdc0761323f813a2224f5fc0a11954b60cc49a6d4bca30d39f16d3d4eed67a
5
5
  SHA512:
6
- metadata.gz: 417c6b7efe6f1ebdf11f037beb537f9fd48f1654e5985699a6b6c2d29342ea7fd22b59df97b48ae4ba09e1d6c75624c198add8d988b6387d071084ae48b0aafb
7
- data.tar.gz: 35335b08093b49f9fb4e57aeba97b2c89d6c01ead9de3cb854086f84b0a70b59f90a178f1aaa44230dd1feea24c70acefa0546f43e2b65a279b8bcd59b0d1146
6
+ metadata.gz: e2c6075e386e2c396e39aba0d8d5d23f037ee94f262d625bf3b51276e9b6615ef2ea6dac248644bf9b2449493db4bcf01e636fb826ed14c60f089859a42fc004
7
+ data.tar.gz: 848728c5dd81a7bb8b3b68e4d99249f8070ba26aecf293322f7d72fcdb7b8e1667cc923fb6caea0b43b2bff4a1ec61a32b0c2ddf9af9c482cfb514894f476e8e
data/.travis.yml CHANGED
@@ -2,8 +2,8 @@
2
2
  cache: bundler
3
3
  language: ruby
4
4
  rvm:
5
- - 2.5.8
6
5
  - 2.6.6
6
+ - 2.7.2
7
7
  after_script: bundle exec codeclimate-test-reporter
8
8
  notifications:
9
9
  webhooks:
@@ -89,8 +89,6 @@
89
89
  #
90
90
  ############################################################################################
91
91
 
92
- require 'enumerator'
93
-
94
92
  require 'binary_struct'
95
93
  require 'util/miq-hash_struct'
96
94
 
@@ -1,5 +1,3 @@
1
- require 'enumerator'
2
-
3
1
  require 'binary_struct'
4
2
  require 'util/miq-hash_struct'
5
3
 
@@ -195,8 +195,6 @@
195
195
  # * zero or more pages numbers of leaves
196
196
 
197
197
  require 'ostruct'
198
- require 'enumerator'
199
-
200
198
  require 'binary_struct'
201
199
 
202
200
  require_relative 'MiqSqlite3Page'
@@ -1,8 +1,6 @@
1
1
  # encoding: US-ASCII
2
2
 
3
3
  require 'ostruct'
4
- require 'enumerator'
5
-
6
4
  require 'binary_struct'
7
5
  require_relative 'MiqSqlite3Util'
8
6
 
@@ -1,6 +1,4 @@
1
1
  require 'ostruct'
2
- require 'enumerator'
3
-
4
2
  require 'binary_struct'
5
3
  require_relative 'MiqSqlite3Util'
6
4
  require_relative 'MiqSqlite3Cell'
@@ -1,6 +1,4 @@
1
1
  require 'ostruct'
2
- require 'enumerator'
3
-
4
2
  require 'binary_struct'
5
3
  require_relative 'MiqSqlite3Util'
6
4
  require_relative 'MiqSqlite3Page'
@@ -1,540 +1,540 @@
1
- # encoding: US-ASCII
2
-
3
- require 'stringio'
4
- require 'binary_struct'
5
- require 'miq_unicode'
6
-
7
- # ////////////////////////////////////////////////////////////////////////////
8
- # // Data definitions.
9
-
10
- # TODO: reserved1 is the infamous magic number. Somehow it works to preserve
11
- # case on Windows XP. Nobody seems to know how. Here it is always set to 0
12
- # (which yields uppercase names on XP).
13
-
14
- module Fat32
15
- using ManageIQ::UnicodeString
16
-
17
- DIR_ENT_SFN = BinaryStruct.new([
18
- 'a11', 'name', # If name[0] = 0, unallocated; if name[0] = 0xe5, deleted. DOES NOT INCLUDE DOT.
19
- 'C', 'attributes', # See FA_ below. If 0x0f then LFN entry.
20
- 'C', 'reserved1', # Reserved.
21
- 'C', 'ctime_tos', # Created time, tenths of second.
22
- 'S', 'ctime_hms', # Created time, hours, minutes & seconds.
23
- 'S', 'ctime_day', # Created day.
24
- 'S', 'atime_day', # Accessed day.
25
- 'S', 'first_clus_hi', # Hi 16-bits of first cluster address.
26
- 'S', 'mtime_hms', # Modified time, hours, minutes & seconds.
27
- 'S', 'mtime_day', # Modified day.
28
- 'S', 'first_clus_lo', # Lo 16-bits of first cluster address.
29
- 'L', 'file_size', # Size of file (0 for directories).
30
- ])
31
-
32
- DIR_ENT_LFN = BinaryStruct.new([
33
- 'C', 'seq_num', # Sequence number, bit 6 marks end, 0xe5 if deleted.
34
- 'a10', 'name', # UNICODE chars 1-5 of name.
35
- 'C', 'attributes', # Always 0x0f.
36
- 'C', 'reserved1', # Reserved.
37
- 'C', 'checksum', # Checksum of SFN entry, all LFN entries must match.
38
- 'a12', 'name2', # UNICODE chars 6-11 of name.
39
- 'S', 'reserved2', # Reserved.
40
- 'a4', 'name3' # UNICODE chars 12-13 of name.
41
- ])
42
-
43
- CHARS_PER_LFN = 13
44
- LFN_NAME_MAXLEN = 260
45
- DIR_ENT_SIZE = 32
46
- ATTRIB_OFFSET = 11
47
-
48
- # ////////////////////////////////////////////////////////////////////////////
49
- # // Class.
50
-
51
- class DirectoryEntry
52
-
53
- # From the UTF-8 perspective.
54
- # LFN name components: entry hash name, char offset, length.
55
- LFN_NAME_COMPONENTS = [
56
- ['name', 0, 5],
57
- ['name2', 5, 6],
58
- ['name3', 11, 2]
59
- ]
60
- # Name component second sub access names.
61
- LFN_NC_HASHNAME = 0
62
- LFN_NC_OFFSET = 1
63
- LFN_NC_LENGTH = 2
64
-
65
- # SFN failure cases.
66
- SFN_NAME_LENGTH = 1
67
- SFN_EXT_LENGTH = 2
68
- SFN_NAME_NULL = 3
69
- SFN_NAME_DEVICE = 4
70
- SFN_ILLEGAL_CHARS = 5
71
-
72
- # LFN failure cases.
73
- LFN_NAME_LENGTH = 1
74
- LFN_NAME_DEVICE = 2
75
- LFN_ILLEGAL_CHARS = 3
76
-
77
- # FileAttributes
78
- FA_READONLY = 0x01
79
- FA_HIDDEN = 0x02
80
- FA_SYSTEM = 0x04
81
- FA_LABEL = 0x08
82
- FA_DIRECTORY = 0x10
83
- FA_ARCHIVE = 0x20
84
- FA_LFN = 0x0f
85
-
86
- # DOS time masks.
87
- MSK_DAY = 0x001f # Range: 1 - 31
88
- MSK_MONTH = 0x01e0 # Right shift 5, Range: 1 - 12
89
- MSK_YEAR = 0xfe00 # Right shift 9, Range: 127 (add 1980 for year).
90
- MSK_SEC = 0x001f # Range: 0 - 29 WARNING: 2 second granularity on this.
91
- MSK_MIN = 0x07e0 # Right shift 5, Range: 0 - 59
92
- MSK_HOUR = 0xf800 # Right shift 11, Range: 0 - 23
93
-
94
- # AllocationFlags
95
- AF_NOT_ALLOCATED = 0x00
96
- AF_DELETED = 0xe5
97
- AF_LFN_LAST = 0x40
98
-
99
- # Members.
100
- attr_reader :unused, :name, :dirty
101
- attr_accessor :parentCluster, :parentOffset
102
- # NOTE: Directory is responsible for setting parent.
103
- # These describe the cluster & offset of the START of the directory entry.
104
-
105
- # Initialization
106
- def initialize(buf = nil)
107
- # Create for write.
108
- if buf == nil
109
- self.create
110
- return
111
- end
112
-
113
- # Handle possibly multiple LFN records.
114
- data = StringIO.new(buf); @lfn_ents = []
115
- checksum = 0; @name = ""
116
- loop do
117
- buf = data.read(DIR_ENT_SIZE)
118
- if buf == nil
119
- @unused = ""
120
- return
121
- end
122
-
123
- # If attribute contains 0x0f then LFN entry.
124
- isLfn = buf[ATTRIB_OFFSET] == FA_LFN
125
- @dir_ent = isLfn ? DIR_ENT_LFN.decode(buf) : DIR_ENT_SFN.decode(buf)
126
- break if !isLfn
127
-
128
- # Ignore this entry if deleted or not allocated.
129
- af = @dir_ent['seq_num']
130
- if af == AF_DELETED || af == AF_NOT_ALLOCATED
131
- @name = @dir_ent['seq_num']
132
- @unused = data.read()
133
- return
134
- end
135
-
136
- # Set checksum or make sure it's the same
137
- checksum = @dir_ent['checksum'] if checksum == 0
138
- raise "Directory entry LFN checksum mismatch." if @dir_ent['checksum'] != checksum
139
-
140
- # Track LFN entry, gather names & prepend to name.
141
- @lfn_ents << @dir_ent
142
- @name = getLongNameFromEntry(@dir_ent) + @name
143
- end #LFN loop
144
-
145
- # Push the rest of the data back.
146
- @unused = data.read()
147
-
148
- # If this is the last record of an LFN chain, check the checksum.
149
- if checksum != 0
150
- csum = calcChecksum
151
- if csum != checksum
152
- puts "Directory entry SFN checksum does not match LFN entries:"
153
- puts "Got 0x#{'%02x' % csum}, should be 0x#{'%02x' % checksum}."
154
- puts "Non LFN OS corruption?"
155
- puts dump
156
- raise "Checksum error"
157
- end
158
- end
159
-
160
- # Populate name if not LFN.
161
- if @name == "" && !@dir_ent['name'].empty?
162
- @name = @dir_ent['name'][0, 8].strip
163
- ext = @dir_ent['name'][8, 3].strip
164
- @name += "." + ext unless ext.empty?
165
- end
166
- end
167
-
168
- # ////////////////////////////////////////////////////////////////////////////
169
- # // Class helpers & accessors.
170
-
171
- # Return this entry as a raw string.
172
- def raw
173
- out = ""
174
- @lfn_ents.each {|ent| out += BinaryStruct.encode(ent, DIR_ENT_LFN)} if @lfn_ents
175
- out += BinaryStruct.encode(@dir_ent, DIR_ENT_SFN)
176
- end
177
-
178
- # Number of dir ent structures (both sfn and lfn).
179
- def numEnts
180
- num = 1
181
- num += @lfn_ents.size if @lfn_ents
182
- return num
183
- end
184
-
185
- # Return normalized 8.3 name.
186
- def shortName
187
- name = @dir_ent['name'][0, 8].strip
188
- ext = @dir_ent['name'][8, 3].strip
189
- name += "." + ext if ext != ""
190
- return name
191
- end
192
-
193
- # Construct & return long name from lfn entries.
194
- def longName
195
- return nil if @lfn_ents == nil
196
- name = ""
197
- @lfn_ents.reverse.each {|ent| name += getLongNameFromEntry(ent)}
198
- return name
199
- end
200
-
201
- # WRITE: change filename.
202
- def name=(filename)
203
- @dirty = true
204
- # dot and double dot are special cases (no processing please).
205
- if filename != "." and filename != ".."
206
- if filename.size > 12 || (not filename.include?(".") && filename.size > 8)
207
- mkLongName(filename)
208
- @name = self.longName
209
- else
210
- @dir_ent['name'] = mkSfn(filename)
211
- @name = self.shortName
212
- end
213
- else
214
- @dir_ent['name']= filename.ljust(11)
215
- @name = filename
216
- end
217
- end
218
-
219
- # WRITE: change magic number.
220
- def magic=(magic)
221
- @dirty = true
222
- @dir_ent['reserved1'] = magic
223
- end
224
-
225
- def magic
226
- return @dir_ent['reserved1']
227
- end
228
-
229
- # WRITE: change attribs.
230
- def setAttribute(attrib, set = true)
231
- @dirty = true
232
- if set
233
- @dir_ent['attributes'] |= attrib
234
- else
235
- @dir_ent['attributes'] &= (~attrib)
236
- end
237
- end
238
-
239
- # WRITE: change length.
240
- def length=(len)
241
- @dirty = true
242
- @dir_ent['file_size'] = len
243
- end
244
-
245
- # WRITE: change first cluster.
246
- def firstCluster=(first_clus)
247
- @dirty = true
248
- @dir_ent['first_clus_hi'] = (first_clus >> 16)
249
- @dir_ent['first_clus_lo'] = (first_clus & 0xffff)
250
- end
251
-
252
- # WRITE: change access time.
253
- def aTime=(tim)
254
- @dirty = true
255
- time, day = rubyToDosTime(tim)
256
- @dir_ent['atime_day'] = day
257
- end
258
-
259
- # To support root dir times (all zero).
260
- def zeroTime
261
- @dirty = true
262
- @dir_ent['atime_day'] = 0
263
- @dir_ent['ctime_tos'] = 0; @dir_ent['ctime_hms'] = 0; @dir_ent['ctime_day'] = 0
264
- @dir_ent['mtime_hms'] = 0; @dir_ent['mtime_day'] = 0
265
- end
266
-
267
- # WRITE: change modified (written) time.
268
- def mTime=(tim)
269
- @dirty = true
270
- @dir_ent['mtime_hms'], @dir_ent['mtime_day'] = rubyToDosTime(tim)
271
- end
272
-
273
- # WRITE: write or rewrite directory entry.
274
- def writeEntry(bs)
275
- return if not @dirty
276
- cluster = @parentCluster; offset = @parentOffset
277
- buf = bs.getCluster(cluster)
278
- if @lfn_ents
279
- @lfn_ents.each {|ent|
280
- buf[offset...(offset + DIR_ENT_SIZE)] = BinaryStruct.encode(ent, DIR_ENT_LFN)
281
- offset += DIR_ENT_SIZE
282
- if offset >= bs.bytesPerCluster
283
- bs.putCluster(cluster, buf)
284
- cluster, buf = bs.getNextCluster(cluster)
285
- offset = 0
286
- end
287
- }
288
- end
289
- buf[offset...(offset + DIR_ENT_SIZE)] = BinaryStruct.encode(@dir_ent, DIR_ENT_SFN)
290
- bs.putCluster(cluster, buf)
291
- @dirty = false
292
- end
293
-
294
- # WRITE: delete file.
295
- def delete(bs)
296
- # Deallocate data chain.
297
- bs.wipeChain(self.firstCluster) if self.firstCluster != 0
298
- # Deallocate dir entry.
299
- if @lfn_ents then @lfn_ents.each {|ent| ent['seq_num'] = AF_DELETED} end
300
- @dir_ent['name'][0] = AF_DELETED
301
- @dirty = true
302
- self.writeEntry(bs)
303
- end
304
-
305
- def close(bs)
306
- writeEntry(bs) if @dirty
307
- end
308
-
309
- def attributes
310
- return @dir_ent['attributes']
311
- end
312
-
313
- def length
314
- return @dir_ent['file_size']
315
- end
316
-
317
- def firstCluster
318
- return (@dir_ent['first_clus_hi'] << 16) + @dir_ent['first_clus_lo']
319
- end
320
-
321
- def isDir?
322
- return true if @dir_ent['attributes'] & FA_DIRECTORY == FA_DIRECTORY
323
- return false
324
- end
325
-
326
- def mTime
327
- return dosToRubyTime(@dir_ent['mtime_day'], @dir_ent['mtime_hms'])
328
- end
329
-
330
- def aTime
331
- return dosToRubyTime(@dir_ent['atime_day'], 0)
332
- end
333
-
334
- def cTime
335
- return dosToRubyTime(@dir_ent['ctime_day'], @dir_ent['ctime_hms'])
336
- end
337
-
338
- # ////////////////////////////////////////////////////////////////////////////
339
- # // Utility functions.
340
-
341
- def getLongNameFromEntry(ent)
342
- pre_name = ""; hashNames = %w(name name2 name3)
343
- hashNames.each {|name|
344
- n = ent["#{name}"]
345
-
346
- # Regexp.new options used below:
347
- # nil (default options: not case insensitive, extended, multiline, etc.)
348
- # 'n' - No encoding on the regexp
349
- regex = Regexp.new('\377', nil, 'n')
350
- pre_name += n.gsub(regex, "").UnicodeToUtf8.gsub(/\000/, "")
351
- }
352
- return pre_name
353
- end
354
-
355
- def incShortName
356
- @dirty = true
357
- num = @dir_ent['name'][7].to_i
358
- num += 1
359
- raise "More than 9 files with name: #{@dir_ent['name'][0, 6]}" if num > 57
360
- @dir_ent['name'][7] = num
361
- csum = calcChecksum()
362
- if @lfn_ents
363
- @lfn_ents.each {|ent| ent['checksum'] = csum}
364
- end
365
- end
366
-
367
- def create
368
- @dirty = true
369
- @dir_ent = Hash.new
370
- @dir_ent['name'] = "FILENAMEEXT"
371
- @name = self.shortName
372
- @dir_ent['attributes'] = FA_ARCHIVE
373
- @dir_ent['ctime_tos'] = 0
374
- @dir_ent['ctime_hms'], @dir_ent['ctime_day'] = rubyToDosTime(Time.now)
375
- @dir_ent['atime_day'] = @dir_ent['ctime_day']
376
- @dir_ent['mtime_hms'], @dir_ent['mtime_day'] = @dir_ent['ctime_hms'], @dir_ent['ctime_day']
377
- # Must fill all members or BinaryStruct.encode fails.
378
- self.magic = 0x00; self.length = 0; self.firstCluster = 0 #magic used to be 0x18
379
- end
380
-
381
- def mkLongName(name)
382
- @lfn_ents = mkLfn(name)
383
- @dir_ent['name'] = mkSfn(name)
384
- # Change magic number to 0.
385
- @dir_ent['reserved1'] = 0
386
- # Do checksums in lfn entries.
387
- csum = calcChecksum()
388
- @lfn_ents.each {|ent| ent['checksum'] = csum}
389
- end
390
-
391
- def mkLfn(name)
392
- name = mkLegalLfn(name)
393
- lfn_ents = []
394
- # Get number of LFN entries necessary to encode name.
395
- ents, leftover = name.length.divmod(CHARS_PER_LFN)
396
- if leftover > 0
397
- ents += 1
398
- name += "\000"
399
- end
400
- # Split out & convert name components.
401
- 1.upto(ents) {|ent_num|
402
- ent = {}; ent['attributes'] = FA_LFN; ent['seq_num'] = ent_num
403
- ent['reserved1'] = 0; ent['reserved2'] = 0;
404
- LFN_NAME_COMPONENTS.each {|comp|
405
- chStart = (ent_num - 1) * CHARS_PER_LFN + comp[LFN_NC_OFFSET]
406
- if chStart > name.length
407
- ent["#{comp[LFN_NC_HASHNAME]}"] = "\377".b * (comp[LFN_NC_LENGTH] * 2)
408
- else
409
- ptName = name[chStart, comp[LFN_NC_LENGTH]]
410
- ptName.Utf8ToUnicode!
411
- if ptName.length < comp[LFN_NC_LENGTH] * 2
412
- ptName += "\377".b * (comp[LFN_NC_LENGTH] * 2 - ptName.length)
413
- end
414
- ent["#{comp[LFN_NC_HASHNAME]}"] = ptName
415
- end
416
- }
417
- lfn_ents << ent
418
- }
419
- lfn_ents.reverse!
420
- lfn_ents[0]['seq_num'] |= AF_LFN_LAST
421
- return lfn_ents
422
- end
423
-
424
- def mkSfn(name)
425
- return mkLegalSfn(name)
426
- end
427
-
428
- def isIllegalSfn(name)
429
- # Check: name length, extension length, NULL file name,
430
- # device names as file names & illegal chars.
431
- return SFN_NAME_LENGTH if name.length > 12
432
- extpos = name.reverse.index(".")
433
- return SFN_EXT_LENGTH if extpos > 3
434
- return SFN_NAME_NULL if extpos == 0
435
- fn = name[0...extpos].downcase
436
- return SFN_NAME_DEVICE if checkForDeviceNames(fn)
437
- return SFN_ILLEGAL_CHARS if name.index(/[;+=\[\]',\"*\\<>\/?\:|]/) != nil
438
- return false
439
- end
440
-
441
- def checkForDeviceName(fn)
442
- %w[aux com1 com2 com3 com4 lpt lpt1 lpt2 lpt3 lpt4 mailslot nul pipe prn].each {|bad|
443
- return true if fn == bad
444
- }
445
- return false
446
- end
447
-
448
- def mkLegalSfn(name)
449
- name = name.upcase; name = name.delete(" ")
450
- name = name + "." if not name.include?(".")
451
- extpos = name.reverse.index(".")
452
- if extpos == 0 then ext = "" else ext = name[-extpos, 3] end
453
- fn = name[0, (name.length - extpos - 1)]
454
- fn = fn[0, 6] + "~1" if fn.length > 8
455
- return (fn.ljust(8) + ext.ljust(3)).gsub(/[;+=\[\]',\"*\\<>\/?\:|]/, "_")
456
- end
457
-
458
- def isIllegalLfn(name)
459
- return LFN_NAME_LENGTH if name.length > LFN_NAME_MAXLEN
460
- return LFN_ILLEGAL_CHARS if name.index(/\/\\:><?/) != nil
461
- return false
462
- end
463
-
464
- def mkLegalLfn(name)
465
- name = name[0...LFN_NAME_MAXLEN] if name.length > LFN_NAME_MAXLEN
466
- return name.gsub(/\/\\:><?/, "_")
467
- end
468
-
469
- def calcChecksum
470
- name = @dir_ent['name']; csum = 0
471
- 0.upto(10) {|i|
472
- csum = ((csum & 1 == 1 ? 0x80 : 0) + (csum >> 1) + name[i]) & 0xff
473
- }
474
- return csum
475
- end
476
-
477
- def dosToRubyTime(dos_day, dos_time)
478
- # Extract d,m,y,s,m,h & range check.
479
- day = dos_day & MSK_DAY; day = 1 if day == 0
480
- month = (dos_day & MSK_MONTH) >> 5; month = 1 if month == 0
481
- month = month.modulo(12) if month > 12
482
- year = ((dos_day & MSK_YEAR) >> 9) + 1980 #DOS year epoc is 1980.
483
- # Extract seconds, range check & expand granularity.
484
- sec = (dos_time & MSK_SEC); sec = sec.modulo(29) if sec > 29; sec *= 2
485
- min = (dos_time & MSK_MIN) >> 5; min = min.modulo(59) if min > 59
486
- hour = (dos_time & MSK_HOUR) >> 11; hour = hour.modulo(23) if hour > 23
487
- # Make a Ruby time.
488
- return Time.mktime(year, month, day, hour, min, sec)
489
- end
490
-
491
- def rubyToDosTime(tim)
492
- # Time
493
- sec = tim.sec; sec -= 1 if sec == 60 #correction for possible leap second.
494
- sec = (sec / 2).to_i #dos granularity is 2sec.
495
- min = tim.min; hour = tim.hour
496
- dos_time = (hour << 11) + (min << 5) + sec
497
- # Day
498
- day = tim.day; month = tim.month
499
- # NOTE: This fails after 2107.
500
- year = tim.year - 1980 #DOS year epoc is 1980.
501
- dos_day = (year << 9) + (month << 5) + day
502
- return dos_time, dos_day
503
- end
504
-
505
- # Dump object.
506
- def dump
507
- out = "\#<#{self.class}:0x#{'%08x' % self.object_id}>\n"
508
- if @lfn_ents
509
- out += "LFN Entries:\n"
510
- @lfn_ents.each {|ent|
511
- out += "Sequence num : 0x#{'%02x' % ent['seq_num']}\n"
512
- n = ent['name']; n.UnicodeToUtf8! unless n == nil
513
- out += "Name1 : '#{n}'\n"
514
- out += "Attributes : 0x#{'%02x' % ent['attributes']}\n"
515
- out += "Reserved1 : 0x#{'%02x' % ent['reserved1']}\n"
516
- out += "Checksum : 0x#{'%02x' % ent['checksum']}\n"
517
- n = ent['name2']; n.UnicodeToUtf8! unless n == nil
518
- out += "Name2 : '#{n}'\n"
519
- out += "Reserved2 : 0x#{'%04x' % ent['reserved2']}\n"
520
- n = ent['name3']; n.UnicodeToUtf8! unless n == nil
521
- out += "Name3 : '#{n}'\n\n"
522
- }
523
- end
524
- out += "SFN Entry:\n"
525
- out += "Name : #{@dir_ent['name']}\n"
526
- out += "Attributes : 0x#{'%02x' % @dir_ent['attributes']}\n"
527
- out += "Reserved1 : 0x#{'%02x' % @dir_ent['reserved1']}\n"
528
- out += "CTime, tenths: 0x#{'%02x' % @dir_ent['ctime_tos']}\n"
529
- out += "CTime, hms : 0x#{'%04x' % @dir_ent['ctime_hms']}\n"
530
- out += "CTime, day : 0x#{'%04x' % @dir_ent['ctime_day']} (#{cTime})\n"
531
- out += "ATime, day : 0x#{'%04x' % @dir_ent['atime_day']} (#{aTime})\n"
532
- out += "First clus hi: 0x#{'%04x' % @dir_ent['first_clus_hi']}\n"
533
- out += "MTime, hms : 0x#{'%04x' % @dir_ent['mtime_hms']}\n"
534
- out += "MTime, day : 0x#{'%04x' % @dir_ent['mtime_day']} (#{mTime})\n"
535
- out += "First clus lo: 0x#{'%04x' % @dir_ent['first_clus_lo']}\n"
536
- out += "File size : 0x#{'%08x' % @dir_ent['file_size']}\n"
537
- end
538
-
539
- end
540
- end # module Fat32
1
+ # encoding: US-ASCII
2
+
3
+ require 'stringio'
4
+ require 'binary_struct'
5
+ require 'miq_unicode'
6
+
7
+ # ////////////////////////////////////////////////////////////////////////////
8
+ # // Data definitions.
9
+
10
+ # TODO: reserved1 is the infamous magic number. Somehow it works to preserve
11
+ # case on Windows XP. Nobody seems to know how. Here it is always set to 0
12
+ # (which yields uppercase names on XP).
13
+
14
+ module Fat32
15
+ using ManageIQ::UnicodeString
16
+
17
+ DIR_ENT_SFN = BinaryStruct.new([
18
+ 'a11', 'name', # If name[0] = 0, unallocated; if name[0] = 0xe5, deleted. DOES NOT INCLUDE DOT.
19
+ 'C', 'attributes', # See FA_ below. If 0x0f then LFN entry.
20
+ 'C', 'reserved1', # Reserved.
21
+ 'C', 'ctime_tos', # Created time, tenths of second.
22
+ 'S', 'ctime_hms', # Created time, hours, minutes & seconds.
23
+ 'S', 'ctime_day', # Created day.
24
+ 'S', 'atime_day', # Accessed day.
25
+ 'S', 'first_clus_hi', # Hi 16-bits of first cluster address.
26
+ 'S', 'mtime_hms', # Modified time, hours, minutes & seconds.
27
+ 'S', 'mtime_day', # Modified day.
28
+ 'S', 'first_clus_lo', # Lo 16-bits of first cluster address.
29
+ 'L', 'file_size', # Size of file (0 for directories).
30
+ ])
31
+
32
+ DIR_ENT_LFN = BinaryStruct.new([
33
+ 'C', 'seq_num', # Sequence number, bit 6 marks end, 0xe5 if deleted.
34
+ 'a10', 'name', # UNICODE chars 1-5 of name.
35
+ 'C', 'attributes', # Always 0x0f.
36
+ 'C', 'reserved1', # Reserved.
37
+ 'C', 'checksum', # Checksum of SFN entry, all LFN entries must match.
38
+ 'a12', 'name2', # UNICODE chars 6-11 of name.
39
+ 'S', 'reserved2', # Reserved.
40
+ 'a4', 'name3' # UNICODE chars 12-13 of name.
41
+ ])
42
+
43
+ CHARS_PER_LFN = 13
44
+ LFN_NAME_MAXLEN = 260
45
+ DIR_ENT_SIZE = 32
46
+ ATTRIB_OFFSET = 11
47
+
48
+ # ////////////////////////////////////////////////////////////////////////////
49
+ # // Class.
50
+
51
+ class DirectoryEntry
52
+
53
+ # From the UTF-8 perspective.
54
+ # LFN name components: entry hash name, char offset, length.
55
+ LFN_NAME_COMPONENTS = [
56
+ ['name', 0, 5],
57
+ ['name2', 5, 6],
58
+ ['name3', 11, 2]
59
+ ]
60
+ # Name component second sub access names.
61
+ LFN_NC_HASHNAME = 0
62
+ LFN_NC_OFFSET = 1
63
+ LFN_NC_LENGTH = 2
64
+
65
+ # SFN failure cases.
66
+ SFN_NAME_LENGTH = 1
67
+ SFN_EXT_LENGTH = 2
68
+ SFN_NAME_NULL = 3
69
+ SFN_NAME_DEVICE = 4
70
+ SFN_ILLEGAL_CHARS = 5
71
+
72
+ # LFN failure cases.
73
+ LFN_NAME_LENGTH = 1
74
+ LFN_NAME_DEVICE = 2
75
+ LFN_ILLEGAL_CHARS = 3
76
+
77
+ # FileAttributes
78
+ FA_READONLY = 0x01
79
+ FA_HIDDEN = 0x02
80
+ FA_SYSTEM = 0x04
81
+ FA_LABEL = 0x08
82
+ FA_DIRECTORY = 0x10
83
+ FA_ARCHIVE = 0x20
84
+ FA_LFN = 0x0f
85
+
86
+ # DOS time masks.
87
+ MSK_DAY = 0x001f # Range: 1 - 31
88
+ MSK_MONTH = 0x01e0 # Right shift 5, Range: 1 - 12
89
+ MSK_YEAR = 0xfe00 # Right shift 9, Range: 127 (add 1980 for year).
90
+ MSK_SEC = 0x001f # Range: 0 - 29 WARNING: 2 second granularity on this.
91
+ MSK_MIN = 0x07e0 # Right shift 5, Range: 0 - 59
92
+ MSK_HOUR = 0xf800 # Right shift 11, Range: 0 - 23
93
+
94
+ # AllocationFlags
95
+ AF_NOT_ALLOCATED = 0x00
96
+ AF_DELETED = 0xe5
97
+ AF_LFN_LAST = 0x40
98
+
99
+ # Members.
100
+ attr_reader :unused, :name, :dirty
101
+ attr_accessor :parentCluster, :parentOffset
102
+ # NOTE: Directory is responsible for setting parent.
103
+ # These describe the cluster & offset of the START of the directory entry.
104
+
105
+ # Initialization
106
+ def initialize(buf = nil)
107
+ # Create for write.
108
+ if buf == nil
109
+ self.create
110
+ return
111
+ end
112
+
113
+ # Handle possibly multiple LFN records.
114
+ data = StringIO.new(buf); @lfn_ents = []
115
+ checksum = 0; @name = ""
116
+ loop do
117
+ buf = data.read(DIR_ENT_SIZE)
118
+ if buf == nil
119
+ @unused = ""
120
+ return
121
+ end
122
+
123
+ # If attribute contains 0x0f then LFN entry.
124
+ isLfn = buf[ATTRIB_OFFSET] == FA_LFN
125
+ @dir_ent = isLfn ? DIR_ENT_LFN.decode(buf) : DIR_ENT_SFN.decode(buf)
126
+ break if !isLfn
127
+
128
+ # Ignore this entry if deleted or not allocated.
129
+ af = @dir_ent['seq_num']
130
+ if af == AF_DELETED || af == AF_NOT_ALLOCATED
131
+ @name = @dir_ent['seq_num']
132
+ @unused = data.read()
133
+ return
134
+ end
135
+
136
+ # Set checksum or make sure it's the same
137
+ checksum = @dir_ent['checksum'] if checksum == 0
138
+ raise "Directory entry LFN checksum mismatch." if @dir_ent['checksum'] != checksum
139
+
140
+ # Track LFN entry, gather names & prepend to name.
141
+ @lfn_ents << @dir_ent
142
+ @name = getLongNameFromEntry(@dir_ent) + @name
143
+ end #LFN loop
144
+
145
+ # Push the rest of the data back.
146
+ @unused = data.read()
147
+
148
+ # If this is the last record of an LFN chain, check the checksum.
149
+ if checksum != 0
150
+ csum = calcChecksum
151
+ if csum != checksum
152
+ puts "Directory entry SFN checksum does not match LFN entries:"
153
+ puts "Got 0x#{'%02x' % csum}, should be 0x#{'%02x' % checksum}."
154
+ puts "Non LFN OS corruption?"
155
+ puts dump
156
+ raise "Checksum error"
157
+ end
158
+ end
159
+
160
+ # Populate name if not LFN.
161
+ if @name == "" && !@dir_ent['name'].empty?
162
+ @name = @dir_ent['name'][0, 8].strip
163
+ ext = @dir_ent['name'][8, 3].strip
164
+ @name += "." + ext unless ext.empty?
165
+ end
166
+ end
167
+
168
+ # ////////////////////////////////////////////////////////////////////////////
169
+ # // Class helpers & accessors.
170
+
171
+ # Return this entry as a raw string.
172
+ def raw
173
+ out = ""
174
+ @lfn_ents.each {|ent| out += BinaryStruct.encode(ent, DIR_ENT_LFN)} if @lfn_ents
175
+ out += BinaryStruct.encode(@dir_ent, DIR_ENT_SFN)
176
+ end
177
+
178
+ # Number of dir ent structures (both sfn and lfn).
179
+ def numEnts
180
+ num = 1
181
+ num += @lfn_ents.size if @lfn_ents
182
+ return num
183
+ end
184
+
185
+ # Return normalized 8.3 name.
186
+ def shortName
187
+ name = @dir_ent['name'][0, 8].strip
188
+ ext = @dir_ent['name'][8, 3].strip
189
+ name += "." + ext if ext != ""
190
+ return name
191
+ end
192
+
193
+ # Construct & return long name from lfn entries.
194
+ def longName
195
+ return nil if @lfn_ents == nil
196
+ name = ""
197
+ @lfn_ents.reverse.each {|ent| name += getLongNameFromEntry(ent)}
198
+ return name
199
+ end
200
+
201
+ # WRITE: change filename.
202
+ def name=(filename)
203
+ @dirty = true
204
+ # dot and double dot are special cases (no processing please).
205
+ if filename != "." and filename != ".."
206
+ if filename.size > 12 || (not filename.include?(".") && filename.size > 8)
207
+ mkLongName(filename)
208
+ @name = self.longName
209
+ else
210
+ @dir_ent['name'] = mkSfn(filename)
211
+ @name = self.shortName
212
+ end
213
+ else
214
+ @dir_ent['name']= filename.ljust(11)
215
+ @name = filename
216
+ end
217
+ end
218
+
219
+ # WRITE: change magic number.
220
+ def magic=(magic)
221
+ @dirty = true
222
+ @dir_ent['reserved1'] = magic
223
+ end
224
+
225
+ def magic
226
+ return @dir_ent['reserved1']
227
+ end
228
+
229
+ # WRITE: change attribs.
230
+ def setAttribute(attrib, set = true)
231
+ @dirty = true
232
+ if set
233
+ @dir_ent['attributes'] |= attrib
234
+ else
235
+ @dir_ent['attributes'] &= (~attrib)
236
+ end
237
+ end
238
+
239
+ # WRITE: change length.
240
+ def length=(len)
241
+ @dirty = true
242
+ @dir_ent['file_size'] = len
243
+ end
244
+
245
+ # WRITE: change first cluster.
246
+ def firstCluster=(first_clus)
247
+ @dirty = true
248
+ @dir_ent['first_clus_hi'] = (first_clus >> 16)
249
+ @dir_ent['first_clus_lo'] = (first_clus & 0xffff)
250
+ end
251
+
252
+ # WRITE: change access time.
253
+ def aTime=(tim)
254
+ @dirty = true
255
+ time, day = rubyToDosTime(tim)
256
+ @dir_ent['atime_day'] = day
257
+ end
258
+
259
+ # To support root dir times (all zero).
260
+ def zeroTime
261
+ @dirty = true
262
+ @dir_ent['atime_day'] = 0
263
+ @dir_ent['ctime_tos'] = 0; @dir_ent['ctime_hms'] = 0; @dir_ent['ctime_day'] = 0
264
+ @dir_ent['mtime_hms'] = 0; @dir_ent['mtime_day'] = 0
265
+ end
266
+
267
+ # WRITE: change modified (written) time.
268
+ def mTime=(tim)
269
+ @dirty = true
270
+ @dir_ent['mtime_hms'], @dir_ent['mtime_day'] = rubyToDosTime(tim)
271
+ end
272
+
273
+ # WRITE: write or rewrite directory entry.
274
+ def writeEntry(bs)
275
+ return if not @dirty
276
+ cluster = @parentCluster; offset = @parentOffset
277
+ buf = bs.getCluster(cluster)
278
+ if @lfn_ents
279
+ @lfn_ents.each {|ent|
280
+ buf[offset...(offset + DIR_ENT_SIZE)] = BinaryStruct.encode(ent, DIR_ENT_LFN)
281
+ offset += DIR_ENT_SIZE
282
+ if offset >= bs.bytesPerCluster
283
+ bs.putCluster(cluster, buf)
284
+ cluster, buf = bs.getNextCluster(cluster)
285
+ offset = 0
286
+ end
287
+ }
288
+ end
289
+ buf[offset...(offset + DIR_ENT_SIZE)] = BinaryStruct.encode(@dir_ent, DIR_ENT_SFN)
290
+ bs.putCluster(cluster, buf)
291
+ @dirty = false
292
+ end
293
+
294
+ # WRITE: delete file.
295
+ def delete(bs)
296
+ # Deallocate data chain.
297
+ bs.wipeChain(self.firstCluster) if self.firstCluster != 0
298
+ # Deallocate dir entry.
299
+ if @lfn_ents then @lfn_ents.each {|ent| ent['seq_num'] = AF_DELETED} end
300
+ @dir_ent['name'][0] = AF_DELETED
301
+ @dirty = true
302
+ self.writeEntry(bs)
303
+ end
304
+
305
+ def close(bs)
306
+ writeEntry(bs) if @dirty
307
+ end
308
+
309
+ def attributes
310
+ return @dir_ent['attributes']
311
+ end
312
+
313
+ def length
314
+ return @dir_ent['file_size']
315
+ end
316
+
317
+ def firstCluster
318
+ return (@dir_ent['first_clus_hi'] << 16) + @dir_ent['first_clus_lo']
319
+ end
320
+
321
+ def isDir?
322
+ return true if @dir_ent['attributes'] & FA_DIRECTORY == FA_DIRECTORY
323
+ return false
324
+ end
325
+
326
+ def mTime
327
+ return dosToRubyTime(@dir_ent['mtime_day'], @dir_ent['mtime_hms'])
328
+ end
329
+
330
+ def aTime
331
+ return dosToRubyTime(@dir_ent['atime_day'], 0)
332
+ end
333
+
334
+ def cTime
335
+ return dosToRubyTime(@dir_ent['ctime_day'], @dir_ent['ctime_hms'])
336
+ end
337
+
338
+ # ////////////////////////////////////////////////////////////////////////////
339
+ # // Utility functions.
340
+
341
+ def getLongNameFromEntry(ent)
342
+ pre_name = ""; hashNames = %w(name name2 name3)
343
+ hashNames.each {|name|
344
+ n = ent["#{name}"]
345
+
346
+ # Regexp.new options used below:
347
+ # nil (default options: not case insensitive, extended, multiline, etc.)
348
+ # 'n' - No encoding on the regexp
349
+ regex = Regexp.new('\377', nil, 'n')
350
+ pre_name += n.gsub(regex, "").UnicodeToUtf8.gsub(/\000/, "")
351
+ }
352
+ return pre_name
353
+ end
354
+
355
+ def incShortName
356
+ @dirty = true
357
+ num = @dir_ent['name'][7].to_i
358
+ num += 1
359
+ raise "More than 9 files with name: #{@dir_ent['name'][0, 6]}" if num > 57
360
+ @dir_ent['name'][7] = num
361
+ csum = calcChecksum()
362
+ if @lfn_ents
363
+ @lfn_ents.each {|ent| ent['checksum'] = csum}
364
+ end
365
+ end
366
+
367
+ def create
368
+ @dirty = true
369
+ @dir_ent = Hash.new
370
+ @dir_ent['name'] = "FILENAMEEXT"
371
+ @name = self.shortName
372
+ @dir_ent['attributes'] = FA_ARCHIVE
373
+ @dir_ent['ctime_tos'] = 0
374
+ @dir_ent['ctime_hms'], @dir_ent['ctime_day'] = rubyToDosTime(Time.now)
375
+ @dir_ent['atime_day'] = @dir_ent['ctime_day']
376
+ @dir_ent['mtime_hms'], @dir_ent['mtime_day'] = @dir_ent['ctime_hms'], @dir_ent['ctime_day']
377
+ # Must fill all members or BinaryStruct.encode fails.
378
+ self.magic = 0x00; self.length = 0; self.firstCluster = 0 #magic used to be 0x18
379
+ end
380
+
381
+ def mkLongName(name)
382
+ @lfn_ents = mkLfn(name)
383
+ @dir_ent['name'] = mkSfn(name)
384
+ # Change magic number to 0.
385
+ @dir_ent['reserved1'] = 0
386
+ # Do checksums in lfn entries.
387
+ csum = calcChecksum()
388
+ @lfn_ents.each {|ent| ent['checksum'] = csum}
389
+ end
390
+
391
+ def mkLfn(name)
392
+ name = mkLegalLfn(name)
393
+ lfn_ents = []
394
+ # Get number of LFN entries necessary to encode name.
395
+ ents, leftover = name.length.divmod(CHARS_PER_LFN)
396
+ if leftover > 0
397
+ ents += 1
398
+ name += "\000"
399
+ end
400
+ # Split out & convert name components.
401
+ 1.upto(ents) {|ent_num|
402
+ ent = {}; ent['attributes'] = FA_LFN; ent['seq_num'] = ent_num
403
+ ent['reserved1'] = 0; ent['reserved2'] = 0;
404
+ LFN_NAME_COMPONENTS.each {|comp|
405
+ chStart = (ent_num - 1) * CHARS_PER_LFN + comp[LFN_NC_OFFSET]
406
+ if chStart > name.length
407
+ ent["#{comp[LFN_NC_HASHNAME]}"] = "\377".b * (comp[LFN_NC_LENGTH] * 2)
408
+ else
409
+ ptName = name[chStart, comp[LFN_NC_LENGTH]]
410
+ ptName.Utf8ToUnicode!
411
+ if ptName.length < comp[LFN_NC_LENGTH] * 2
412
+ ptName += "\377".b * (comp[LFN_NC_LENGTH] * 2 - ptName.length)
413
+ end
414
+ ent["#{comp[LFN_NC_HASHNAME]}"] = ptName
415
+ end
416
+ }
417
+ lfn_ents << ent
418
+ }
419
+ lfn_ents.reverse!
420
+ lfn_ents[0]['seq_num'] |= AF_LFN_LAST
421
+ return lfn_ents
422
+ end
423
+
424
+ def mkSfn(name)
425
+ return mkLegalSfn(name)
426
+ end
427
+
428
+ def isIllegalSfn(name)
429
+ # Check: name length, extension length, NULL file name,
430
+ # device names as file names & illegal chars.
431
+ return SFN_NAME_LENGTH if name.length > 12
432
+ extpos = name.reverse.index(".")
433
+ return SFN_EXT_LENGTH if extpos > 3
434
+ return SFN_NAME_NULL if extpos == 0
435
+ fn = name[0...extpos].downcase
436
+ return SFN_NAME_DEVICE if checkForDeviceNames(fn)
437
+ return SFN_ILLEGAL_CHARS if name.index(/[;+=\[\]',\"*\\<>\/?\:|]/) != nil
438
+ return false
439
+ end
440
+
441
+ def checkForDeviceName(fn)
442
+ %w[aux com1 com2 com3 com4 lpt lpt1 lpt2 lpt3 lpt4 mailslot nul pipe prn].each {|bad|
443
+ return true if fn == bad
444
+ }
445
+ return false
446
+ end
447
+
448
+ def mkLegalSfn(name)
449
+ name = name.upcase; name = name.delete(" ")
450
+ name = name + "." if not name.include?(".")
451
+ extpos = name.reverse.index(".")
452
+ if extpos == 0 then ext = "" else ext = name[-extpos, 3] end
453
+ fn = name[0, (name.length - extpos - 1)]
454
+ fn = fn[0, 6] + "~1" if fn.length > 8
455
+ return (fn.ljust(8) + ext.ljust(3)).gsub(/[;+=\[\]',\"*\\<>\/?\:|]/, "_")
456
+ end
457
+
458
+ def isIllegalLfn(name)
459
+ return LFN_NAME_LENGTH if name.length > LFN_NAME_MAXLEN
460
+ return LFN_ILLEGAL_CHARS if name.index(/\/\\:><?/) != nil
461
+ return false
462
+ end
463
+
464
+ def mkLegalLfn(name)
465
+ name = name[0...LFN_NAME_MAXLEN] if name.length > LFN_NAME_MAXLEN
466
+ return name.gsub(/\/\\:><?/, "_")
467
+ end
468
+
469
+ def calcChecksum
470
+ name = @dir_ent['name']; csum = 0
471
+ 0.upto(10) {|i|
472
+ csum = ((csum & 1 == 1 ? 0x80 : 0) + (csum >> 1) + name[i]) & 0xff
473
+ }
474
+ return csum
475
+ end
476
+
477
+ def dosToRubyTime(dos_day, dos_time)
478
+ # Extract d,m,y,s,m,h & range check.
479
+ day = dos_day & MSK_DAY; day = 1 if day == 0
480
+ month = (dos_day & MSK_MONTH) >> 5; month = 1 if month == 0
481
+ month = month.modulo(12) if month > 12
482
+ year = ((dos_day & MSK_YEAR) >> 9) + 1980 #DOS year epoc is 1980.
483
+ # Extract seconds, range check & expand granularity.
484
+ sec = (dos_time & MSK_SEC); sec = sec.modulo(29) if sec > 29; sec *= 2
485
+ min = (dos_time & MSK_MIN) >> 5; min = min.modulo(59) if min > 59
486
+ hour = (dos_time & MSK_HOUR) >> 11; hour = hour.modulo(23) if hour > 23
487
+ # Make a Ruby time.
488
+ return Time.mktime(year, month, day, hour, min, sec)
489
+ end
490
+
491
+ def rubyToDosTime(tim)
492
+ # Time
493
+ sec = tim.sec; sec -= 1 if sec == 60 #correction for possible leap second.
494
+ sec = (sec / 2).to_i #dos granularity is 2sec.
495
+ min = tim.min; hour = tim.hour
496
+ dos_time = (hour << 11) + (min << 5) + sec
497
+ # Day
498
+ day = tim.day; month = tim.month
499
+ # NOTE: This fails after 2107.
500
+ year = tim.year - 1980 #DOS year epoc is 1980.
501
+ dos_day = (year << 9) + (month << 5) + day
502
+ return dos_time, dos_day
503
+ end
504
+
505
+ # Dump object.
506
+ def dump
507
+ out = "\#<#{self.class}:0x#{'%08x' % self.object_id}>\n"
508
+ if @lfn_ents
509
+ out += "LFN Entries:\n"
510
+ @lfn_ents.each {|ent|
511
+ out += "Sequence num : 0x#{'%02x' % ent['seq_num']}\n"
512
+ n = ent['name']; n.UnicodeToUtf8! unless n == nil
513
+ out += "Name1 : '#{n}'\n"
514
+ out += "Attributes : 0x#{'%02x' % ent['attributes']}\n"
515
+ out += "Reserved1 : 0x#{'%02x' % ent['reserved1']}\n"
516
+ out += "Checksum : 0x#{'%02x' % ent['checksum']}\n"
517
+ n = ent['name2']; n.UnicodeToUtf8! unless n == nil
518
+ out += "Name2 : '#{n}'\n"
519
+ out += "Reserved2 : 0x#{'%04x' % ent['reserved2']}\n"
520
+ n = ent['name3']; n.UnicodeToUtf8! unless n == nil
521
+ out += "Name3 : '#{n}'\n\n"
522
+ }
523
+ end
524
+ out += "SFN Entry:\n"
525
+ out += "Name : #{@dir_ent['name']}\n"
526
+ out += "Attributes : 0x#{'%02x' % @dir_ent['attributes']}\n"
527
+ out += "Reserved1 : 0x#{'%02x' % @dir_ent['reserved1']}\n"
528
+ out += "CTime, tenths: 0x#{'%02x' % @dir_ent['ctime_tos']}\n"
529
+ out += "CTime, hms : 0x#{'%04x' % @dir_ent['ctime_hms']}\n"
530
+ out += "CTime, day : 0x#{'%04x' % @dir_ent['ctime_day']} (#{cTime})\n"
531
+ out += "ATime, day : 0x#{'%04x' % @dir_ent['atime_day']} (#{aTime})\n"
532
+ out += "First clus hi: 0x#{'%04x' % @dir_ent['first_clus_hi']}\n"
533
+ out += "MTime, hms : 0x#{'%04x' % @dir_ent['mtime_hms']}\n"
534
+ out += "MTime, day : 0x#{'%04x' % @dir_ent['mtime_day']} (#{mTime})\n"
535
+ out += "First clus lo: 0x#{'%04x' % @dir_ent['first_clus_lo']}\n"
536
+ out += "File size : 0x#{'%08x' % @dir_ent['file_size']}\n"
537
+ end
538
+
539
+ end
540
+ end # module Fat32