manageiq-smartstate 0.6.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +34 -35
  3. data/.rubocop.yml +3 -3
  4. data/.rubocop_cc.yml +3 -4
  5. data/.rubocop_local.yml +1 -1
  6. data/.travis.yml +2 -3
  7. data/lib/MiqVm/MiqRhevmVm.rb +1 -1
  8. data/lib/db/MiqBdb/MiqBdb.rb +0 -2
  9. data/lib/db/MiqBdb/MiqBdbPage.rb +0 -2
  10. data/lib/db/MiqSqlite/MiqSqlite3.rb +0 -2
  11. data/lib/db/MiqSqlite/MiqSqlite3Cell.rb +0 -2
  12. data/lib/db/MiqSqlite/MiqSqlite3Page.rb +0 -2
  13. data/lib/db/MiqSqlite/MiqSqlite3Table.rb +0 -2
  14. data/lib/disk/modules/MSCommon.rb +3 -1
  15. data/lib/disk/modules/VhdxDisk.rb +3 -1
  16. data/lib/fs/MiqMountManager.rb +2 -29
  17. data/lib/fs/VimDatastoreFS/VimDatastoreFS.rb +1 -6
  18. data/lib/fs/fat32/directory_entry.rb +540 -540
  19. data/lib/fs/iso9660/boot_sector.rb +3 -2
  20. data/lib/fs/iso9660/directory_entry.rb +3 -2
  21. data/lib/fs/iso9660/rock_ridge.rb +3 -1
  22. data/lib/fs/ntfs/attrib_attribute_list.rb +3 -1
  23. data/lib/fs/ntfs/attrib_file_name.rb +3 -1
  24. data/lib/fs/ntfs/attrib_header.rb +3 -1
  25. data/lib/fs/ntfs/attrib_index_root.rb +3 -1
  26. data/lib/fs/ntfs/attrib_volume_name.rb +3 -1
  27. data/lib/manageiq/smartstate/util.rb +18 -0
  28. data/lib/manageiq/smartstate/version.rb +1 -1
  29. data/lib/metadata/MIQExtract/MIQExtract.rb +2 -3
  30. data/lib/metadata/VmConfig/GetNativeCfg.rb +2 -4
  31. data/lib/metadata/VmConfig/VmConfig.rb +7 -6
  32. data/lib/metadata/VmConfig/cfgConfig.rb +4 -0
  33. data/lib/metadata/VmConfig/xmlConfig.rb +3 -3
  34. data/lib/metadata/linux/MiqRpmPackages.rb +3 -1
  35. data/lib/metadata/util/win32/Win32Accounts.rb +3 -1
  36. data/lib/metadata/util/win32/Win32EventLog.rb +3 -1
  37. data/lib/metadata/util/win32/Win32Software.rb +5 -3
  38. data/lib/metadata/util/win32/decode.rb +0 -0
  39. data/lib/metadata/util/win32/fleece_hives.rb +0 -9
  40. data/lib/metadata/util/win32/ms-registry.rb +3 -2
  41. data/lib/metadata/util/win32/peheader.rb +3 -2
  42. data/lib/metadata/util/win32/system_path_win.rb +0 -2
  43. data/lib/miq_unicode.rb +45 -0
  44. data/manageiq-smartstate.gemspec +8 -6
  45. metadata +66 -23
  46. data/lib/fs/MetakitFS/MetakitFS.rb +0 -530
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc46ede682ac6c4b95558f1856c26b8bedda0cc01a8d4302d72530cf9cf01cbc
4
- data.tar.gz: ded7f8e35edf8a05db918015aaec80b934256232df374a428eb5e22a7911f1ac
3
+ metadata.gz: 3547ea48f2af9d840bfceb8c0c3ae20b6e57bb7b1494e705a940e0998f28d9a6
4
+ data.tar.gz: dbbdc0761323f813a2224f5fc0a11954b60cc49a6d4bca30d39f16d3d4eed67a
5
5
  SHA512:
6
- metadata.gz: ecb7a9753e7863f0a1c2e755e238d45ba88036dd8660a5a71507157347e034e324ff5e5352a056fb7f840aeeb5f761a1224d9172aa7986b71aee9c967ff68211
7
- data.tar.gz: 24ffa299fb984257c85b907a7acd57fd4c364512af79dda73b3c73b6ed610a1f2c61de34750433090d81092659754f6820d16416ea470cdb4ffac8ee86f8352b
6
+ metadata.gz: e2c6075e386e2c396e39aba0d8d5d23f037ee94f262d625bf3b51276e9b6615ef2ea6dac248644bf9b2449493db4bcf01e636fb826ed14c60f089859a42fc004
7
+ data.tar.gz: 848728c5dd81a7bb8b3b68e4d99249f8070ba26aecf293322f7d72fcdb7b8e1667cc923fb6caea0b43b2bff4a1ec61a32b0c2ddf9af9c482cfb514894f476e8e
data/.codeclimate.yml CHANGED
@@ -1,17 +1,23 @@
1
- ---
2
- exclude_paths:
3
- - ".git/"
4
- - "**.xml"
5
- - "**.yaml"
6
- - "**.yml"
7
- - lib/metadata/linux/test/Packages
8
- - lib/metadata/linux/test/tc_LinuxUtils.rb
9
- - locale/
10
- - spec/
11
- - test/
12
- - tools/
13
- - tmp/
14
- engines:
1
+ checks:
2
+ method-complexity:
3
+ enabled: true
4
+ config:
5
+ threshold: 8
6
+ prepare:
7
+ fetch:
8
+ - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/.rubocop_base.yml
9
+ path: ".rubocop_base.yml"
10
+ - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/.rubocop_cc_base.yml
11
+ path: ".rubocop_cc_base.yml"
12
+ - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/styles/base.yml
13
+ path: styles/base.yml
14
+ - url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/styles/cc_base.yml
15
+ path: styles/cc_base.yml
16
+ plugins:
17
+ rubocop:
18
+ enabled: true
19
+ config: ".rubocop_cc.yml"
20
+ channel: rubocop-0-82
15
21
  brakeman:
16
22
  enabled: false
17
23
  bundler-audit:
@@ -25,7 +31,7 @@ engines:
25
31
  ruby:
26
32
  mass_threshold: 25
27
33
  count_threshold: 5
28
- javascript:
34
+ javascript:
29
35
  eslint:
30
36
  enabled: false
31
37
  channel: eslint-3
@@ -33,23 +39,16 @@ engines:
33
39
  enabled: false
34
40
  markdownlint:
35
41
  enabled: false
36
- rubocop:
37
- enabled: true
38
- config: ".rubocop_cc.yml"
39
- channel: rubocop-0-69
40
- checks:
41
- method-complexity:
42
- enabled: true
43
- config:
44
- threshold: 8
45
- prepare:
46
- fetch:
47
- - url: https://raw.githubusercontent.com/ManageIQ/guides/master/.rubocop_base.yml
48
- path: ".rubocop_base.yml"
49
- - url: https://raw.githubusercontent.com/ManageIQ/guides/master/.rubocop_cc_base.yml
50
- path: ".rubocop_cc_base.yml"
51
- ratings:
52
- paths:
53
- - Gemfile.lock
54
- - "**.rake"
55
- - "**.rb"
42
+ exclude_patterns:
43
+ - ".git/"
44
+ - "**.xml"
45
+ - "**.yaml"
46
+ - "**.yml"
47
+ - lib/metadata/linux/test/Packages
48
+ - lib/metadata/linux/test/tc_LinuxUtils.rb
49
+ - locale/
50
+ - spec/
51
+ - test/
52
+ - tools/
53
+ - tmp/
54
+ version: '2'
data/.rubocop.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  inherit_from:
2
- - https://raw.githubusercontent.com/ManageIQ/guides/master/.rubocop_base.yml
3
- # put all local rubocop config into .rubocop_local.yml as it will be loaded by .rubocop_cc.yml as well
4
- - .rubocop_local.yml
2
+ - ".rubocop_local.yml"
3
+ inherit_gem:
4
+ manageiq-style: ".rubocop_base.yml"
data/.rubocop_cc.yml CHANGED
@@ -1,5 +1,4 @@
1
1
  inherit_from:
2
- # this is downloaded by .codeclimate.yml
3
- - .rubocop_base.yml
4
- - .rubocop_cc_base.yml
5
- - .rubocop_local.yml
2
+ - ".rubocop_base.yml"
3
+ - ".rubocop_cc_base.yml"
4
+ - ".rubocop_local.yml"
data/.rubocop_local.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  #
2
2
  # Overrides
3
3
  #
4
- GlobalVars:
4
+ Style/GlobalVars:
5
5
  AllowedVariables:
6
6
  # Loggers
7
7
  - $log
data/.travis.yml CHANGED
@@ -1,10 +1,9 @@
1
1
  ---
2
- sudo: false
3
2
  cache: bundler
4
3
  language: ruby
5
4
  rvm:
6
- - 2.5.7
7
- - 2.6.5
5
+ - 2.6.6
6
+ - 2.7.2
8
7
  after_script: bundle exec codeclimate-test-reporter
9
8
  notifications:
10
9
  webhooks:
@@ -42,7 +42,7 @@ class MiqRhevmVm < MiqVm
42
42
  disks = @rhevm.collect_vm_disks(@rhevmVm)
43
43
  disks.each_with_index do |disk, idx|
44
44
  $log.debug "MiqRhevmVm#getCfg: disk = #{disk.inspect}"
45
- storage_domain = disk.storage_domains.first
45
+ storage_domain = disk.storage_domains&.first
46
46
  if storage_domain.nil?
47
47
  $log.info("Disk <#{disk.name}> is skipped due to unassigned storage domain")
48
48
  next
@@ -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,10 +1,12 @@
1
1
  require 'disk/modules/MiqLargeFile'
2
- require 'util/miq-unicode'
2
+ require 'miq_unicode'
3
3
  require 'binary_struct'
4
4
  require 'memory_buffer'
5
5
  require 'Scvmm/miq_hyperv_disk'
6
6
 
7
7
  module MSCommon
8
+ using ManageIQ::UnicodeString
9
+
8
10
  # NOTE: All values are stored in network byte order.
9
11
 
10
12
  FOOTER = BinaryStruct.new([
@@ -1,6 +1,6 @@
1
1
  # encoding: US-ASCII
2
2
 
3
- require 'util/miq-unicode'
3
+ require 'miq_unicode'
4
4
  require 'binary_struct'
5
5
  require 'disk/MiqDisk'
6
6
  require 'memory_buffer'
@@ -8,6 +8,8 @@ require 'disk/modules/MiqLargeFile'
8
8
  require 'disk/modules/vhdx_bat_entry'
9
9
 
10
10
  module VhdxDisk
11
+ using ManageIQ::UnicodeString
12
+
11
13
  # NOTE: All values are stored in network byte order.
12
14
 
13
15
  VHDX_FILE_IDENTIFIER = BinaryStruct.new([
@@ -8,6 +8,7 @@ class MiqMountManager < MiqFS
8
8
  def self.mountVolumes(volMgr, vmCfg, ost = nil)
9
9
  rootTrees = []
10
10
  noFsVolumes = []
11
+
11
12
  volMgr.visibleVolumes.each do |dobj|
12
13
  $log.debug("MiqMountManager.mountVolumes >> fileName=#{dobj.dInfo.fileName}, partition=#{dobj.partNum}") if $log
13
14
  fs = MiqFS.getFS(dobj)
@@ -20,10 +21,7 @@ class MiqMountManager < MiqFS
20
21
  rootTrees << new(rsm, dobj, volMgr, vmCfg, ost)
21
22
  end
22
23
  end
23
- if volMgr.kind_of?(MiqNativeVolumeManager)
24
- require 'MetakitFS'
25
- rootTrees.each { |rt| rt.findPayload(noFsVolumes) } unless noFsVolumes.empty?
26
- end
24
+
27
25
  rootTrees
28
26
  end # def self.mountVolumes
29
27
 
@@ -41,31 +39,6 @@ class MiqMountManager < MiqFS
41
39
  super(rootModule, rootVolume)
42
40
  end # def initialize
43
41
 
44
- def findPayload(noFsVolumes)
45
- $log.debug "MiqMountManager.findPayload: searching for payloads:" if $log
46
- noFsVolumes.each do |v|
47
- next unless v.respond_to?(:devFile)
48
- if v.devFile
49
- $log.debug "\tMiqMountManager.findPayload: devFile = #{v.devFile}" if $log
50
- v.mkfile = v.devFile
51
- unless MetakitFS.supported?(v)
52
- $log.debug "\tMiqMountManager.findPayload: devFile = #{v.devFile} not mkfs, skipping" if $log
53
- v.mkfile = nil
54
- next
55
- end
56
- mkFs = MiqFS.new(MetakitFS, v)
57
- if mkFs.fsId == "MIQPAYLOAD"
58
- $log.debug "\tMiqMountManager.findPayload: payload found devFile = #{v.devFile}" if $log
59
- @payloads << mkFs
60
- else
61
- $log.debug "\tMiqMountManager.findPayload: devFile = #{v.devFile} not payload, fsId = #{mkFs.fsId}" if $log
62
- end
63
- else
64
- $log.debug "\tMiqMountManager.findPayload: devFile not set, fileName = #{v.dInfo.fileName}" if $log
65
- end
66
- end
67
- end
68
-
69
42
  #
70
43
  # Override standard MiqFS methods to account for mount indirection.
71
44
  # The getFsPath method is defined by the OS-specific mount modules.
@@ -105,12 +105,7 @@ module VimDatastoreFS
105
105
  #
106
106
  # fpos = 0
107
107
  # fsize = @pSize.get(fileRow)
108
- # if ftruncate && fsize != 0
109
- # @pSize.set fileRow, 0
110
- # @pData.set fileRow, Metakit::Bytes.new("", 0)
111
- # elsif fappend
112
- # fpos = fsize
113
- # end
108
+ # fpos = fsize if fappend
114
109
  #
115
110
  # return(MkFile.new(p, fileRow, fpos, fread, fwrite))
116
111
  # end # def fs_fileOpen
@@ -1,540 +1,540 @@
1
- # encoding: US-ASCII
2
-
3
- require 'stringio'
4
-
5
- require 'binary_struct'
6
- require 'util/miq-unicode'
7
-
8
- # ////////////////////////////////////////////////////////////////////////////
9
- # // Data definitions.
10
-
11
- # TODO: reserved1 is the infamous magic number. Somehow it works to preserve
12
- # case on Windows XP. Nobody seems to know how. Here it is always set to 0
13
- # (which yields uppercase names on XP).
14
-
15
- module Fat32
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