smp_tool 0.1.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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +13 -0
  3. data/.vscode/launch.json +21 -0
  4. data/CHANGELOG.md +3 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +109 -0
  7. data/Rakefile +16 -0
  8. data/lib/smp_tool/autoloader.rb +24 -0
  9. data/lib/smp_tool/basic_10.rb +95 -0
  10. data/lib/smp_tool/basic_20.rb +96 -0
  11. data/lib/smp_tool/filename.rb +71 -0
  12. data/lib/smp_tool/version.rb +5 -0
  13. data/lib/smp_tool/virtual_volume/data_entry.rb +47 -0
  14. data/lib/smp_tool/virtual_volume/data_entry_header.rb +85 -0
  15. data/lib/smp_tool/virtual_volume/file_interface.rb +33 -0
  16. data/lib/smp_tool/virtual_volume/utils/converter_from_volume_io.rb +81 -0
  17. data/lib/smp_tool/virtual_volume/utils/converter_to_volume_io.rb +125 -0
  18. data/lib/smp_tool/virtual_volume/utils/empty_vol_data_initializer.rb +32 -0
  19. data/lib/smp_tool/virtual_volume/utils/file_converter.rb +55 -0
  20. data/lib/smp_tool/virtual_volume/utils/file_extracter.rb +90 -0
  21. data/lib/smp_tool/virtual_volume/utils/volume_params_validator.rb +20 -0
  22. data/lib/smp_tool/virtual_volume/utils.rb +10 -0
  23. data/lib/smp_tool/virtual_volume/volume.rb +249 -0
  24. data/lib/smp_tool/virtual_volume/volume_data.rb +216 -0
  25. data/lib/smp_tool/virtual_volume/volume_params.rb +70 -0
  26. data/lib/smp_tool/virtual_volume/volume_params_contract.rb +53 -0
  27. data/lib/smp_tool/virtual_volume.rb +8 -0
  28. data/lib/smp_tool/volume_io/bootloader.rb +18 -0
  29. data/lib/smp_tool/volume_io/data.rb +14 -0
  30. data/lib/smp_tool/volume_io/dir_entry.rb +45 -0
  31. data/lib/smp_tool/volume_io/dir_seg.rb +33 -0
  32. data/lib/smp_tool/volume_io/dir_seg_header.rb +23 -0
  33. data/lib/smp_tool/volume_io/directory.rb +26 -0
  34. data/lib/smp_tool/volume_io/file_content.rb +13 -0
  35. data/lib/smp_tool/volume_io/home_block.rb +18 -0
  36. data/lib/smp_tool/volume_io/volume_io.rb +53 -0
  37. data/lib/smp_tool/volume_io.rb +10 -0
  38. data/lib/smp_tool.rb +54 -0
  39. data/sig/smp_tool.rbs +4 -0
  40. data/smp_tool.gemspec +40 -0
  41. metadata +206 -0
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SMPTool
4
+ module VirtualVolume
5
+ #
6
+ # Provides set of methods to work with volume's data.
7
+ #
8
+ class VolumeData < SimpleDelegator
9
+ def initialize(obj, extra_word)
10
+ @obj = obj
11
+ @extra_word = extra_word
12
+
13
+ super(obj)
14
+ end
15
+
16
+ def snapshot
17
+ map(&:snapshot)
18
+ end
19
+
20
+ #
21
+ # Rename file on the virtual volume.
22
+ #
23
+ # @param [SMPTool::Filename] old_id
24
+ # @param [SMPTool::Filename] new_id
25
+ #
26
+ # @return [Array<String>]
27
+ # Old and new ASCII filenames of a renamed file.
28
+ #
29
+ # @raise [ArgumentError]
30
+ # - Can't assign name `new_id` to the file since another file with
31
+ # the same name already exists;
32
+ # - File `old_id` not found.
33
+ #
34
+ def f_rename(old_id, new_id)
35
+ _raise_already_exists(new_id.print_ascii) if _already_exists?(new_id.radix50)
36
+
37
+ idx = _find_idx(old_id.radix50)
38
+
39
+ _raise_file_not_found(old_id.print_ascii) unless idx
40
+
41
+ self[idx].rename(new_id.radix50)
42
+
43
+ [old_id.print_ascii, new_id.print_ascii]
44
+ end
45
+
46
+ #
47
+ # Delete file from the virtual volume.
48
+ #
49
+ # @param [SMPTool::Filename] file_id
50
+ #
51
+ # @return [String]
52
+ # ASCII filename of a deleted file.
53
+ #
54
+ # @raise [ArgumentError]
55
+ # - File with `file_id` not found.
56
+ #
57
+ def f_delete(file_id)
58
+ idx = _find_idx(file_id.radix50)
59
+
60
+ _raise_file_not_found(file_id.print_ascii) unless idx
61
+
62
+ self[idx].clean
63
+
64
+ file_id.print_ascii
65
+ end
66
+
67
+ #
68
+ # Consolidate all free space at the end of the volume.
69
+ #
70
+ # @return [Integer] n_free_clusters
71
+ # Number of free clusters that were joined.
72
+ #
73
+ def squeeze
74
+ n_free_clusters = calc_n_free_clusters
75
+
76
+ return n_free_clusters if n_free_clusters.zero?
77
+
78
+ reject!(&:empty_entry?)
79
+
80
+ push_empty_entry(n_free_clusters)
81
+
82
+ n_free_clusters
83
+ end
84
+
85
+ #
86
+ # Calculate total number of free clusters.
87
+ #
88
+ # @return [Integer]
89
+ #
90
+ def calc_n_free_clusters
91
+ self.select(&:empty_entry?)
92
+ .sum(&:n_clusters)
93
+ end
94
+
95
+ #
96
+ # Append a file.
97
+ #
98
+ # @param [SMPTool::VirtualVolume::DataEntry] file
99
+ #
100
+ # @return [String] file.print_ascii_filename
101
+ # ASCII filename of the pushed file.
102
+ #
103
+ def f_push(file)
104
+ _raise_already_exists(file.print_ascii_filename) if _already_exists?(file.filename)
105
+
106
+ # We're starting from the end of the array, since free space tend to locate
107
+ # at the end of the volume (esp. after the 'squeeze' command).
108
+ idx = index(reverse_each.detect { |e| e.n_clusters >= file.n_clusters && e.empty_entry? })
109
+
110
+ unless idx
111
+ raise ArgumentError,
112
+ "no free space found to fit the file (try to squeeze, delete files or allocate more clusters)"
113
+ end
114
+
115
+ _f_push(file, idx)
116
+
117
+ file.print_ascii_filename
118
+ end
119
+
120
+ def push_empty_entry(n_free_clusters)
121
+ push(_new_empty_entry(n_free_clusters))
122
+
123
+ self
124
+ end
125
+
126
+ #
127
+ # <Description>
128
+ #
129
+ # @param [Integer] n_clusters
130
+ # Number of clusters to add (pos. int.) or to trim (neg. int.).
131
+ #
132
+ # @return [Integer] n_clusters
133
+ # Number of clusters that were added/trimmed.
134
+ #
135
+ def resize(n_clusters)
136
+ n_free_clusters = calc_n_free_clusters
137
+ diff = n_free_clusters + n_clusters
138
+
139
+ if reject(&:empty_entry?).empty? && diff < 1
140
+ raise ArgumentError, "Can't trim: volume should keep at least one empty/file entry"
141
+ end
142
+
143
+ reject!(&:empty_entry?)
144
+ push_empty_entry(diff) unless diff.zero?
145
+
146
+ n_clusters
147
+ end
148
+
149
+ private
150
+
151
+ def _new_empty_entry(n_free_clusters)
152
+ DataEntry.new(
153
+ header: DataEntryHeader.new(
154
+ _free_entry_header_params(n_free_clusters)
155
+ ),
156
+ data: PAD_CHR * (n_free_clusters * CLUSTER_SIZE)
157
+ )
158
+ end
159
+
160
+ def _f_push(file, idx)
161
+ n_clusters_left = self[idx].n_clusters - file.n_clusters
162
+
163
+ insert(idx, file)
164
+ delete_at(idx + 1)
165
+
166
+ return if n_clusters_left.zero?
167
+
168
+ insert(-idx, _new_empty_entry(n_clusters_left))
169
+ end
170
+
171
+ def _free_entry_header_params(n_clusters)
172
+ {
173
+ status: EMPTY_ENTRY,
174
+ filename: [PAD_WORD, PAD_WORD, PAD_WORD],
175
+ n_clusters: n_clusters,
176
+ ch_job: DEF_CH_JOB,
177
+ date: DEF_DATE,
178
+ extra_word: @extra_word
179
+ }
180
+ end
181
+
182
+ def _raise_file_not_found(filename)
183
+ raise ArgumentError, "File '#{filename}' not found on the volume"
184
+ end
185
+
186
+ def _raise_already_exists(filename)
187
+ raise ArgumentError, "File with the filename '#{filename}' already exists on the volume"
188
+ end
189
+
190
+ #
191
+ # Check if file with the Radix-50 filename `file_id` already exists.
192
+ #
193
+ # @param [Array<Integer>] file_id
194
+ # RADIX-50 filename.
195
+ #
196
+ # @return [Boolean]
197
+ #
198
+ def _already_exists?(file_id)
199
+ idx = _find_idx(file_id)
200
+ idx.nil? ? false : true
201
+ end
202
+
203
+ #
204
+ # Find index of a file with the Radix-50 filename `file_id`.
205
+ #
206
+ # @param [Array<Integer>] file_id
207
+ # RADIX-50 filename.
208
+ #
209
+ # @return [Integer, nil]
210
+ #
211
+ def _find_idx(file_id)
212
+ find_index { |e| e.filename == file_id }
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SMPTool
4
+ module VirtualVolume
5
+ #
6
+ # Virtual volume parameters.
7
+ #
8
+ class VolumeParams
9
+ attr_accessor :n_clusters_allocated
10
+
11
+ attr_reader :n_extra_bytes_per_entry, :n_dir_segs,
12
+ :n_clusters_per_dir_seg, :extra_word, :n_max_entries_per_dir_seg,
13
+ :n_max_entries
14
+
15
+ def initialize(
16
+ n_clusters_allocated:,
17
+ n_extra_bytes_per_entry:,
18
+ n_dir_segs:,
19
+ n_clusters_per_dir_seg:,
20
+ extra_word:
21
+ )
22
+ @n_clusters_allocated = n_clusters_allocated
23
+ @n_extra_bytes_per_entry = n_extra_bytes_per_entry
24
+ @n_dir_segs = n_dir_segs
25
+ @n_clusters_per_dir_seg = n_clusters_per_dir_seg
26
+ @extra_word = extra_word
27
+
28
+ _validate_input
29
+
30
+ @n_max_entries_per_dir_seg = _calc_n_max_entries_per_dir_seg
31
+ @n_max_entries = @n_dir_segs * @n_max_entries_per_dir_seg
32
+ end
33
+
34
+ def snapshot
35
+ {
36
+ n_clusters_allocated: @n_clusters_allocated,
37
+ n_extra_bytes_per_entry: @n_extra_bytes_per_entry,
38
+ n_dir_segs: @n_dir_segs,
39
+ n_clusters_per_dir_seg: @n_clusters_per_dir_seg,
40
+ extra_word: @extra_word,
41
+ n_max_entries_per_dir_seg: @n_max_entries_per_dir_seg,
42
+ n_max_entries: @n_max_entries
43
+ }
44
+ end
45
+
46
+ private
47
+
48
+ def _validate_input
49
+ result = VolumeParamsContract.new.call(_input_snapshot)
50
+
51
+ raise ArgumentError, result.errors.to_h.to_a.join(": ") unless result.success?
52
+ end
53
+
54
+ def _input_snapshot
55
+ {
56
+ n_clusters_allocated: @n_clusters_allocated,
57
+ n_extra_bytes_per_entry: @n_extra_bytes_per_entry,
58
+ n_dir_segs: @n_dir_segs,
59
+ n_clusters_per_dir_seg: @n_clusters_per_dir_seg,
60
+ extra_word: @extra_word
61
+ }
62
+ end
63
+
64
+ def _calc_n_max_entries_per_dir_seg
65
+ entry_size = ENTRY_BASE_SIZE + @n_extra_bytes_per_entry
66
+ (((@n_clusters_per_dir_seg * CLUSTER_SIZE) - HEADER_SIZE - FOOTER_SIZE) / entry_size).floor
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SMPTool
4
+ module VirtualVolume
5
+ #
6
+ # Virtual volume params contract.
7
+ #
8
+ class VolumeParamsContract < Dry::Validation::Contract
9
+ config.messages.default_locale = :en
10
+
11
+ json do
12
+ required(:n_clusters_allocated).value(
13
+ :integer,
14
+ lteq?: N_CLUSTERS_MAX
15
+ )
16
+
17
+ required(:n_extra_bytes_per_entry).value(
18
+ :integer,
19
+ included_in?: [0, 2]
20
+ )
21
+
22
+ required(:n_dir_segs).value(
23
+ :integer,
24
+ gteq?: 1,
25
+ lteq?: 2
26
+ )
27
+
28
+ required(:n_clusters_per_dir_seg).value(
29
+ :integer,
30
+ gteq?: 1,
31
+ lteq?: 2
32
+ )
33
+
34
+ required(:extra_word).value(
35
+ :integer
36
+ )
37
+ end
38
+
39
+ rule(:n_dir_segs, :n_clusters_per_dir_seg) do
40
+ msg = "Can't use :n_dir_segs => #{values[:n_dir_segs]}" \
41
+ "& :n_clusters_per_dir_seg => #{values[:n_clusters_per_dir_seg]}"
42
+
43
+ key.failure(msg) if values[:n_dir_segs] == 2 && values[:n_clusters_per_dir_seg] == 1
44
+ end
45
+
46
+ rule(:n_clusters_allocated, :n_clusters_per_dir_seg, :n_dir_segs) do
47
+ n_min_clusters = N_SYS_CLUSTERS + values[:n_clusters_per_dir_seg] * values[:n_dir_segs] + 1
48
+ msg = "Min. number of clusters for the configuration is: #{n_min_clusters}"
49
+ key.failure(msg) if values[:n_clusters_allocated] < n_min_clusters
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SMPTool
4
+ #
5
+ # Namespace for the virtual volume.
6
+ #
7
+ module VirtualVolume; end
8
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SMPTool
4
+ module VolumeIO
5
+ #
6
+ # Bootloader.
7
+ #
8
+ class Bootloader < BinData::Record
9
+ endian :little
10
+
11
+ array :bytes, initial_length: -> { CLUSTER_SIZE } do
12
+ uint8
13
+ end
14
+
15
+ string :padding, length: -> { CLUSTER_SIZE - padding.rel_offset }, pad_byte: PAD_BYTE
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SMPTool
4
+ module VolumeIO
5
+ #
6
+ # Volume data.
7
+ #
8
+ class Data < BinData::Array
9
+ default_parameter read_until: -> { index == all_entries.length - 1 }
10
+
11
+ file_content
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SMPTool
4
+ module VolumeIO
5
+ #
6
+ # A single directory entry.
7
+ #
8
+ class DirEntry < BinData::Record
9
+ endian :little
10
+
11
+ virtual :ascii_filename, value: -> { _radix_to_ascii(filename) }, onlyif: -> { status != DIR_SEG_FOOTER }
12
+
13
+ # Status word: empty area/permanent file/end-of-segment:
14
+ uint16le :status
15
+
16
+ # RADIX-50 filename (6 bytes):
17
+ array :filename, onlyif: -> { status != DIR_SEG_FOOTER }, initial_length: RAD50_FN_SIZE do
18
+ uint16le initial_value: PAD_WORD
19
+ end
20
+
21
+ # Number of clusters occupied by the file:
22
+ uint16le :n_clusters, onlyif: -> { status != DIR_SEG_FOOTER }
23
+
24
+ # Job & channel, unused in the MK90:
25
+ uint16le :ch_job, onlyif: -> { status != DIR_SEG_FOOTER }, initial_value: DEF_CH_JOB
26
+
27
+ # Creation date, unused in the MK90:
28
+ uint16le :date, onlyif: -> { status != DIR_SEG_FOOTER }, initial_value: DEF_DATE
29
+
30
+ # Extra word is unused in the BASIC v.1.0, but it is required in the BASIC v.2.0.
31
+ # The use in the v.2.0 is unknown, probably it was reserved for a checksum.
32
+ # BASIC v.2.0 always sets it to 0x00A0.
33
+ uint16le :extra_word,
34
+ onlyif: lambda {
35
+ status != DIR_SEG_FOOTER && header.n_extra_bytes_per_entry.positive?
36
+ }
37
+
38
+ private
39
+
40
+ def _radix_to_ascii(int_arr)
41
+ DECRadix50.decode(DECRadix50::MK90_CHARSET, int_arr)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dir_seg_header"
4
+ require_relative "dir_entry"
5
+
6
+ module SMPTool
7
+ module VolumeIO
8
+ #
9
+ # A single segment from the directory.
10
+ #
11
+ class DirSeg < BinData::Record
12
+ endian :little
13
+ hide :padding
14
+
15
+ dir_seg_header :header
16
+
17
+ array :dir_seg_entries, read_until: -> { element.status == DIR_SEG_FOOTER } do
18
+ dir_entry :dir_entry
19
+ end
20
+
21
+ virtual :n_clusters_per_dir_seg, initial_value: -> { header.data_offset == 3 ? 1 : N_CLUSTERS_PER_DIR_SEG }
22
+ virtual :entry_size, initial_value: -> { ENTRY_BASE_SIZE + header.n_extra_bytes_per_entry }
23
+ virtual :n_max_entries, initial_value: lambda {
24
+ (((n_clusters_per_dir_seg * CLUSTER_SIZE) - header.num_bytes - FOOTER_SIZE) / entry_size).floor
25
+ }
26
+
27
+ string :padding, length: -> { n_clusters_per_dir_seg * CLUSTER_SIZE - padding.rel_offset }, pad_byte: PAD_BYTE
28
+
29
+ # Number of entries in this dirseg:
30
+ virtual :n_entries, initial_value: -> { dir_seg_entries.reject { |e| e.status == DIR_SEG_FOOTER }.length }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SMPTool
4
+ module VolumeIO
5
+ #
6
+ # Directory segment header. See Table 1-2: Directory Segment Header Words [DEC].
7
+ #
8
+ # Notes:
9
+ # - word #4 should be set to 0x0000 for the BASIC v.1.0, 0x0002 for the BASIC v.2.0.
10
+ # - word #5 allows to use only one cluster for the entire directory.
11
+ #
12
+ class DirSegHeader < BinData::Record
13
+ endian :little
14
+
15
+ uint16le :n_dir_segs # The total number of segments in this directory.
16
+ uint16le :i_next_seg # The index of the next logical directory segment.
17
+ uint16le :i_high_seg, # The index of the highest segment currently in use.
18
+ initial_value: 1
19
+ uint16le :n_extra_bytes_per_entry # The number of extra bytes per directory entry, an even number.
20
+ uint16le :data_offset # The block number where the actual stored data begins.
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dir_seg"
4
+
5
+ module SMPTool
6
+ module VolumeIO
7
+ #
8
+ # From the DEC's manual [DEC]:
9
+ #
10
+ # The directory consists of a series of two-block segments. Each segment is 512 words
11
+ # long and contains information about files such as name, length, and creation date.
12
+ #
13
+ class Directory < BinData::Record
14
+ endian :little
15
+
16
+ # Last dirseg should set 'i_next_seg' word in the header to 0,
17
+ # so that indicates the last segment in the directory.
18
+ array :segments, read_until: -> { element.header.i_next_seg.zero? } do
19
+ dir_seg
20
+ end
21
+
22
+ # Total number of entries (from all dirsegs) in this dir.
23
+ virtual :n_entries, initial_value: -> { segments.to_ary.sum(&:n_entries) }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SMPTool
4
+ module VolumeIO
5
+ #
6
+ # Content of a single entry (file).
7
+ #
8
+ class FileContent < BinData::String
9
+ default_parameter read_length: -> { all_entries.to_a[index].n_clusters * CLUSTER_SIZE }
10
+ default_parameter pad_byte: PAD_BYTE
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SMPTool
4
+ module VolumeIO
5
+ #
6
+ # Home block.
7
+ #
8
+ class HomeBlock < BinData::Record
9
+ endian :little
10
+
11
+ array :bytes, initial_length: -> { CLUSTER_SIZE } do
12
+ uint8
13
+ end
14
+
15
+ string :padding, length: -> { CLUSTER_SIZE - padding.rel_offset }, pad_byte: PAD_BYTE
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "bootloader"
4
+ require_relative "home_block"
5
+ require_relative "directory"
6
+ require_relative "file_content"
7
+ require_relative "data"
8
+
9
+ module SMPTool
10
+ module VolumeIO
11
+ #
12
+ # MK90 BASIC volume.
13
+ #
14
+ class VolumeIO < BinData::Record
15
+ hide :bootloader
16
+ hide :home_block
17
+
18
+ bootloader :bootloader
19
+ home_block :home_block
20
+ directory :directory
21
+ data :data
22
+
23
+ virtual :n_clusters_allocated, initial_value: -> { num_bytes / CLUSTER_SIZE }
24
+
25
+ # Empty and permanent entries.
26
+ virtual :all_entries,
27
+ initial_value: lambda {
28
+ directory.segments.flat_map(&:dir_seg_entries)
29
+ .reject { |e| e.status == DIR_SEG_FOOTER }
30
+ }
31
+
32
+ # Permanent entries (i.e. files) only.
33
+ virtual :file_only_entries,
34
+ initial_value: lambda {
35
+ directory.segments.flat_map(&:dir_seg_entries)
36
+ .reject { |e| e.status == DIR_SEG_FOOTER }
37
+ .reject { |e| e.status == EMPTY_ENTRY }
38
+ }
39
+
40
+ virtual :n_extra_bytes_per_entry,
41
+ initial_value: -> { directory.segments.first.header.n_extra_bytes_per_entry }
42
+
43
+ virtual :n_max_entries_per_dir_seg,
44
+ initial_value: -> { directory.segments.first.n_max_entries }
45
+
46
+ virtual :n_dir_segs,
47
+ initial_value: -> { directory.segments.length }
48
+
49
+ virtual :n_clusters_per_dir_seg,
50
+ initial_value: -> { directory.segments.first.n_clusters_per_dir_seg }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SMPTool
4
+ #
5
+ # Namespace for the classes that handle volume IO operations,
6
+ # e.g. reading/parsing volume from a 'raw' binary string, and
7
+ # writing it back to a binary string.
8
+ #
9
+ module VolumeIO; end
10
+ end
data/lib/smp_tool.rb ADDED
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+ require "delegate"
5
+ require "dec_radix_50"
6
+ require "dry/validation"
7
+ require "forwardable"
8
+ require "injalid_dejice"
9
+ require "zeitwerk"
10
+
11
+ require_relative "smp_tool/autoloader"
12
+ require_relative "smp_tool/version"
13
+
14
+ SMPTool::Autoloader.setup
15
+
16
+ #
17
+ # Lib to work with Elektronika MK90 bin volumes.
18
+ #
19
+ module SMPTool
20
+ class Error < StandardError; end
21
+
22
+ #
23
+ # Documentation sources:
24
+ #
25
+ # 1. [DEC] http://www.bitsavers.org/pdf/dec/pdp11/rt11/v5.6_Aug91/AA-PD6PA-TC_RT-11_Volume_and_File_Formats_Manual_Aug91.pdf
26
+ #
27
+
28
+ PAD_BYTE = 0x20
29
+ PAD_CHR = PAD_BYTE.chr.freeze
30
+ PAD_WORD = 0x2020
31
+
32
+ # Sizes, in clusters:
33
+ N_SYS_CLUSTERS = 2 # Bootloader + home block.
34
+ N_CLUSTERS_PER_DIR_SEG = 2
35
+ N_CLUSTERS_MAX = 127
36
+
37
+ # Sizes, in bytes:
38
+ CLUSTER_SIZE = 512
39
+ HEADER_SIZE = 10
40
+ FOOTER_SIZE = 2
41
+ ENTRY_BASE_SIZE = 14
42
+
43
+ # Sizes, in 16-bit words:
44
+ RAD50_FN_SIZE = 3 # RADIX-50 filename size.
45
+
46
+ # Directory entry status codes.
47
+ EMPTY_ENTRY = 0x0200 # Empty entry.
48
+ PERM_ENTRY = 0x0400 # Permanent file (occupied entry).
49
+ DIR_SEG_FOOTER = 0x0800 # Directory segment footer, a.k.a. end-of-segment marker.
50
+
51
+ # Default entry attributes:
52
+ DEF_CH_JOB = 0x0000
53
+ DEF_DATE = 0xFFFF
54
+ end
data/sig/smp_tool.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module SMPTool
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end