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.
- checksums.yaml +4 -4
- data/.codeclimate.yml +34 -35
- data/.rubocop.yml +3 -3
- data/.rubocop_cc.yml +3 -4
- data/.rubocop_local.yml +1 -1
- data/.travis.yml +2 -3
- data/lib/MiqVm/MiqRhevmVm.rb +1 -1
- data/lib/db/MiqBdb/MiqBdb.rb +0 -2
- data/lib/db/MiqBdb/MiqBdbPage.rb +0 -2
- data/lib/db/MiqSqlite/MiqSqlite3.rb +0 -2
- data/lib/db/MiqSqlite/MiqSqlite3Cell.rb +0 -2
- data/lib/db/MiqSqlite/MiqSqlite3Page.rb +0 -2
- data/lib/db/MiqSqlite/MiqSqlite3Table.rb +0 -2
- data/lib/disk/modules/MSCommon.rb +3 -1
- data/lib/disk/modules/VhdxDisk.rb +3 -1
- data/lib/fs/MiqMountManager.rb +2 -29
- data/lib/fs/VimDatastoreFS/VimDatastoreFS.rb +1 -6
- data/lib/fs/fat32/directory_entry.rb +540 -540
- data/lib/fs/iso9660/boot_sector.rb +3 -2
- data/lib/fs/iso9660/directory_entry.rb +3 -2
- data/lib/fs/iso9660/rock_ridge.rb +3 -1
- data/lib/fs/ntfs/attrib_attribute_list.rb +3 -1
- data/lib/fs/ntfs/attrib_file_name.rb +3 -1
- data/lib/fs/ntfs/attrib_header.rb +3 -1
- data/lib/fs/ntfs/attrib_index_root.rb +3 -1
- data/lib/fs/ntfs/attrib_volume_name.rb +3 -1
- data/lib/manageiq/smartstate/util.rb +18 -0
- data/lib/manageiq/smartstate/version.rb +1 -1
- data/lib/metadata/MIQExtract/MIQExtract.rb +2 -3
- data/lib/metadata/VmConfig/GetNativeCfg.rb +2 -4
- data/lib/metadata/VmConfig/VmConfig.rb +7 -6
- data/lib/metadata/VmConfig/cfgConfig.rb +4 -0
- data/lib/metadata/VmConfig/xmlConfig.rb +3 -3
- data/lib/metadata/linux/MiqRpmPackages.rb +3 -1
- data/lib/metadata/util/win32/Win32Accounts.rb +3 -1
- data/lib/metadata/util/win32/Win32EventLog.rb +3 -1
- data/lib/metadata/util/win32/Win32Software.rb +5 -3
- data/lib/metadata/util/win32/decode.rb +0 -0
- data/lib/metadata/util/win32/fleece_hives.rb +0 -9
- data/lib/metadata/util/win32/ms-registry.rb +3 -2
- data/lib/metadata/util/win32/peheader.rb +3 -2
- data/lib/metadata/util/win32/system_path_win.rb +0 -2
- data/lib/miq_unicode.rb +45 -0
- data/manageiq-smartstate.gemspec +8 -6
- metadata +66 -23
- data/lib/fs/MetakitFS/MetakitFS.rb +0 -530
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3547ea48f2af9d840bfceb8c0c3ae20b6e57bb7b1494e705a940e0998f28d9a6
|
4
|
+
data.tar.gz: dbbdc0761323f813a2224f5fc0a11954b60cc49a6d4bca30d39f16d3d4eed67a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e2c6075e386e2c396e39aba0d8d5d23f037ee94f262d625bf3b51276e9b6615ef2ea6dac248644bf9b2449493db4bcf01e636fb826ed14c60f089859a42fc004
|
7
|
+
data.tar.gz: 848728c5dd81a7bb8b3b68e4d99249f8070ba26aecf293322f7d72fcdb7b8e1667cc923fb6caea0b43b2bff4a1ec61a32b0c2ddf9af9c482cfb514894f476e8e
|
data/.codeclimate.yml
CHANGED
@@ -1,17 +1,23 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
-
|
9
|
-
|
10
|
-
-
|
11
|
-
|
12
|
-
-
|
13
|
-
|
14
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
-
|
3
|
-
|
4
|
-
- .
|
2
|
+
- ".rubocop_local.yml"
|
3
|
+
inherit_gem:
|
4
|
+
manageiq-style: ".rubocop_base.yml"
|
data/.rubocop_cc.yml
CHANGED
data/.rubocop_local.yml
CHANGED
data/.travis.yml
CHANGED
data/lib/MiqVm/MiqRhevmVm.rb
CHANGED
@@ -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
|
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
|
data/lib/db/MiqBdb/MiqBdb.rb
CHANGED
data/lib/db/MiqBdb/MiqBdbPage.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
require 'disk/modules/MiqLargeFile'
|
2
|
-
require '
|
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 '
|
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([
|
data/lib/fs/MiqMountManager.rb
CHANGED
@@ -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
|
-
|
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
|
-
#
|
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 '
|
6
|
-
|
7
|
-
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
#
|
12
|
-
#
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|