unxls 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ # [MS-OSHARED]: Office Common Data Types and Objects Structures
4
+ module Unxls::Oshared
5
+ using Unxls::Helpers
6
+
7
+ extend self
8
+
9
+ # 2.3.7.4 AntiMoniker
10
+ # This structure specifies an anti-moniker. An anti-moniker acts as the inverse of any moniker it is composed onto, effectively canceling out that moniker. In a composite moniker, anti-monikers are used to cancel out existing moniker elements, because monikers cannot be removed from a composite moniker. For more information about anti-monikers, see [MSDN-IMAMI].
11
+ # @param io [StringIO]
12
+ # @return [Symbol]
13
+ def antimoniker(io)
14
+ { count: io.read(4).unpack('V').first } # count (4 bytes): An unsigned integer that specifies the number of anti-monikers that have been composed together to create this instance. When an anti-moniker is composed with another anti-moniker, the resulting composition would have a count field equaling the sum of the two count fields of the composed anti-monikers. This value MUST be less than or equal to 1048576.
15
+ end
16
+
17
+ # 2.3.7.3 CompositeMoniker
18
+ # This structure specifies a composite moniker. A composite moniker is a collection of arbitrary monikers. For more information about composite monikers see [MSDN-IMGCMI].
19
+ # @param io [StringIO]
20
+ # @return [Symbol]
21
+ def compositemoniker(io)
22
+ c_monikers = io.read(4).unpack('V').first
23
+
24
+ {
25
+ cMonikers: c_monikers, # cMonikers (4 bytes): An unsigned integer that specifies the count of monikers in monikerArray.
26
+ monikerArray: c_monikers.times.map { hyperlinkmoniker(io) } # monikerArray (variable): An array of HyperlinkMonikers (section 2.3.7.2). Each array element specifies a moniker of arbitrary type.
27
+ }
28
+ end
29
+
30
+ # Double-byte unterminated string used in HyperlinkMoniker
31
+ # @param io [StringIO]
32
+ # @param length [Integer]
33
+ # @return [String]
34
+ def _db_unterminated(io, length)
35
+ Unxls::Biff8::Structure._encode_string(io.read(length))
36
+ end
37
+
38
+ # Double-byte 0x0000-terminated string used in HyperlinkMoniker
39
+ # @param io [StringIO]
40
+ # @return [String]
41
+ def _db_zero_terminated(io)
42
+ string = String.new
43
+ while (char = io.read(2)) != "\x00\x00".encode(Encoding::ASCII_8BIT)
44
+ string << char
45
+ end
46
+ Unxls::Biff8::Structure._encode_string(string)
47
+ end
48
+
49
+ # 2.3.7.8 FileMoniker
50
+ # This structure specifies a file moniker. For more information about file monikers, see [MSDN-FM].
51
+ # @param io [StringIO]
52
+ # @return [Hash]
53
+ def filemoniker(io)
54
+ c_anti, ansi_length = io.read(6).unpack('vV')
55
+ ansi_path = io.read(ansi_length).unpack('Z*').first.encode(Encoding::ASCII, { invalid: :replace, undef: :replace }) # 0-terminated
56
+
57
+ result = {
58
+ cAnti: c_anti, # cAnti (2 bytes): An unsigned integer that specifies the number of parent directory indicators at the beginning of the ansiPath field.
59
+ ansiLength: ansi_length, # ansiLength (4 bytes): An unsigned integer that specifies the number of ANSI characters in ansiPath, including the terminating NULL character. This value MUST be less than or equal to 32767.
60
+ ansiPath: ansi_path, # ansiPath (variable): A null-terminated array of ANSI characters that specifies the file path. The number of characters in the array is specified by ansiLength.
61
+ }
62
+
63
+ end_server = io.read(2).unpack('v').first
64
+ path_is_unc = end_server != 0xFFFF
65
+ result[:endServer] = end_server if path_is_unc # endServer (2 bytes): An unsigned integer that specifies the number of Unicode characters used to specify the server portion of the path if the path is a UNC path (including the leading "\\"). If the path is not a UNC path, this field MUST equal 0xFFFF.
66
+
67
+ result[:versionNumber] = io.read(2).unpack('v').first # versionNumber (2 bytes): An unsigned integer that specifies the version number of this file moniker serialization implementation. MUST equal 0xDEAD.
68
+
69
+ io.read(20) # reserved1 (16 bytes): MUST be zero and MUST be ignored; reserved2 (4 bytes): MUST be zero and MUST be ignored.
70
+
71
+ result[:cbUnicodePathSize] = io.read(4).unpack('V').first # cbUnicodePathSize (4 bytes): An unsigned integer that specifies the size, in bytes, of cbUnicodePathBytes, usKeyValue, and unicodePath.
72
+ return result if result[:cbUnicodePathSize] == 0 # ansiPath can be fully specified in ANSI characters
73
+
74
+ cb_unicode_path_bytes, _ = io.read(6).unpack('Vv')
75
+ result[:cbUnicodePathBytes] = cb_unicode_path_bytes # cbUnicodePathBytes (4 bytes): An optional unsigned integer that specifies the size, in bytes, of the unicodePath field. This field exists if and only if cbUnicodePathSize is greater than zero.
76
+ # (skipped) usKeyValue (2 bytes): An optional unsigned integer that MUST be 3 if present. This field exists if and only if cbUnicodePathSize is greater than zero.
77
+ result[:unicodePath] = _db_unterminated(io, cb_unicode_path_bytes) # unicodePath (variable): An optional array of Unicode characters that specifies the complete file path. This path MUST be the complete Unicode version of the file path specified in ansiPath and MUST include additional Unicode characters that cannot be completely specified in ANSI characters. The number of characters in this array is specified by cbUnicodePathBytes/2. This array MUST NOT include a terminating NULL character. This field exists if and only if cbUnicodePathSize is greater than zero.
78
+
79
+ result
80
+ end
81
+
82
+ # 2.3.7.1 Hyperlink Object
83
+ # This structure specifies a hyperlink and hyperlink-related information.
84
+ # @param io [StringIO]
85
+ # @return [Hash]
86
+ def hyperlink(io)
87
+ result = { streamVersion: io.read(4).unpack('V').first } # streamVersion (4 bytes): An unsigned integer that specifies the version number of the serialization implementation used to save this structure. This value MUST equal 2.
88
+
89
+ attrs = Unxls::BitOps.new(io.read(4).unpack('V').first)
90
+ result[:hlstmfHasMoniker] = attrs.set_at?(0) # A - hlstmfHasMoniker (1 bit): A bit that specifies whether this structure contains a moniker. If hlstmfMonikerSavedAsStr equals 1, this value MUST equal 1.
91
+ result[:hlstmfIsAbsolute] = attrs.set_at?(1) # B - hlstmfIsAbsolute (1 bit): A bit that specifies whether this hyperlink is an absolute path or relative path.
92
+ result[:hlstmfSiteGaveDisplayName] = attrs.set_at?(2) # C - hlstmfSiteGaveDisplayName (1 bit): A bit that specifies whether the creator of the hyperlink specified a display name.
93
+ result[:hlstmfHasLocationStr] = attrs.set_at?(3) # D - hlstmfHasLocationStr (1 bit): A bit that specifies whether this structure contains a hyperlink location.
94
+ result[:hlstmfHasDisplayName] = attrs.set_at?(4) # E - hlstmfHasDisplayName (1 bit): A bit that specifies whether this structure contains a display name.
95
+ result[:hlstmfHasGUID] = attrs.set_at?(5) # F - hlstmfHasGUID (1 bit): A bit that specifies whether this structure contains a GUID as specified by [MS-DTYP].
96
+ result[:hlstmfHasCreationTime] = attrs.set_at?(6) # G - hlstmfHasCreationTime (1 bit): A bit that specifies whether this structure contains the creation time of the file that contains the hyperlink.
97
+ result[:hlstmfHasFrameName] = attrs.set_at?(7) # H - hlstmfHasFrameName (1 bit): A bit that specifies whether this structure contains a target frame name.
98
+ result[:hlstmfMonikerSavedAsStr] = attrs.set_at?(8) # I - hlstmfMonikerSavedAsStr (1 bit): A bit that specifies whether the moniker was saved as a string.
99
+ result[:hlstmfAbsFromGetdataRel] = attrs.set_at?(9) # J - hlstmfAbsFromGetdataRel (1 bit): A bit that specifies whether the hyperlink specified by this structure is an absolute path generated from a relative path.
100
+ # reserved (22 bits): MUST be zero and MUST be ignored.
101
+
102
+ result[:displayName] = hyperlinkstring(io) if result[:hlstmfHasDisplayName] # displayName (variable): An optional HyperlinkString (section 2.3.7.9) that specifies the display name for the hyperlink. MUST exist if and only if hlstmfHasDisplayName equals 1.
103
+ result[:targetFrameName] = hyperlinkstring(io) if result[:hlstmfHasFrameName] # targetFrameName (variable): An optional HyperlinkString (section 2.3.7.9) that specifies the target frame. MUST exist if and only if hlstmfHasFrameName equals 1.
104
+ result[:moniker] = hyperlinkstring(io) if result[:hlstmfHasMoniker] && result[:hlstmfMonikerSavedAsStr] # moniker (variable): An optional HyperlinkString (section 2.3.7.9) that specifies the hyperlink moniker. MUST exist if and only if hlstmfHasMoniker equals 1 and hlstmfMonikerSavedAsStr equals 1.
105
+
106
+ if result[:hlstmfHasMoniker] && !result[:hlstmfMonikerSavedAsStr]
107
+ result[:oleMoniker] = hyperlinkmoniker(io) # oleMoniker (variable): An optional HyperlinkMoniker (section 2.3.7.2) that specifies the hyperlink moniker. MUST exist if and only if hlstmfHasMoniker equals 1 and hlstmfMonikerSavedAsStr equals 0.
108
+ end
109
+
110
+ result[:location] = hyperlinkstring(io) if result[:hlstmfHasLocationStr] # location (variable): An optional HyperlinkString (section 2.3.7.9) that specifies the hyperlink location. MUST exist if and only if hlstmfHasLocationStr equals 1.
111
+ result[:guid] = io.read(16) if result[:hlstmfHasGUID] # guid (16 bytes): An optional GUID (see 2.3.4 GUID and UUID) as specified by [MS-DTYP] that identifies this hyperlink. MUST exist if and only if hlstmfHasGUID equals 1.
112
+ result[:fileTime] = Unxls::Dtyp.filetime(io.read(8)) if result[:hlstmfHasCreationTime] # fileTime (8 bytes): An optional FileTime structure as specified by [MS-DTYP] that specifies the UTC file creation time. MUST exist if and only if hlstmfHasCreationTime equals 1.
113
+
114
+ result
115
+ end
116
+
117
+ # 2.3.7.2 HyperlinkMoniker
118
+ # This structure specifies a hyperlink moniker.
119
+ # @param io [StringIO]
120
+ # @return [Hash]
121
+ def hyperlinkmoniker(io)
122
+ id = io.read(4).unpack('V').first
123
+ io.pos -= 4
124
+
125
+ moniker_type = {
126
+ 0x79eac9e0 => :URLMoniker,
127
+ 0x00000303 => :FileMoniker,
128
+ 0x00000309 => :CompositeMoniker,
129
+ 0x00000305 => :AntiMoniker,
130
+ 0x00000304 => :ItemMoniker,
131
+ }[id]
132
+
133
+ result = {
134
+ monikerClsid: Unxls::Dtyp.guid(io.read(16)), # monikerClsid (16 bytes): A class identifier (CLSID) that specifies the Component Object Model (COM) component that saved this structure.
135
+ monikerClsid_d: moniker_type,
136
+ }
137
+
138
+ processor_method = moniker_type.downcase
139
+ result[:data] = self.send(processor_method, io) if self.respond_to?(processor_method) # data (variable): A moniker of the type specified by monikerClsid.
140
+
141
+ result
142
+ end
143
+
144
+ # 2.3.7.9 HyperlinkString
145
+ # This structure specifies a string for a hyperlink.
146
+ # @param io [StringIO]
147
+ # @return [String]
148
+ def hyperlinkstring(io)
149
+ io.read(4) # length (4 bytes): An unsigned integer that specifies the number of Unicode characters in the string field, including the null-terminating character.
150
+ _db_zero_terminated(io) # string (variable): A null-terminated array of Unicode characters. The number of characters in the array is specified by the length field.
151
+ end
152
+
153
+ # 2.3.7.5 ItemMoniker
154
+ # This structure specifies an item moniker. Item monikers are used to identify objects within containers, such as a portion of a document, an embedded object within a compound document, or a range of cells within a spreadsheet. For more information about item monikers, see [MSDN-IMCOM].
155
+ # @param io [StringIO]
156
+ # @return [Symbol]
157
+ def itemmoniker(io)
158
+ :not_implemented
159
+ end
160
+
161
+ # 2.3.7.7 URICreateFlags
162
+ # This structure specifies creation flags for an [RFC3986] compliant URI. For more information about URI creation flags, see [MSDN-CreateUri].
163
+ # @param data [String]
164
+ # @return [Hash]
165
+ def uricreateflags(data)
166
+ attrs = Unxls::BitOps.new(data.unpack('V').first)
167
+
168
+ {
169
+ createAllowRelative: attrs.set_at?(0), # A - createAllowRelative (1 bit): A bit that specifies that if the URI scheme is unspecified and not implicitly "file," a relative scheme is assumed during creation of the URI.
170
+ createAllowImplicitWildcardScheme: attrs.set_at?(1), # B - createAllowImplicitWildcardScheme (1 bit): A bit that specifies that if the URI scheme is unspecified and not implicitly "file," a wildcard scheme is assumed during creation of the URI.
171
+ createAllowImplicitFileScheme: attrs.set_at?(2), # C - createAllowImplicitFileScheme (1 bit): A bit that specifies that if the URI scheme is unspecified and the URI begins with a drive letter or a UNC path, a file scheme is assumed during creation of the URI.
172
+ createNoFrag: attrs.set_at?(3), # D - createNoFrag (1 bit): A bit that specifies that if a URI query string is present, the URI fragment is not looked for during creation of the URI.
173
+ createNoCanonicalize: attrs.set_at?(4), # E - createNoCanonicalize (1 bit): A bit that specifies that the scheme, host, authority, path, and fragment will not be canonicalized during creation of the URI. This value MUST be 0 if createCanonicalize equals 1.
174
+ createCanonicalize: attrs.set_at?(5), # F - createCanonicalize (1 bit): A bit that specifies that the scheme, host, authority, path, and fragment will be canonicalized during creation of the URI. This value MUST be 0 if createNoCanonicalize equals 1.
175
+ createFileUseDosPath: attrs.set_at?(6), # G - createFileUseDosPath (1 bit): A bit that specifies that MS-DOS path compatibility mode will be used during creation of file URIs.
176
+ createDecodeExtraInfo: attrs.set_at?(7), # H - createDecodeExtraInfo (1 bit): A bit that specifies that percent encoding and percent decoding canonicalizations will be performed on the URI query and URI fragment during creation of the URI. This field takes precedence over the createNoCanonicalize field.
177
+ createNoDecodeExtraInfo: attrs.set_at?(8), # I - createNoDecodeExtraInfo (1 bit): A bit that specifies that percent encoding and percent decoding canonicalizations will not be performed on the URI query and URI fragment during creation of the URI. This field takes precedence over the createCanonicalize field.
178
+ createCrackUnknownSchemes: attrs.set_at?(9), # J - createCrackUnknownSchemes (1 bit): A bit that specifies that hierarchical URIs with unrecognized URI schemes will be treated like hierarchical URIs during creation of the URI. This value MUST be 0 if createNoCrackUnknownSchemes equals 1.
179
+ createNoCrackUnknownSchemes: attrs.set_at?(10), # K - createNoCrackUnknownSchemes (1 bit): A bit that specifies that hierarchical URIs with unrecognized URI schemes will be treated like opaque URIs during creation of the URI. This value MUST be 0 if createCrackUnknownSchemes equals 1.
180
+ createPreProcessHtmlUri: attrs.set_at?(11), # L - createPreProcessHtmlUri (1 bit): A bit that specifies that preprocessing will be performed on the URI to remove control characters and white space during creation of the URI. This value MUST be 0 if createNoPreProcessHtmlUri equals 1.
181
+ createNoPreProcessHtmlUri: attrs.set_at?(12), # M - createNoPreProcessHtmlUri (1 bit): A bit that specifies that preprocessing will not be performed on the URI to remove control characters and white space during creation of the URI. This value MUST be 0 if createPreProcessHtmlUri equals 1.
182
+ createIESettings: attrs.set_at?(13), # N - createIESettings (1 bit): A bit that specifies that registry settings will be used to determine default URL parsing behavior during creation of the URI. This value MUST be 0 if createNoIESettings equals 1.
183
+ createNoIESettings: attrs.set_at?(14), # O - createNoIESettings (1 bit): A bit that specifies that registry settings will not be used to determine default URL parsing behavior during creation of the URI. This value MUST be 0 if createIESettings equals 1.
184
+ createNoEncodeForbiddenCharacters: attrs.set_at?(15), # P - createNoEncodeForbiddenCharacters (1 bit): A bit that specifies that URI characters forbidden in [RFC3986] will not be percent-encoded during creation of the URI.
185
+ # reserved (16 bits): MUST be zero and MUST be ignored.
186
+ }
187
+ end
188
+
189
+ # 2.3.7.6 URLMoniker
190
+ # This structure specifies a URL moniker. For more information about URL monikers, see [MSDN-URLM].
191
+ # @param io [StringIO]
192
+ # @return [Hash]
193
+ def urlmoniker(io)
194
+ length = io.read(4).unpack('V').first
195
+ moniker_data_io = io.read(length).to_sio
196
+
197
+ result = {
198
+ length: length, # length (4 bytes): An unsigned integer that specifies the size of this structure in bytes, excluding the size of the length field.
199
+ url: _db_zero_terminated(moniker_data_io) # url (variable): A null-terminated array of Unicode characters that specifies the URL. The number of characters in the array is determined by the position of the terminating NULL character.
200
+ }
201
+ return result if moniker_data_io.eof?
202
+
203
+ result[:serialGUID] = Unxls::Dtyp.guid(moniker_data_io.read(16)) # serialGUID (16 bytes): An optional GUID as specified by [MS-DTYP] for this implementation of the URL moniker serialization. This field MUST equal {0xF4815879, 0x1D3B, 0x487F, 0xAF, 0x2C, 0x82, 0x5D, 0xC4, 0x85, 0x27, 0x63} if present.
204
+ result[:serialVersion] = moniker_data_io.read(4).unpack('V').first # serialVersion (4 bytes): An optional unsigned integer that specifies the version number of this implementation of the URL moniker serialization. This field MUST equal 0 if present.
205
+ result[:uriFlags] = uricreateflags(moniker_data_io.read(4)) # uriFlags (4 bytes): An optional URICreateFlags structure (section 2.3.7.7) that specifies creation flags for an [RFC3986] compliant URI.
206
+
207
+ result
208
+ end
209
+
210
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Unxls::Parser
4
+
5
+ attr_reader :file
6
+ attr_accessor :settings
7
+
8
+ # @param file [File]
9
+ # @param settings [Hash]
10
+ def initialize(file, settings)
11
+ @file = file
12
+ @settings = settings
13
+ end
14
+
15
+ # @return [Hash]
16
+ def parse
17
+ biff_version = detect_biff_version
18
+ result = {}
19
+
20
+ case biff_version
21
+ when :BIFF8
22
+ result[:workbook_stream] = Unxls::Biff8::WorkbookStream.new(self).parse
23
+ # parse other storages/streams
24
+ else
25
+ raise "Sorry, #{biff_version} is not supported yet"
26
+ end
27
+
28
+ result
29
+ end
30
+
31
+ private
32
+
33
+ # See "2.3 File Structure" of OpenOffice's doc, p.14
34
+ # See "5.8 BOF – Beginning of File" of OpenOffice's doc, p.135
35
+ # Test files: https://www.openoffice.org/sc/testdocs/
36
+ # @return [Symbol]
37
+ def detect_biff_version
38
+ stream = begin
39
+ compound = Ole::Storage.open(@file)
40
+ Unxls::Log.debug(compound.dir.entries('.'), 'OLE compound file entries:', :cyan) # @debug
41
+
42
+ if compound.file.exists?('Workbook') # BIFF8
43
+ compound.file.open('Workbook')
44
+ elsif compound.file.exists?('Book') # BIFF5
45
+ compound.file.open('Book')
46
+ else
47
+ raise 'Error opening workbook stream'
48
+ end
49
+ rescue Ole::Storage::FormatError
50
+ @file # Worksheet stream, BIFF2-4
51
+ end
52
+
53
+ id, _, vers, dt = stream.tap(&:rewind).read(8).unpack('v4')
54
+
55
+ id_d = {
56
+ 0x0009 => :BIFF2,
57
+ 0x0209 => :BIFF3,
58
+ 0x0409 => :BIFF4,
59
+ 0x0809 => :BIFF5_8
60
+ }[id]
61
+
62
+ vers_d = {
63
+ 0x0000 => :BIFF5, # see 5.8.2 of OpenOffice's doc, p.136
64
+ 0x0200 => :BIFF2,
65
+ 0x0300 => :BIFF3,
66
+ 0x0400 => :BIFF4,
67
+ 0x0500 => :BIFF5,
68
+ 0x0600 => :BIFF8,
69
+ }[vers]
70
+
71
+ dt_d = {
72
+ 0x0005 => :globals, # Workbook globals substream
73
+ 0x0006 => :vb_module, # Visual Basic module substream
74
+ 0x0010 => :dialog_or_work_sheet, # If fDialog flag in the WsBool record in the substream is 1, it's a dialog sheet substream
75
+ 0x0020 => :chart, # Cart sheet substream
76
+ 0x0040 => :macro, # Macro sheet substream
77
+ 0x0100 => :workspace # Workspace substream
78
+ }[dt]
79
+
80
+ version_params = {
81
+ stream_class: stream.class.to_s,
82
+ id: Unxls::Log.h2b(id),
83
+ id_d: id_d,
84
+ vers: Unxls::Log.h2b(vers),
85
+ vers_d: vers_d,
86
+ dt: Unxls::Log.h2b(dt),
87
+ dt_d: dt_d,
88
+ }
89
+ Unxls::Log.debug(version_params, 'Detecting BIFF version:', :cyan) # @debug
90
+
91
+ if stream.is_a?(File) && %i(BIFF2 BIFF3 BIFF4).include?(id_d)
92
+ id_d
93
+ elsif id_d == :BIFF5_8 && vers_d && dt_d == :globals
94
+ vers_d
95
+ else
96
+ raise 'Cannot detect BIFF version'
97
+ end
98
+ end
99
+
100
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unxls
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Unxls::BitOps do
4
+ it '#set_at? detects bit flags' do
5
+ expect(
6
+ Unxls::BitOps.new(0b0001).set_at?(0)
7
+ ).to eq true
8
+
9
+ expect(
10
+ Unxls::BitOps.new(0b0010).set_at?(1)
11
+ ).to eq true
12
+
13
+ expect(
14
+ Unxls::BitOps.new(0b0100).set_at?(1)
15
+ ).to eq false
16
+
17
+ expect(
18
+ Unxls::BitOps.new(0b10000000_00000000_00000000_00000000).set_at?(31)
19
+ ).to eq true
20
+
21
+ expect(
22
+ Unxls::BitOps.new(0b01000000_00000000_00000000_00000000).set_at?(31)
23
+ ).to eq false
24
+
25
+ expect(
26
+ Unxls::BitOps.new(0b10000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000).set_at?(63)
27
+ ).to eq true
28
+
29
+ expect(
30
+ Unxls::BitOps.new(0b01000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000).set_at?(63)
31
+ ).to eq false
32
+
33
+ expect(
34
+ Unxls::BitOps.new(0b1111).set_at?(-1)
35
+ ).to eq nil
36
+ end
37
+
38
+ it '#value_at extracts values that are not byte-multiple' do
39
+ {
40
+ 0b1100 => 2..3,
41
+ 0b11000000 => 6..7,
42
+ 0b11000000_00000000 => 14..15,
43
+ 0b11000000_00000000_00000000_00000000 => 30..31,
44
+ 0b11000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000 => 62..63
45
+ }.each do |bits, range|
46
+ expect(
47
+ Unxls::BitOps.new(bits).value_at(range)
48
+ ).to eq 3
49
+ end
50
+
51
+ expect(
52
+ Unxls::BitOps.new(0b0110).value_at(2..2)
53
+ ).to eq 1
54
+
55
+ expect(
56
+ Unxls::BitOps.new(0b0110).value_at(0..1)
57
+ ).to eq 2
58
+
59
+ expect(
60
+ Unxls::BitOps.new(0b0110).value_at(-1..0)
61
+ ).to eq nil
62
+
63
+ expect(
64
+ Unxls::BitOps.new(0b0110).value_at(-2..-1)
65
+ ).to eq nil
66
+
67
+ expect(
68
+ Unxls::BitOps.new(0b0110).value_at(-1..-2)
69
+ ).to eq nil
70
+
71
+ expect(
72
+ Unxls::BitOps.new(0b0101).value_at(0)
73
+ ).to eq 1
74
+
75
+ expect(
76
+ Unxls::BitOps.new(0b0101).value_at(1)
77
+ ).to eq 0
78
+
79
+ expect(
80
+ Unxls::BitOps.new(0b1111).value_at(-1)
81
+ ).to eq nil
82
+ end
83
+
84
+ it '#rol rotates left' do
85
+ expect(
86
+ Unxls::BitOps.new(0b1100).rol(4, 1)
87
+ ).to eq 0b1001
88
+ end
89
+
90
+ it '#ror rotates right' do
91
+ expect(
92
+ Unxls::BitOps.new(0b1100).ror(4, 1)
93
+ ).to eq 0b0110
94
+ end
95
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file was generated by the `rspec --init` command. Conventionally, all
4
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
5
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
6
+ # this file to always be loaded, without a need to explicitly require it in any
7
+ # files.
8
+ #
9
+ # Given that it is always loaded, you are encouraged to keep this file as
10
+ # light-weight as possible. Requiring heavyweight dependencies from this file
11
+ # will add to the boot time of your test suite on EVERY test run, even for an
12
+ # individual file that may not need all of that loaded. Instead, consider making
13
+ # a separate helper file that requires the additional dependencies and performs
14
+ # the additional setup, and require it from the spec files that actually need
15
+ # it.
16
+ #
17
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
18
+ RSpec.configure do |config|
19
+ # rspec-expectations config goes here. You can use an alternate
20
+ # assertion/expectation library such as wrong or the stdlib/minitest
21
+ # assertions if you prefer.
22
+ config.expect_with :rspec do |expectations|
23
+ # This option will default to `true` in RSpec 4. It makes the `description`
24
+ # and `failure_message` of custom matchers include text for helper methods
25
+ # defined using `chain`, e.g.:
26
+ # be_bigger_than(2).and_smaller_than(4).description
27
+ # # => "be bigger than 2 and smaller than 4"
28
+ # ...rather than:
29
+ # # => "be bigger than 2"
30
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
31
+ end
32
+
33
+ # rspec-mocks config goes here. You can use an alternate test double
34
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
35
+ config.mock_with :rspec do |mocks|
36
+ # Prevents you from mocking or stubbing a method that does not exist on
37
+ # a real object. This is generally recommended, and will default to
38
+ # `true` in RSpec 4.
39
+ mocks.verify_partial_doubles = true
40
+ end
41
+
42
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
43
+ # have no way to turn it off -- the option exists only for backwards
44
+ # compatibility in RSpec 3). It causes shared context metadata to be
45
+ # inherited by the metadata hash of host groups and examples, rather than
46
+ # triggering implicit auto-inclusion in groups with matching metadata.
47
+ config.shared_context_metadata_behavior = :apply_to_host_groups
48
+
49
+ # The settings below are suggested to provide a good initial experience
50
+ # with RSpec, but feel free to customize to your heart's content.
51
+ =begin
52
+ # This allows you to limit a spec run to individual examples or groups
53
+ # you care about by tagging them with `:focus` metadata. When nothing
54
+ # is tagged with `:focus`, all examples get run. RSpec also provides
55
+ # aliases for `it`, `describe`, and `context` that include `:focus`
56
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
57
+ config.filter_run_when_matching :focus
58
+
59
+ # Allows RSpec to persist some state between runs in order to support
60
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
61
+ # you configure your source control system to ignore this file.
62
+ config.example_status_persistence_file_path = "spec/examples.txt"
63
+
64
+ # Limits the available syntax to the non-monkey patched syntax that is
65
+ # recommended. For more details, see:
66
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
67
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
68
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
69
+ config.disable_monkey_patching!
70
+
71
+ # This setting enables warnings. It's recommended, but in some cases may
72
+ # be too noisy due to issues in dependencies.
73
+ config.warnings = true
74
+
75
+ # Many RSpec users commonly either run the entire suite or an individual
76
+ # file, and it's useful to allow more verbose output when running an
77
+ # individual spec file.
78
+ if config.files_to_run.one?
79
+ # Use the documentation formatter for detailed output,
80
+ # unless a formatter has already been configured
81
+ # (e.g. via a command-line flag).
82
+ config.default_formatter = "doc"
83
+ end
84
+
85
+ # Print the 10 slowest examples and example groups at the
86
+ # end of the spec run, to help surface which specs are running
87
+ # particularly slow.
88
+ config.profile_examples = 10
89
+
90
+ # Run specs in random order to surface order dependencies. If you find an
91
+ # order dependency and want to debug it, you can fix the order by providing
92
+ # the seed, which is printed after each run.
93
+ # --seed 1234
94
+ config.order = :random
95
+
96
+ # Seed global randomization in this process using the `--seed` CLI option.
97
+ # Setting this allows you to use `--seed` to deterministically reproduce
98
+ # test failures related to randomization by passing the same `--seed` value
99
+ # as the one that triggered the failure.
100
+ Kernel.srand config.seed
101
+ =end
102
+ end
103
+
104
+ # Silence output from pending examples in documentation formatter
105
+ module FormatterOverrides
106
+ def example_pending(_); end
107
+ def dump_pending(_); end
108
+ end
109
+ RSpec::Core::Formatters::DocumentationFormatter.prepend FormatterOverrides
110
+ RSpec::Core::Formatters::ProgressFormatter.prepend FormatterOverrides
111
+
112
+ require 'pry'
113
+ require_relative '../lib/unxls'
114
+
115
+ def testfile(*path)
116
+ Pathname.new(__FILE__).dirname.join('files', *path)
117
+ end