orb_template 0.1.3 → 0.2.2

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.
@@ -18,6 +18,12 @@ module ORB
18
18
  # Tags that should be ignored
19
19
  IGNORED_BODY_TAGS = %w[script style].freeze
20
20
 
21
+ # Maximum allowed brace nesting depth to prevent memory exhaustion
22
+ MAX_BRACE_DEPTH = 100
23
+
24
+ # Maximum allowed template source size in bytes (2MB)
25
+ MAX_TEMPLATE_SIZE = 2 * 1024 * 1024
26
+
21
27
  # Tags that are self-closing by HTML5 spec
22
28
  VOID_ELEMENTS = %w[area base br col command embed hr img input keygen link meta param source track wbr].freeze
23
29
 
@@ -28,7 +34,6 @@ module ORB
28
34
  @raise_errors = options.fetch(:raise_errors, true)
29
35
 
30
36
  # Streaming Tokenizer State
31
- @cursor = 0
32
37
  @column = 1
33
38
  @line = 1
34
39
  @errors = []
@@ -36,11 +41,15 @@ module ORB
36
41
  @attributes = []
37
42
  @braces = []
38
43
  @state = :initial
39
- @buffer = StringIO.new
44
+ @buffer = +''
40
45
  end
41
46
 
42
47
  # Main Entry
43
48
  def tokenize
49
+ if @source.string.bytesize > MAX_TEMPLATE_SIZE
50
+ raise ORB::SyntaxError.new("Template exceeds maximum size (#{MAX_TEMPLATE_SIZE} bytes)", 0)
51
+ end
52
+
44
53
  next_token until @source.eos?
45
54
 
46
55
  # Consume remaining buffer
@@ -56,11 +65,6 @@ module ORB
56
65
 
57
66
  # Dispatcher based on current state
58
67
  def next_token
59
- # Detect infinite loop
60
- # if @previous_cursor == @cursor && @previous_state == @state
61
- # raise "Internal Error: detected infinite loop in :#{@state}"
62
- # end
63
-
64
68
  # Dispatch to state handler
65
69
  send(:"next_in_#{@state}")
66
70
  end
@@ -114,7 +118,7 @@ module ORB
114
118
  add_token(:tag_open, nil)
115
119
  move_by_matched
116
120
  transition_to(:tag_open)
117
- elsif @source.scan(OTHER)
121
+ elsif @source.scan(INITIAL_TEXT) || @source.scan(OTHER)
118
122
  buffer_matched
119
123
  move_by_matched
120
124
  else
@@ -233,7 +237,7 @@ module ORB
233
237
  current_attribute[2] = attribute_value
234
238
  move_by_matched
235
239
  transition_to(:tag_open_content)
236
- elsif @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
240
+ elsif @source.scan(SINGLE_QUOTED_TEXT) || @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(OTHER)
237
241
  buffer_matched
238
242
  move_by_matched
239
243
  else
@@ -249,7 +253,7 @@ module ORB
249
253
  current_attribute[2] = attribute_value
250
254
  move_by_matched
251
255
  transition_to(:tag_open_content)
252
- elsif @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
256
+ elsif @source.scan(DOUBLE_QUOTED_TEXT) || @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(OTHER)
253
257
  buffer_matched
254
258
  move_by_matched
255
259
  else
@@ -260,7 +264,7 @@ module ORB
260
264
  # Read next token in :attribute_value_expression state
261
265
  def next_in_attribute_value_expression
262
266
  if @source.scan(BRACE_OPEN)
263
- @braces << "{"
267
+ push_brace
264
268
  buffer_matched
265
269
  move_by_matched
266
270
  elsif @source.scan(BRACE_CLOSE)
@@ -275,7 +279,7 @@ module ORB
275
279
  move_by_matched
276
280
  transition_to(:tag_open_content)
277
281
  end
278
- elsif @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
282
+ elsif @source.scan(EXPRESSION_TEXT) || @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(OTHER)
279
283
  buffer_matched
280
284
  move_by_matched
281
285
  else
@@ -286,7 +290,7 @@ module ORB
286
290
  # Read next token in :splat_attribute_expression state
287
291
  def next_in_splat_attribute_expression
288
292
  if @source.scan(BRACE_OPEN)
289
- @braces << "{"
293
+ push_brace
290
294
  buffer_matched
291
295
  move_by_matched
292
296
  elsif @source.scan(BRACE_CLOSE)
@@ -301,7 +305,7 @@ module ORB
301
305
  clear_braces
302
306
  transition_to(:tag_open_content)
303
307
  end
304
- elsif @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
308
+ elsif @source.scan(EXPRESSION_TEXT) || @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(OTHER)
305
309
  buffer_matched
306
310
  move_by_matched
307
311
  else
@@ -346,7 +350,7 @@ module ORB
346
350
  update_current_token(text)
347
351
  move_by_matched
348
352
  transition_to(:initial)
349
- elsif @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
353
+ elsif @source.scan(COMMENT_TEXT) || @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(OTHER)
350
354
  buffer_matched
351
355
  move_by_matched
352
356
  else
@@ -361,7 +365,7 @@ module ORB
361
365
  update_current_token(text)
362
366
  move_by_matched
363
367
  transition_to(:initial)
364
- elsif @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
368
+ elsif @source.scan(COMMENT_TEXT) || @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(OTHER)
365
369
  buffer_matched
366
370
  move_by_matched
367
371
  else
@@ -385,7 +389,7 @@ module ORB
385
389
  # Read block expression until closing brace
386
390
  def next_in_block_open_content
387
391
  if @source.scan(BRACE_OPEN)
388
- @braces << "{"
392
+ push_brace
389
393
  buffer_matched
390
394
  move_by_matched
391
395
  elsif @source.scan(BRACE_CLOSE)
@@ -399,7 +403,7 @@ module ORB
399
403
  buffer_matched
400
404
  move_by_matched
401
405
  end
402
- elsif @source.scan(BLANK) || @source.scan(OTHER)
406
+ elsif @source.scan(BLOCK_CONTENT_TEXT) || @source.scan(OTHER)
403
407
  buffer_matched
404
408
  move_by_matched
405
409
  end
@@ -437,7 +441,7 @@ module ORB
437
441
  move_by_matched
438
442
  transition_to(:initial)
439
443
  elsif @source.scan(BRACE_OPEN)
440
- @braces << "{"
444
+ push_brace
441
445
  buffer_matched
442
446
  move_by_matched
443
447
  elsif @source.scan(BRACE_CLOSE)
@@ -462,7 +466,7 @@ module ORB
462
466
  move_by_matched
463
467
  transition_to(:initial)
464
468
  elsif @source.scan(BRACE_OPEN)
465
- @braces << "{"
469
+ push_brace
466
470
  buffer_matched
467
471
  move_by_matched
468
472
  elsif @source.scan(BRACE_CLOSE)
@@ -498,7 +502,7 @@ module ORB
498
502
  buffer(tmp)
499
503
  move_by(tmp)
500
504
  end
501
- elsif @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
505
+ elsif @source.scan(VERBATIM_TEXT) || @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(OTHER)
502
506
  buffer_matched
503
507
  move_by_matched
504
508
  end
@@ -537,20 +541,29 @@ module ORB
537
541
  @braces = []
538
542
  end
539
543
 
544
+ def push_brace
545
+ if @braces.length >= MAX_BRACE_DEPTH
546
+ raise ORB::SyntaxError.new("Maximum brace nesting depth (#{MAX_BRACE_DEPTH}) exceeded", @line)
547
+ end
548
+
549
+ @braces << "{"
550
+ end
551
+
540
552
  # Moves the cursor
541
553
  def move(line, column)
542
554
  @line = line
543
555
  @column = column
544
556
  end
545
557
 
558
+ # Update line/column tracking from a matched string.
559
+ # Uses String#count and String#rindex instead of re-scanning with StringScanner.
546
560
  def move_by(str)
547
- scan = StringScanner.new(str)
548
- until scan.eos?
549
- if scan.scan(NEWLINE) || scan.scan(CRLF)
550
- move(@line + 1, 1)
551
- elsif scan.scan(OTHER)
552
- move(@line, @column + scan.matched.size)
553
- end
561
+ newlines = str.count("\n")
562
+ if newlines.positive?
563
+ @line += newlines
564
+ @column = str.length - str.rindex("\n")
565
+ else
566
+ @column += str.length
554
567
  end
555
568
  end
556
569
 
@@ -565,7 +578,11 @@ module ORB
565
578
 
566
579
  # Create a new token
567
580
  def create_token(type, value, meta = {})
568
- Token.new(type, value, meta.merge(line: @line, column: @column) { |_k, v1, _v2| v1 })
581
+ if meta.empty?
582
+ Token.new(type, value, { line: @line, column: @column })
583
+ else
584
+ Token.new(type, value, { line: @line, column: @column }.merge!(meta))
585
+ end
569
586
  end
570
587
 
571
588
  # Create a token and add it to the token list
@@ -586,18 +603,18 @@ module ORB
586
603
 
587
604
  # Read the buffer to a string
588
605
  def read_buffer
589
- @buffer.string.clone
606
+ @buffer.dup
590
607
  end
591
608
 
592
609
  # Clear the buffer
593
610
  def clear_buffer
594
- @buffer = StringIO.new
611
+ @buffer = +''
595
612
  end
596
613
 
597
614
  # Read the buffer to a string and clear it
598
615
  def consume_buffer
599
- str = read_buffer
600
- clear_buffer
616
+ str = @buffer
617
+ @buffer = +''
601
618
  str
602
619
  end
603
620
 
data/lib/orb/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ORB
4
- VERSION = "0.1.3"
4
+ VERSION = "0.2.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: orb_template
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - KUY.io Inc.
@@ -39,6 +39,7 @@ files:
39
39
  - Makefile
40
40
  - README.md
41
41
  - Rakefile
42
+ - docs/2026-03-12-security-analysis.md
42
43
  - lib/orb.rb
43
44
  - lib/orb/ast.rb
44
45
  - lib/orb/ast/abstract_node.rb
@@ -98,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
99
  - !ruby/object:Gem::Version
99
100
  version: '0'
100
101
  requirements: []
101
- rubygems_version: 3.7.2
102
+ rubygems_version: 4.0.6
102
103
  specification_version: 4
103
104
  summary: The ORB template language for Ruby.
104
105
  test_files: []