mangledotdev 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 (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/mangledotdev.rb +419 -0
  3. metadata +41 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a3e6f62d683b5a119aa43e4ec953e29b837019d5ccdfc24bcd319e2597181f5c
4
+ data.tar.gz: de1edd7d42fec313777ad5fe28aa6edf7389c2941dbc4219c0601334c3e39fde
5
+ SHA512:
6
+ metadata.gz: f993e4e3917a0311c6be42b72a61a98b2e76bdae80e47db9595e2ee8b57424b70872186504d2b7883969a49de0b56c61fb5691127b3d3dbfc7b64a468df74136
7
+ data.tar.gz: 54182dfc4c70ce26b9242996a1825a5b391abfc4174a4df6bb4154eac640ee0deddbe2d8d7d6d4cddc726d98f6d432dc6df3474f7174e2b931f5e4e9d1d7401d
@@ -0,0 +1,419 @@
1
+ require 'json'
2
+ require 'open3'
3
+ require 'securerandom'
4
+ require 'stringio'
5
+
6
+ # InputManager - Manages sending requests to other processes and handling responses.
7
+ #
8
+ # This is an instance-based class - create one instance per request.
9
+ #
10
+ # Attributes:
11
+ # response (Hash): Complete response with status, data, errors, warnings
12
+ #
13
+ # Methods:
14
+ # request(): Send a request to another process
15
+ # get_response(): Get the full response object
16
+ # get_data(): Get the response data (returns nil on error)
17
+ class InputManager
18
+ attr_reader :response
19
+
20
+ # Generate a unique key for request/response matching.
21
+ # @return [String] Unique key
22
+ def self.gen_key
23
+ SecureRandom.hex(16)
24
+ end
25
+
26
+ # Initialize a new InputManager instance.
27
+ def initialize
28
+ @process = nil
29
+ @raw_request = nil
30
+ @request = nil
31
+ @response = nil
32
+ @data = nil
33
+ @key = nil
34
+ end
35
+
36
+ private
37
+
38
+ # Validate file and build command to execute.
39
+ #
40
+ # @param language [String] Programming language/runtime
41
+ # @param file [String] Path to file to execute
42
+ # @return [Array<String>] Command array for subprocess
43
+ # @raise [ArgumentError] Invalid file extension, file not found, or permission error
44
+ def get_command(language, file)
45
+ lang_upper = language.to_s.upcase
46
+
47
+ # On Windows, convert forward slashes to backslashes for file system operations
48
+ if Gem.win_platform?
49
+ file = file.gsub('/', '\\')
50
+ end
51
+
52
+ file_ext = File.extname(file).downcase
53
+
54
+ # Extension validation - FIRST before file existence check
55
+ extension_map = {
56
+ 'PYTHON' => ['.py'],
57
+ 'PY' => ['.py'],
58
+ 'JAVASCRIPT' => ['.js'],
59
+ 'JS' => ['.js'],
60
+ 'NODE' => ['.js'],
61
+ 'NODEJS' => ['.js'],
62
+ 'RUBY' => ['.rb'],
63
+ 'RB' => ['.rb'],
64
+ 'C' => ['.c', '.out', '.exe', ''],
65
+ 'CS' => ['.exe', '.dll', ''],
66
+ 'CPP' => ['.cpp', '.cc', '.cxx', '.out', '.exe', ''],
67
+ 'C#' => ['.exe', '.dll', ''],
68
+ 'C++' => ['.cpp', '.cc', '.cxx', '.out', '.exe', ''],
69
+ 'CSHARP' => ['.exe', '.dll', ''],
70
+ 'CPLUSPLUS' => ['.cpp', '.cc', '.cxx', '.out', '.exe', ''],
71
+ 'EXE' => ['.cpp', '.cc', '.cxx', '.out', '.exe', ''],
72
+ 'JAR' => ['.jar'],
73
+ 'JAVA' => ['.jar'],
74
+ 'RUST' => ['.rs', '.exe', '.out', ''],
75
+ 'RS' => ['.rs', '.exe', '.out', ''],
76
+ 'GO' => ['.go', '.exe', '.out', ''],
77
+ 'GOLANG' => ['.go', '.exe', '.out', '']
78
+ }
79
+
80
+ if extension_map.key?(lang_upper)
81
+ valid_extensions = extension_map[lang_upper]
82
+ unless valid_extensions.include?(file_ext)
83
+ expected = valid_extensions.map { |ext| ext.empty? ? '(no extension)' : ext }.join(', ')
84
+ raise ArgumentError, "Invalid file '#{file}' for language '#{language}'. Expected: e.g. 'file#{expected}'"
85
+ end
86
+ end
87
+
88
+ # File existence check
89
+ raise ArgumentError, "File not found: #{file}" unless File.exist?(file)
90
+ raise ArgumentError, "Path is not a file: #{file}" unless File.file?(file)
91
+
92
+ # Permission checks
93
+ compiled_languages = ['C', 'CS', 'CPP', 'C#', 'C++', 'CSHARP', 'CPLUSPLUS', 'EXE', 'RUST', 'RS', 'GO', 'GOLANG']
94
+ if compiled_languages.include?(lang_upper)
95
+ raise ArgumentError, "File is not executable: #{file}" unless File.executable?(file)
96
+ else
97
+ raise ArgumentError, "File is not readable: #{file}" unless File.readable?(file)
98
+ end
99
+
100
+ # Auto-add ./ for compiled executables if not present and not absolute path
101
+ if compiled_languages.include?(lang_upper) && !File.absolute_path?(file) && !file.start_with?('./')
102
+ file = './' + file
103
+ end
104
+
105
+ # Build command
106
+ lang_map = {
107
+ 'PYTHON' => ['python', file],
108
+ 'PY' => ['python', file],
109
+ 'JAVASCRIPT' => ['node', file],
110
+ 'JS' => ['node', file],
111
+ 'NODE' => ['node', file],
112
+ 'NODEJS' => ['node', file],
113
+ 'RUBY' => ['ruby', file],
114
+ 'RB' => ['ruby', file],
115
+ 'C' => [file],
116
+ 'CS' => file_ext == '.dll' ? ['dotnet', file] : [file],
117
+ 'CPP' => [file],
118
+ 'C#' => file_ext == '.dll' ? ['dotnet', file] : [file],
119
+ 'C++' => [file],
120
+ 'CSHARP' => file_ext == '.dll' ? ['dotnet', file] : [file],
121
+ 'CPLUSPLUS' => [file],
122
+ 'EXE' => [file],
123
+ 'JAR' => ['java', '-jar', file],
124
+ 'JAVA' => ['java', '-jar', file],
125
+ 'RUST' => [file],
126
+ 'RS' => [file],
127
+ 'GO' => file_ext == '.go' ? ['go', 'run', file] : [file],
128
+ 'GOLANG' => file_ext == '.go' ? ['go', 'run', file] : [file]
129
+ }
130
+
131
+ raise ArgumentError, "Unsupported language: #{language}" unless lang_map.key?(lang_upper)
132
+
133
+ lang_map[lang_upper]
134
+ end
135
+
136
+ public
137
+
138
+ # Send a request to another process.
139
+ #
140
+ # @param is_unique [Boolean] Expect single output (true) or multiple (false)
141
+ # @param optional_output [Boolean] Output is optional (true) or required (false)
142
+ # @param data [Object] Data to send (any JSON-serializable type)
143
+ # @param language [String] Target language/runtime
144
+ # @param file [String] Path to target file
145
+ #
146
+ # Sets @response (Hash) with keys:
147
+ # - request_status (Boolean|nil): Success status
148
+ # - data: Response data (preserves type)
149
+ # - optionalOutput (Boolean): Echo of parameter
150
+ # - isUnique (Boolean): Echo of parameter
151
+ # - warnings (Array<String>): Warning messages
152
+ # - errors (Array<String>): Error messages
153
+ def request(is_unique: true, optional_output: true, data: nil, language:, file:)
154
+ begin
155
+ @key = InputManager.gen_key
156
+ command = get_command(language, file)
157
+
158
+ @raw_request = {
159
+ key: @key,
160
+ optionalOutput: optional_output,
161
+ isUnique: is_unique,
162
+ data: data
163
+ }
164
+ @request = JSON.generate(@raw_request)
165
+
166
+ response = {
167
+ request_status: nil,
168
+ data: nil,
169
+ optionalOutput: optional_output,
170
+ isUnique: is_unique,
171
+ warnings: [],
172
+ errors: []
173
+ }
174
+
175
+ stdout, stderr, status = Open3.capture3(*command, stdin_data: @request)
176
+
177
+ unless status.success?
178
+ response[:request_status] = false
179
+ response[:errors] << "Process exited with code #{status.exitstatus}"
180
+ response[:errors] << "stderr: #{stderr.strip}" unless stderr.strip.empty?
181
+ response[:warnings] << "Warning: these kind of errors result from an error in the targeted script."
182
+ @response = response
183
+ return
184
+ end
185
+
186
+ @response_data = []
187
+ stdout.strip.split("\n").each do |line|
188
+ next if line.strip.empty?
189
+
190
+ begin
191
+ parsed_data = JSON.parse(line)
192
+
193
+ # Validate response has matching key or null key (for init errors)
194
+ # This ensures we only process responses meant for this request
195
+ if parsed_data.is_a?(Hash) && (parsed_data['key'] == @key || parsed_data['key'].nil?)
196
+ @response_data << parsed_data
197
+ end
198
+ rescue JSON::ParserError
199
+ # Ignore lines that aren't valid JSON (e.g., debug prints)
200
+ end
201
+ end
202
+
203
+ if @response_data.length != 0
204
+ failure = false
205
+ @response_data.each do |resp|
206
+ failure = true unless resp['request_status']
207
+ end
208
+
209
+ response[:request_status] = !failure
210
+
211
+ response[:isUnique] = @response_data[0]['isUnique']
212
+
213
+ @response_data.each do |resp|
214
+ resp['errors'].each do |err|
215
+ response[:errors] << err
216
+ end
217
+ end
218
+
219
+ data_list = []
220
+ @response_data.each do |resp|
221
+ data_list << resp['data']
222
+ end
223
+
224
+ if response[:isUnique]
225
+ if data_list.length == 1
226
+ response[:data] = data_list[0]
227
+ else
228
+ response[:request_status] = false
229
+ response[:data] = nil
230
+ response[:errors] << "Error: Expected 1 output (isUnique=True) but received #{data_list.length}."
231
+ end
232
+ else
233
+ response[:data] = data_list
234
+ end
235
+ elsif optional_output
236
+ response[:request_status] = nil
237
+ response[:data] = nil
238
+ response[:warnings] << "Warning: the output setting is set to optional, and the targeted program didn't gave any output."
239
+ else
240
+ response[:request_status] = false
241
+ response[:data] = nil
242
+ response[:errors] << "Error: OutputManager might not be used or not correctly."
243
+ end
244
+
245
+ @response = response
246
+
247
+ rescue ArgumentError => e
248
+ @response = {
249
+ request_status: false,
250
+ data: nil,
251
+ optionalOutput: optional_output,
252
+ isUnique: is_unique,
253
+ warnings: ["Warning: targeted file not found or can't be executed, consider checking file informations and language dependencies."],
254
+ errors: ["Error: #{e.message}"]
255
+ }
256
+ rescue => e
257
+ @response = {
258
+ request_status: false,
259
+ data: nil,
260
+ optionalOutput: optional_output,
261
+ isUnique: is_unique,
262
+ warnings: [],
263
+ errors: ["Unexpected error: #{e.message}"]
264
+ }
265
+ end
266
+ end
267
+
268
+ # Get the full response object.
269
+ #
270
+ # @return [Hash] The complete response with status, data, errors, warnings.
271
+ def get_response
272
+ @response
273
+ end
274
+
275
+ # Get the response data if request was successful.
276
+ #
277
+ # @return [Object, nil] The data from the response (any type), or nil if request failed.
278
+ # The return type matches the type sent by the target process.
279
+ def get_data
280
+ if @response
281
+ return @response[:data] if @response[:request_status]
282
+ end
283
+ nil
284
+ end
285
+
286
+ # Bundle any value for use with request() - for API consistency with other languages.
287
+ # In Ruby, this just returns the value as-is since Ruby handles serialization automatically.
288
+ #
289
+ # @param value [Object] Any value
290
+ # @return [Object] The same value (Ruby handles JSON serialization automatically)
291
+ def self.bundle(value)
292
+ value
293
+ end
294
+ end
295
+
296
+ # OutputManager - Manages receiving requests from other processes and sending responses.
297
+ #
298
+ # This is a class-based/static manager - all methods are class methods.
299
+ # Must call init() before using.
300
+ #
301
+ # Class Attributes:
302
+ # data: The request data (accessible after init())
303
+ #
304
+ # Class Methods:
305
+ # init(): Initialize and read request from stdin
306
+ # output(val): Send response back via stdout
307
+ class OutputManager
308
+ @@original_stdout = nil
309
+ @@request = nil
310
+ @@data = nil
311
+ @@request_status = nil
312
+ @@optional = nil
313
+ @@unique_state = nil
314
+ @@init_error = nil
315
+ @@errors = []
316
+ @@warnings = []
317
+
318
+ class << self
319
+ attr_accessor :data
320
+
321
+ # Initialize OutputManager and read request from stdin.
322
+ #
323
+ # Must be called before using output() or accessing data.
324
+ # Suppresses stdout to prevent pollution of JSON protocol.
325
+ def init
326
+ # Save original stdout so we can restore it later
327
+ @@original_stdout = $stdout
328
+
329
+ # Redirect stdout to StringIO to suppress all puts/print statements
330
+ $stdout = StringIO.new
331
+
332
+ # Read the entire stdin (the JSON request from InputManager)
333
+ @@request = $stdin.read
334
+ @@data = JSON.parse(@@request)
335
+ self.data = @@data['data']
336
+ @@optional = @@data['optionalOutput']
337
+
338
+ # Reset state for new request
339
+ @@errors = []
340
+ @@warnings = []
341
+ @@init_error = nil
342
+ @@request_status = nil
343
+ @@unique_state = nil
344
+ end
345
+
346
+ # Send response back to the calling process.
347
+ #
348
+ # @param val [Object] Data to send (any JSON-serializable type)
349
+ #
350
+ # Note:
351
+ # Can be called multiple times if isUnique=false in request.
352
+ # Will error if called multiple times when isUnique=true.
353
+ def output(val)
354
+ # Check if OutputManager was initialized
355
+ if @@data.nil?
356
+ unless @@init_error
357
+ $stdout = @@original_stdout if @@original_stdout
358
+
359
+ @@request_status = false
360
+ @@errors << "Error: OutputManager isn't initialized."
361
+ write_output(nil, @@data)
362
+ @@init_error = true
363
+ end
364
+ else
365
+ # Check if we can output based on isUnique setting
366
+ # unique_state tracks if we've already output once
367
+ if !@@unique_state || !@@data['isUnique']
368
+ @@request_status = true
369
+ write_output(val, @@data)
370
+ else
371
+ # Multiple outputs when isUnique=true is an error
372
+ @@request_status = false
373
+ @@errors << "Error: outputs out of bound (isUnique: #{@@unique_state})."
374
+ write_output(val, @@data)
375
+ end
376
+
377
+ # Mark that we've output once
378
+ @@unique_state = @@data['isUnique']
379
+
380
+ # Re-suppress stdout after writing response
381
+ $stdout = StringIO.new
382
+ end
383
+ end
384
+
385
+ # Bundle any value for use with output() - for API consistency with other languages.
386
+ # In Ruby, this just returns the value as-is since Ruby handles serialization automatically.
387
+ #
388
+ # @param value [Object] Any value
389
+ # @return [Object] The same value (Ruby handles JSON serialization automatically)
390
+ def bundle(value)
391
+ value
392
+ end
393
+
394
+ private
395
+
396
+ # Internal method to write JSON response to stdout.
397
+ #
398
+ # @param args [Object] Data to send in response
399
+ # @param _data [Hash] Request data for key/metadata
400
+ def write_output(args, _data)
401
+ # Restore original stdout to actually write the response
402
+ $stdout = @@original_stdout if @@original_stdout
403
+
404
+ # Build and write JSON response
405
+ response = {
406
+ key: _data ? _data['key'] : nil,
407
+ request_status: @@request_status,
408
+ data: args,
409
+ optionalOutput: @@optional,
410
+ isUnique: _data ? _data['isUnique'] : nil,
411
+ errors: @@errors,
412
+ warnings: @@warnings
413
+ }
414
+
415
+ puts JSON.generate(response)
416
+ $stdout.flush
417
+ end
418
+ end
419
+ end
metadata ADDED
@@ -0,0 +1,41 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mangledotdev
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Wass B.
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: The universal programming language communication system.
13
+ email:
14
+ - wass.b@proton.me
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/mangledotdev.rb
20
+ homepage: https://github.com/WassBe/mangle.dev
21
+ licenses:
22
+ - Apache-2.0
23
+ metadata: {}
24
+ rdoc_options: []
25
+ require_paths:
26
+ - lib
27
+ required_ruby_version: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: '0'
32
+ required_rubygems_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ requirements: []
38
+ rubygems_version: 4.0.3
39
+ specification_version: 4
40
+ summary: Mangle.dev for Ruby.
41
+ test_files: []