tusktsk 2.0.1
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 +7 -0
- data/CHANGELOG.md +38 -0
- data/LICENSE +14 -0
- data/README.md +759 -0
- data/cli/main.rb +1488 -0
- data/exe/tsk +10 -0
- data/lib/peanut_config.rb +621 -0
- data/lib/tusk/license.rb +303 -0
- data/lib/tusk/protection.rb +180 -0
- data/lib/tusk_lang/shell_storage.rb +104 -0
- data/lib/tusk_lang/tsk.rb +501 -0
- data/lib/tusk_lang/tsk_parser.rb +234 -0
- data/lib/tusk_lang/tsk_parser_enhanced.rb +563 -0
- data/lib/tusk_lang/version.rb +5 -0
- data/lib/tusk_lang.rb +14 -0
- metadata +249 -0
@@ -0,0 +1,234 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# TSK Parser for Ruby
|
4
|
+
# Handles TOML-like TSK format parsing and generation
|
5
|
+
|
6
|
+
module TuskLang
|
7
|
+
# Parser for TSK (TuskLang Configuration) format
|
8
|
+
class TSKParser
|
9
|
+
# Parse TSK content into hash
|
10
|
+
def self.parse(content)
|
11
|
+
data, _ = parse_with_comments(content)
|
12
|
+
data
|
13
|
+
end
|
14
|
+
|
15
|
+
# Parse TSK content with comments preserved
|
16
|
+
def self.parse_with_comments(content)
|
17
|
+
lines = content.split("\n")
|
18
|
+
result = {}
|
19
|
+
comments = {}
|
20
|
+
current_section = nil
|
21
|
+
in_multiline_string = false
|
22
|
+
multiline_key = nil
|
23
|
+
multiline_content = []
|
24
|
+
|
25
|
+
lines.each_with_index do |line, i|
|
26
|
+
trimmed_line = line.strip
|
27
|
+
|
28
|
+
# Handle multiline strings
|
29
|
+
if in_multiline_string
|
30
|
+
if trimmed_line == '"""'
|
31
|
+
if current_section && multiline_key
|
32
|
+
result[current_section][multiline_key] = multiline_content.join("\n")
|
33
|
+
end
|
34
|
+
in_multiline_string = false
|
35
|
+
multiline_key = nil
|
36
|
+
multiline_content = []
|
37
|
+
next
|
38
|
+
end
|
39
|
+
multiline_content << line
|
40
|
+
next
|
41
|
+
end
|
42
|
+
|
43
|
+
# Capture comments
|
44
|
+
if trimmed_line.start_with?('#')
|
45
|
+
comments[i] = trimmed_line
|
46
|
+
next
|
47
|
+
end
|
48
|
+
|
49
|
+
# Skip empty lines
|
50
|
+
next if trimmed_line.empty?
|
51
|
+
|
52
|
+
# Section header
|
53
|
+
section_match = trimmed_line.match(/^\[(.+)\]$/)
|
54
|
+
if section_match
|
55
|
+
current_section = section_match[1]
|
56
|
+
result[current_section] = {}
|
57
|
+
next
|
58
|
+
end
|
59
|
+
|
60
|
+
# Key-value pair
|
61
|
+
if current_section && trimmed_line.include?('=')
|
62
|
+
separator_index = trimmed_line.index('=')
|
63
|
+
key = trimmed_line[0...separator_index].strip
|
64
|
+
value_str = trimmed_line[(separator_index + 1)..-1].strip
|
65
|
+
|
66
|
+
# Check for multiline string start
|
67
|
+
if value_str == '"""'
|
68
|
+
in_multiline_string = true
|
69
|
+
multiline_key = key
|
70
|
+
next
|
71
|
+
end
|
72
|
+
|
73
|
+
value = parse_value(value_str)
|
74
|
+
result[current_section][key] = value
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
[result, comments]
|
79
|
+
end
|
80
|
+
|
81
|
+
# Parse a TSK value string into appropriate Ruby type
|
82
|
+
private_class_method def self.parse_value(value_str)
|
83
|
+
# Null
|
84
|
+
return nil if value_str == 'null'
|
85
|
+
|
86
|
+
# Boolean
|
87
|
+
return true if value_str == 'true'
|
88
|
+
return false if value_str == 'false'
|
89
|
+
|
90
|
+
# Number
|
91
|
+
return value_str.to_i if value_str.match?(/^-?\d+$/)
|
92
|
+
return value_str.to_f if value_str.match?(/^-?\d+\.\d+$/)
|
93
|
+
|
94
|
+
# String
|
95
|
+
if value_str.start_with?('"') && value_str.end_with?('"')
|
96
|
+
return value_str[1...-1].gsub('\\"', '"').gsub('\\\\', '\\')
|
97
|
+
end
|
98
|
+
|
99
|
+
# Array
|
100
|
+
if value_str.start_with?('[') && value_str.end_with?(']')
|
101
|
+
array_content = value_str[1...-1].strip
|
102
|
+
return [] if array_content.empty?
|
103
|
+
|
104
|
+
items = split_array_items(array_content)
|
105
|
+
return items.map { |item| parse_value(item.strip) }
|
106
|
+
end
|
107
|
+
|
108
|
+
# Object/Hash
|
109
|
+
if value_str.start_with?('{') && value_str.end_with?('}')
|
110
|
+
obj_content = value_str[1...-1].strip
|
111
|
+
return {} if obj_content.empty?
|
112
|
+
|
113
|
+
pairs = split_object_pairs(obj_content)
|
114
|
+
obj = {}
|
115
|
+
|
116
|
+
pairs.each do |pair|
|
117
|
+
if pair.include?('=')
|
118
|
+
eq_index = pair.index('=')
|
119
|
+
key = pair[0...eq_index].strip
|
120
|
+
value = pair[(eq_index + 1)..-1].strip
|
121
|
+
# Remove quotes from key if present
|
122
|
+
clean_key = key.start_with?('"') && key.end_with?('"') ? key[1...-1] : key
|
123
|
+
obj[clean_key] = parse_value(value)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
return obj
|
128
|
+
end
|
129
|
+
|
130
|
+
# Return as string if no other type matches
|
131
|
+
value_str
|
132
|
+
end
|
133
|
+
|
134
|
+
# Split array items considering nested structures
|
135
|
+
private_class_method def self.split_array_items(content)
|
136
|
+
items = []
|
137
|
+
current = ''
|
138
|
+
depth = 0
|
139
|
+
in_string = false
|
140
|
+
|
141
|
+
content.each_char.with_index do |ch, i|
|
142
|
+
if ch == '"' && (i == 0 || content[i - 1] != '\\')
|
143
|
+
in_string = !in_string
|
144
|
+
end
|
145
|
+
|
146
|
+
unless in_string
|
147
|
+
depth += 1 if ch == '[' || ch == '{'
|
148
|
+
depth -= 1 if ch == ']' || ch == '}'
|
149
|
+
|
150
|
+
if ch == ',' && depth == 0
|
151
|
+
items << current.strip
|
152
|
+
current = ''
|
153
|
+
next
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
current += ch
|
158
|
+
end
|
159
|
+
|
160
|
+
items << current.strip unless current.strip.empty?
|
161
|
+
items
|
162
|
+
end
|
163
|
+
|
164
|
+
# Split object pairs considering nested structures
|
165
|
+
private_class_method def self.split_object_pairs(content)
|
166
|
+
pairs = []
|
167
|
+
current = ''
|
168
|
+
depth = 0
|
169
|
+
in_string = false
|
170
|
+
|
171
|
+
content.each_char.with_index do |ch, i|
|
172
|
+
if ch == '"' && (i == 0 || content[i - 1] != '\\')
|
173
|
+
in_string = !in_string
|
174
|
+
end
|
175
|
+
|
176
|
+
unless in_string
|
177
|
+
depth += 1 if ch == '[' || ch == '{'
|
178
|
+
depth -= 1 if ch == ']' || ch == '}'
|
179
|
+
|
180
|
+
if ch == ',' && depth == 0
|
181
|
+
pairs << current.strip
|
182
|
+
current = ''
|
183
|
+
next
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
current += ch
|
188
|
+
end
|
189
|
+
|
190
|
+
pairs << current.strip unless current.strip.empty?
|
191
|
+
pairs
|
192
|
+
end
|
193
|
+
|
194
|
+
# Convert hash back to TSK string format
|
195
|
+
def self.stringify(data)
|
196
|
+
result = []
|
197
|
+
|
198
|
+
data.each do |section, section_data|
|
199
|
+
result << "[#{section}]"
|
200
|
+
|
201
|
+
if section_data.is_a?(Hash)
|
202
|
+
section_data.each do |key, value|
|
203
|
+
result << "#{key} = #{format_value(value)}"
|
204
|
+
end
|
205
|
+
else
|
206
|
+
result << "value = #{format_value(section_data)}"
|
207
|
+
end
|
208
|
+
|
209
|
+
result << ''
|
210
|
+
end
|
211
|
+
|
212
|
+
result.join("\n").strip
|
213
|
+
end
|
214
|
+
|
215
|
+
# Format a value for TSK string representation
|
216
|
+
private_class_method def self.format_value(value)
|
217
|
+
case value
|
218
|
+
when nil then 'null'
|
219
|
+
when true then 'true'
|
220
|
+
when false then 'false'
|
221
|
+
when String then "\"#{value.gsub('"', '\\"').gsub('\\', '\\\\')}\""
|
222
|
+
when Numeric then value.to_s
|
223
|
+
when Hash
|
224
|
+
pairs = value.map { |k, v| "\"#{k}\" = #{format_value(v)}" }
|
225
|
+
"{#{pairs.join(', ')}}"
|
226
|
+
when Array
|
227
|
+
items = value.map { |v| format_value(v) }
|
228
|
+
"[#{items.join(', ')}]"
|
229
|
+
else
|
230
|
+
"\"#{value}\""
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|