manageiq-smartstate 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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