sparkql 1.3.3 → 1.3.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1fb6060ec97e59958c12e0ed3338540afa180bf28eab0224af3ffef568dbf469
4
- data.tar.gz: 982d3956830686faeb210e6abf52125d18d4d68fa0d231b00a251a4876e1bd14
3
+ metadata.gz: ee4b94747c3d671e7ceac9675caaebaf3d8661a4a9316327719e7221f439b3bc
4
+ data.tar.gz: '09a7eddd5cd21a23d2083c8312f0a1bec48a0f21938c0871538cdb08ef52d1d3'
5
5
  SHA512:
6
- metadata.gz: 6754d10ca449bbd3467127a8470bb4233dd00b5162984ca636bcd93aff41743b25ace78fdd30ba77cbb6a79bef55d36118f67f2102b676ca9184db04c2437c01
7
- data.tar.gz: d337d7169ceb202c6f4d8a066196c472404cccf5e1b6f0fc13bf07a4fba62fa6d135c04bfc017544c73fb84ca1834f5d06c78bbf5ee2848ac16b06372b09b711
6
+ metadata.gz: 67b098969f33bf5eaa19277d3d202687c8ceddf4183d4f54cb703cdb7b5ed3aff934a201a3e238226a27bfcecce9b2c58e31e2ce64f76fa0f03babf23b980291
7
+ data.tar.gz: 42db5ca0692fca37b627562870641af084f539c64909fb8858e8bc872d4371d365621fe502cbba8289dbd0ea47e13cdc80df24c83172ebc3d88a92ec1291e64c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ v1.3.5, 2026-04-30
2
+ * [IMPROVEMENT] add date_date() function
3
+
4
+ v1.3.4, 2026-01-20
5
+ * [BUGFIX] Validate limit for days and weekdays
1
6
 
2
7
  v1.3.3, 2025-08-12
3
8
  -------------------
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.3.3
1
+ 1.3.5
@@ -21,6 +21,7 @@ module Sparkql
21
21
  VALID_REGEX_FLAGS = ['', 'i'].freeze
22
22
  MIN_DATE_TIME = Time.new(1970, 1, 1, 0, 0, 0, '+00:00').iso8601
23
23
  MAX_DATE_TIME = Time.new(9999, 12, 31, 23, 59, 59, '+00:00').iso8601
24
+ MAX_DAYS = (365 * 1000).freeze # 1000 years ought to cover most cases
24
25
  VALID_CAST_TYPES = %i[field character decimal integer].freeze
25
26
 
26
27
  SUPPORTED_FUNCTIONS = {
@@ -43,16 +44,16 @@ module Sparkql
43
44
  regex: {
44
45
  args: [:character],
45
46
  opt_args: [{
46
- type: :character,
47
- default: ''
48
- }],
47
+ type: :character,
48
+ default: ''
49
+ }],
49
50
  return_type: :character
50
51
  },
51
52
  substring: {
52
53
  args: [%i[field character], :integer],
53
54
  opt_args: [{
54
- type: :integer
55
- }],
55
+ type: :integer
56
+ }],
56
57
  resolve_for_type: true,
57
58
  return_type: :character
58
59
  },
@@ -526,6 +527,15 @@ module Sparkql
526
527
 
527
528
  # Offset the current timestamp by a number of days
528
529
  def days(number_of_days)
530
+ if number_of_days.abs > MAX_DAYS
531
+ @errors << Sparkql::ParserError.new(token: number_of_days,
532
+ message: "Function call 'days' max offset #{MAX_DAYS} days",
533
+ status: :fatal,
534
+ syntax: false,
535
+ constraint: true)
536
+ return
537
+ end
538
+
529
539
  # date calculated as the offset from midnight tommorrow. Zero will provide values for all times
530
540
  # today.
531
541
  d = current_date + number_of_days
@@ -536,6 +546,15 @@ module Sparkql
536
546
  end
537
547
 
538
548
  def weekdays(number_of_days)
549
+ if number_of_days.abs > MAX_DAYS
550
+ @errors << Sparkql::ParserError.new(token: number_of_days,
551
+ message: "Function call 'weekdays' max offset #{MAX_DAYS} days",
552
+ status: :fatal,
553
+ syntax: false,
554
+ constraint: true)
555
+ return
556
+ end
557
+
539
558
  today = current_date
540
559
  weekend_start = today.saturday? || today.sunday?
541
560
  direction = number_of_days.positive? ? 1 : -1
@@ -632,6 +651,10 @@ module Sparkql
632
651
  }
633
652
  end
634
653
 
654
+ def date_date(value)
655
+ date_datetime(value)
656
+ end
657
+
635
658
  def time_datetime(datetime)
636
659
  {
637
660
  type: :time,
@@ -445,6 +445,24 @@ class FunctionResolverTest < Test::Unit::TestCase
445
445
  end
446
446
  end
447
447
 
448
+ test 'days - exceed max' do
449
+ f = FunctionResolver.new('days',
450
+ [{ type: :integer, value: 365_001 }],
451
+ current_timestamp: EXAMPLE_DATE)
452
+ f.validate
453
+ assert !f.errors?
454
+ assert_nil f.call
455
+ assert f.errors?, "function 'days' limit 365000"
456
+
457
+ f = FunctionResolver.new('days',
458
+ [{ type: :integer, value: -365_001 }],
459
+ current_timestamp: EXAMPLE_DATE)
460
+ f.validate
461
+ assert !f.errors?
462
+ assert_nil f.call
463
+ assert f.errors?, "function 'days' limit 365000"
464
+ end
465
+
448
466
  test 'weekdays()' do
449
467
  friday = Date.new(2012, 10, 19)
450
468
  saturday = Date.new(2012, 10, 20)
@@ -514,6 +532,24 @@ class FunctionResolverTest < Test::Unit::TestCase
514
532
  end
515
533
  end
516
534
 
535
+ test 'weekdays - exceed max' do
536
+ f = FunctionResolver.new('weekdays',
537
+ [{ type: :integer, value: 365_001 }],
538
+ current_timestamp: EXAMPLE_DATE)
539
+ f.validate
540
+ assert !f.errors?
541
+ assert_nil f.call
542
+ assert f.errors?, "function 'weekdays' limit 365000"
543
+
544
+ f = FunctionResolver.new('weekdays',
545
+ [{ type: :integer, value: -365_001 }],
546
+ current_timestamp: EXAMPLE_DATE)
547
+ f.validate
548
+ assert !f.errors?
549
+ assert_nil f.call
550
+ assert f.errors?, "function 'weekdays' limit 365000"
551
+ end
552
+
517
553
  test 'months()' do
518
554
  f = FunctionResolver.new('months',
519
555
  [{ type: :integer, value: 3 }],
@@ -0,0 +1,150 @@
1
+ require 'test_helper'
2
+
3
+ # Security regression tests covering SQL injection attack vectors identified
4
+ # during audit. The parser is responsible for rejecting malformed input and
5
+ # producing type-safe output. Consumers of parser output must still use
6
+ # parameterized queries and quote field identifiers — see comments below.
7
+ class SecurityTest < Test::Unit::TestCase
8
+ include Sparkql
9
+
10
+ def setup
11
+ @parser = Parser.new
12
+ end
13
+
14
+ # --- String value injection ---
15
+
16
+ test 'rejects unclosed string literals' do
17
+ assert_parse_error "City Eq 'Fargo"
18
+ assert_parse_error "City Eq Fargo'"
19
+ end
20
+
21
+ test 'rejects double-quote SQL injection in string values' do
22
+ # SparkQL only recognises \' as an escape sequence inside strings.
23
+ # A bare '' does not escape the quote — the lexer matches 'test' and
24
+ # the leftover 'injection' token causes a parse error.
25
+ assert_parse_error "City Eq 'test''injection'"
26
+ end
27
+
28
+ test 'string with backslash-escaped quote parses and escapes safely' do
29
+ # \' is the valid SparkQL escape for a literal apostrophe. :value retains
30
+ # outer quotes; character_escape strips them and unescapes \' to '.
31
+ # The resulting value contains a single quote — consumers MUST use bind
32
+ # parameters, not string interpolation, when building SQL from this value.
33
+ expressions = @parser.parse("City Eq 'O\\'Brien'")
34
+ assert !@parser.errors?, @parser.errors.inspect
35
+ assert_equal "O'Brien", @parser.character_escape(expressions.first[:value])
36
+ end
37
+
38
+ test 'sql payload inside valid sparkql string is accepted but value is raw' do
39
+ # The lexer correctly accepts 'val\'; DROP TABLE t; --' as a character
40
+ # literal (\' is a valid escape sequence). character_escape returns the raw
41
+ # string including the injected SQL — safe only with parameterized queries.
42
+ expressions = @parser.parse("City Eq 'val\\'; DROP TABLE t; --'")
43
+ assert !@parser.errors?, @parser.errors.inspect
44
+ assert_equal "val'; DROP TABLE t; --", @parser.character_escape(expressions.first[:value])
45
+ end
46
+
47
+ test 'plain string :value retains outer quotes before character_escape' do
48
+ expressions = @parser.parse("City Eq 'Fargo'")
49
+ assert !@parser.errors?, @parser.errors.inspect
50
+ assert_equal "'Fargo'", expressions.first[:value]
51
+ assert_equal :character, expressions.first[:type]
52
+ assert_equal 'Fargo', @parser.character_escape(expressions.first[:value])
53
+ end
54
+
55
+ # --- Numeric/decimal injection ---
56
+
57
+ test 'rejects sql keywords appended after integer token' do
58
+ assert_parse_error "Price Eq 100; DROP TABLE listings"
59
+ assert_parse_error "Price Eq 100 UNION SELECT"
60
+ end
61
+
62
+ test 'integer values are coerced to integers preventing non-numeric injection' do
63
+ expressions = @parser.parse("Price Eq 100")
64
+ assert !@parser.errors?, @parser.errors.inspect
65
+ assert_equal '100', expressions.first[:value]
66
+ assert_equal :integer, expressions.first[:type]
67
+ end
68
+
69
+ test 'decimal values are stored as strings preventing non-numeric injection' do
70
+ expressions = @parser.parse("Price Eq 100.50")
71
+ assert !@parser.errors?, @parser.errors.inspect
72
+ assert_equal '100.50', expressions.first[:value]
73
+ assert_equal :decimal, expressions.first[:type]
74
+ end
75
+
76
+ # --- Operator injection ---
77
+
78
+ test 'rejects sql operators that are not in the sparkql whitelist' do
79
+ assert_parse_error "City LIKE '%Fargo%'"
80
+ assert_parse_error "Price > 100"
81
+ assert_parse_error "Price < 100"
82
+ assert_parse_error "Price != 100"
83
+ end
84
+
85
+ test 'rejects sql keywords used as conjunctions' do
86
+ # OR/AND lowercased is valid SparkQL, but 1=1 is not a valid expression.
87
+ # Uppercase SQL keywords like UNION are rejected entirely.
88
+ assert_parse_error "City Eq 'Fargo' UNION SELECT City FROM listings"
89
+ assert_parse_error "City Eq 'Fargo' OR 1=1"
90
+ assert_parse_error "City Eq 'Fargo' AND 1=1"
91
+ end
92
+
93
+ # --- Function name injection ---
94
+
95
+ test 'rejects function names not in the supported functions whitelist' do
96
+ assert_parse_error "City Eq unknownfn('Fargo')"
97
+ assert_parse_error "City Eq exec('xp_cmdshell')"
98
+ assert_parse_error "City Eq sleep(10)"
99
+ end
100
+
101
+ # --- Custom field name handling ---
102
+
103
+ test 'custom field regex rejects names starting with dollar sign or period' do
104
+ assert_parse_error '"$BadGroup"."Field" Eq 10'
105
+ assert_parse_error '"Group"."$BadField" Eq 10'
106
+ assert_parse_error '"Group".".BadField" Eq 10'
107
+ assert_parse_error '"Group.Bad"."Field" Eq 10'
108
+ assert_parse_error '"Group"."Field.Sub" Eq 10'
109
+ assert_parse_error '""."" Eq 10'
110
+ end
111
+
112
+ test 'valid custom field names parse and field is returned verbatim' do
113
+ filter = '"General Property Description"."Taxes" Lt 500.0'
114
+ expressions = @parser.parse(filter)
115
+ assert !@parser.errors?, @parser.errors.inspect
116
+ assert expressions.first[:custom_field]
117
+ # The field value is returned as-is with its surrounding double-quotes.
118
+ # Consumers MUST validate this against allowed metadata and/or quote it
119
+ # as a SQL identifier before using it in a query — never interpolate directly.
120
+ assert_equal '"General Property Description"."Taxes"', expressions.first[:field]
121
+ end
122
+
123
+ test 'custom field name containing sql-significant characters is returned verbatim' do
124
+ # SparkQL allows characters like ; and ' inside double-quoted field names.
125
+ # The CUSTOM_FIELD regex does not strip these — that is the consumer's job.
126
+ filter = %("It's a field"."Value" Eq 10)
127
+ expressions = @parser.parse(filter)
128
+ assert !@parser.errors?, @parser.errors.inspect
129
+ assert expressions.first[:custom_field]
130
+ assert expressions.first[:field].include?("It's a field"), \
131
+ "Expected field to contain the group name, got: #{expressions.first[:field].inspect}"
132
+ end
133
+
134
+ # --- Date/datetime injection ---
135
+
136
+ test 'valid dates parse to their string representation' do
137
+ expressions = @parser.parse("DateField Eq 2023-06-15")
138
+ assert !@parser.errors?, @parser.errors.inspect
139
+ assert_equal '2023-06-15', expressions.first[:value]
140
+ assert_equal :date, expressions.first[:type]
141
+ end
142
+
143
+ private
144
+
145
+ def assert_parse_error(filter)
146
+ parser = Parser.new
147
+ parser.parse(filter)
148
+ assert parser.errors?, "Expected parse error for: #{filter.inspect}"
149
+ end
150
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sparkql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.3
4
+ version: 1.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wade McEwen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-19 00:00:00.000000000 Z
11
+ date: 2026-05-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: georuby
@@ -155,6 +155,7 @@ files:
155
155
  - test/unit/lexer_test.rb
156
156
  - test/unit/parser_compatability_test.rb
157
157
  - test/unit/parser_test.rb
158
+ - test/unit/security_test.rb
158
159
  homepage: ''
159
160
  licenses:
160
161
  - Apache 2.0