jq 1.0.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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +32 -0
- data/.github/workflows/release-gem.yml +38 -0
- data/.gitignore +49 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +49 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +40 -0
- data/LICENSE +21 -0
- data/README.md +214 -0
- data/Rakefile +15 -0
- data/SECURITY.md +121 -0
- data/ext/jq/extconf.rb +79 -0
- data/ext/jq/jq_ext.c +385 -0
- data/ext/jq/jq_ext.h +24 -0
- data/jq.gemspec +35 -0
- data/lib/jq.rb +101 -0
- data/sig/jq.rbs +48 -0
- metadata +123 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 85a8def8b4728ce33cb6bf3322969e08d95abd40dec1efd317a7a0de764f333a
|
|
4
|
+
data.tar.gz: 96a55036c41973603dba2bbcea2970ebd6312fa2334c1afb79feba3327015228
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 301b55c4c77ec72aca48de6cf73d8ef421710b0cea7c6aa041191c0d39cbbe1763530920bdcf98c333d80c07ce4677c5fad78f3841ec8448e7ce2db3571a9148
|
|
7
|
+
data.tar.gz: 6d3c8b437eff186dc30e24f6add659312c37c4f0c4f73c602f172d93655c2eb02362ed128d21dd59f6b47bbb2f14805c1214b20ef71348eae92d4710aebd7b6d
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_call:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, macos-latest]
|
|
16
|
+
ruby: ['3.3', '3.4']
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Install autotools (macOS)
|
|
22
|
+
if: runner.os == 'macOS'
|
|
23
|
+
run: brew install autoconf automake libtool
|
|
24
|
+
|
|
25
|
+
- name: Set up Ruby
|
|
26
|
+
uses: ruby/setup-ruby@v1
|
|
27
|
+
with:
|
|
28
|
+
ruby-version: ${{ matrix.ruby }}
|
|
29
|
+
bundler-cache: true
|
|
30
|
+
|
|
31
|
+
- name: Run tests
|
|
32
|
+
run: bundle exec rake
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: write
|
|
8
|
+
id-token: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
ci:
|
|
12
|
+
uses: ./.github/workflows/ci.yml
|
|
13
|
+
|
|
14
|
+
release:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
environment: Release
|
|
17
|
+
|
|
18
|
+
needs: [ci]
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Set up Ruby
|
|
24
|
+
uses: ruby/setup-ruby@v1
|
|
25
|
+
with:
|
|
26
|
+
ruby-version: '3.3'
|
|
27
|
+
bundler-cache: true
|
|
28
|
+
|
|
29
|
+
- name: Publish to RubyGems
|
|
30
|
+
uses: rubygems/release-gem@v1
|
|
31
|
+
with:
|
|
32
|
+
await-release: true
|
|
33
|
+
|
|
34
|
+
- name: Create GitHub Release
|
|
35
|
+
uses: softprops/action-gh-release@v2
|
|
36
|
+
with:
|
|
37
|
+
files: pkg/*.gem
|
|
38
|
+
generate_release_notes: true
|
data/.gitignore
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/.bundle/
|
|
2
|
+
/.yardoc
|
|
3
|
+
/_yardoc/
|
|
4
|
+
/coverage/
|
|
5
|
+
/doc/
|
|
6
|
+
/pkg/
|
|
7
|
+
/spec/reports/
|
|
8
|
+
/tmp/
|
|
9
|
+
|
|
10
|
+
# rspec failure tracking
|
|
11
|
+
spec/examples.txt
|
|
12
|
+
|
|
13
|
+
# Compiled extensions
|
|
14
|
+
*.bundle
|
|
15
|
+
*.so
|
|
16
|
+
*.o
|
|
17
|
+
*.a
|
|
18
|
+
*.log
|
|
19
|
+
|
|
20
|
+
# Compiled extension outputs
|
|
21
|
+
/lib/jq/jq_ext.bundle
|
|
22
|
+
/lib/jq/jq_ext.so
|
|
23
|
+
/lib/jq/jq_ext.dll
|
|
24
|
+
|
|
25
|
+
# Build artifacts
|
|
26
|
+
/ext/jq/*.o
|
|
27
|
+
/ext/jq/*.so
|
|
28
|
+
/ext/jq/*.bundle
|
|
29
|
+
/ext/jq/Makefile
|
|
30
|
+
/ext/jq/mkmf.log
|
|
31
|
+
/ext/jq/tmp/
|
|
32
|
+
|
|
33
|
+
# miniportile
|
|
34
|
+
/ext/jq/ports/
|
|
35
|
+
/ext/jq/tmp/
|
|
36
|
+
|
|
37
|
+
# OS files
|
|
38
|
+
.DS_Store
|
|
39
|
+
Thumbs.db
|
|
40
|
+
|
|
41
|
+
# Editor files
|
|
42
|
+
*.swp
|
|
43
|
+
*.swo
|
|
44
|
+
*~
|
|
45
|
+
.vscode/
|
|
46
|
+
.idea/
|
|
47
|
+
|
|
48
|
+
# Gem files
|
|
49
|
+
*.gem
|
data/.tool-versions
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby 3.3.9
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-01-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release of jq-ruby gem
|
|
13
|
+
- `JQ.filter` method for applying jq filters to JSON
|
|
14
|
+
- Support for `raw_output` option (jq -r)
|
|
15
|
+
- Support for `compact_output` option (jq -c)
|
|
16
|
+
- Support for `sort_keys` option (jq -S)
|
|
17
|
+
- Support for `multiple_outputs` option to return all results
|
|
18
|
+
- `JQ.validate_filter!` method for validating jq filter expressions
|
|
19
|
+
- Exception hierarchy:
|
|
20
|
+
- `JQ::Error` - Base exception class
|
|
21
|
+
- `JQ::CompileError` - Invalid jq filter
|
|
22
|
+
- `JQ::RuntimeError` - Runtime execution error
|
|
23
|
+
- `JQ::ParseError` - Invalid JSON input
|
|
24
|
+
- Bundled jq 1.8.1 compiled from source via mini_portile2
|
|
25
|
+
- Self-contained installation with no system dependencies
|
|
26
|
+
- SHA256 verification of jq source tarball
|
|
27
|
+
- Static compilation for reliability
|
|
28
|
+
- Comprehensive test suite with >95% coverage
|
|
29
|
+
- Core functionality tests
|
|
30
|
+
- Filter validation tests
|
|
31
|
+
- Error handling tests
|
|
32
|
+
- Security tests
|
|
33
|
+
- Memory leak detection tests
|
|
34
|
+
- Complete documentation
|
|
35
|
+
- README with usage examples
|
|
36
|
+
- SECURITY.md with security considerations
|
|
37
|
+
- RBS type signatures
|
|
38
|
+
- Ruby 3.3+ required
|
|
39
|
+
- Thread safety documentation (jq is NOT thread-safe)
|
|
40
|
+
|
|
41
|
+
### Security
|
|
42
|
+
|
|
43
|
+
- Proper jv memory lifecycle management prevents buffer overflows
|
|
44
|
+
- All inputs validated for type before processing
|
|
45
|
+
- JSON parsed by jq's built-in parser (no eval/injection risk)
|
|
46
|
+
- Filter expressions cannot execute system commands (safe by design)
|
|
47
|
+
- Tested with large inputs, deeply nested structures, and malicious filters
|
|
48
|
+
|
|
49
|
+
[1.0.0]: https://github.com/persona-id/jq-ruby/releases/tag/v1.0.0
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
jq (1.0.0)
|
|
5
|
+
mini_portile2 (~> 2.8)
|
|
6
|
+
|
|
7
|
+
GEM
|
|
8
|
+
remote: https://rubygems.org/
|
|
9
|
+
specs:
|
|
10
|
+
diff-lcs (1.6.2)
|
|
11
|
+
mini_portile2 (2.8.9)
|
|
12
|
+
rake (13.3.1)
|
|
13
|
+
rake-compiler (1.3.1)
|
|
14
|
+
rake
|
|
15
|
+
rspec (3.13.2)
|
|
16
|
+
rspec-core (~> 3.13.0)
|
|
17
|
+
rspec-expectations (~> 3.13.0)
|
|
18
|
+
rspec-mocks (~> 3.13.0)
|
|
19
|
+
rspec-core (3.13.6)
|
|
20
|
+
rspec-support (~> 3.13.0)
|
|
21
|
+
rspec-expectations (3.13.5)
|
|
22
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
23
|
+
rspec-support (~> 3.13.0)
|
|
24
|
+
rspec-mocks (3.13.7)
|
|
25
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
26
|
+
rspec-support (~> 3.13.0)
|
|
27
|
+
rspec-support (3.13.6)
|
|
28
|
+
|
|
29
|
+
PLATFORMS
|
|
30
|
+
arm64-darwin-24
|
|
31
|
+
ruby
|
|
32
|
+
|
|
33
|
+
DEPENDENCIES
|
|
34
|
+
jq!
|
|
35
|
+
rake (~> 13.0)
|
|
36
|
+
rake-compiler (~> 1.2)
|
|
37
|
+
rspec (~> 3.13)
|
|
38
|
+
|
|
39
|
+
BUNDLED WITH
|
|
40
|
+
2.7.2
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Persona Identity Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# jq-ruby
|
|
2
|
+
|
|
3
|
+
A minimal, security-focused Ruby gem that wraps the jq C library for JSON transformation.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'jq'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install jq
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Note:** This gem bundles jq 1.8.1 and builds it from source automatically during installation. No system dependencies are required.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Basic Filtering
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require 'jq'
|
|
33
|
+
|
|
34
|
+
# Basic filter
|
|
35
|
+
result = JQ.filter('{"name":"Alice","age":30}', '.name')
|
|
36
|
+
# => "\"Alice\""
|
|
37
|
+
|
|
38
|
+
# Identity filter (compact output is the default)
|
|
39
|
+
result = JQ.filter('{"a":1}', '.')
|
|
40
|
+
# => "{\"a\":1}"
|
|
41
|
+
|
|
42
|
+
# Nested access
|
|
43
|
+
result = JQ.filter('{"user":{"name":"Bob"}}', '.user.name')
|
|
44
|
+
# => "\"Bob\""
|
|
45
|
+
|
|
46
|
+
# Array operations
|
|
47
|
+
result = JQ.filter('[1,2,3]', '.[1]')
|
|
48
|
+
# => "2"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Options
|
|
52
|
+
|
|
53
|
+
#### Raw Output
|
|
54
|
+
|
|
55
|
+
Get raw strings without JSON encoding (equivalent to `jq -r`):
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
result = JQ.filter('{"name":"Alice"}', '.name', raw_output: true)
|
|
59
|
+
# => "Alice" (not "\"Alice\"")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### Compact Output
|
|
63
|
+
|
|
64
|
+
**Compact output is the default.** JSON is returned on a single line without extra whitespace:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
result = JQ.filter('{"a":1,"b":2}', '.')
|
|
68
|
+
# => "{\"a\":1,\"b\":2}" (default)
|
|
69
|
+
|
|
70
|
+
# To get pretty-printed output, set compact_output: false
|
|
71
|
+
result = JQ.filter('{"a":1,"b":2}', '.', compact_output: false)
|
|
72
|
+
# => "{\n \"a\": 1,\n \"b\": 2\n}"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### Sort Keys
|
|
76
|
+
|
|
77
|
+
Sort object keys for deterministic output (equivalent to `jq -S`):
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
result = JQ.filter('{"z":1,"a":2}', '.', sort_keys: true)
|
|
81
|
+
# => "{\"a\":2,\"z\":1}"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### Multiple Outputs
|
|
85
|
+
|
|
86
|
+
Return an array of all results instead of just the first:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
result = JQ.filter('[1,2,3]', '.[]', multiple_outputs: true)
|
|
90
|
+
# => ["1", "2", "3"]
|
|
91
|
+
|
|
92
|
+
# Combining with raw_output
|
|
93
|
+
result = JQ.filter('["a","b","c"]', '.[]', multiple_outputs: true, raw_output: true)
|
|
94
|
+
# => ["a", "b", "c"]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Complex Transformations
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# Map operation
|
|
101
|
+
json = '[{"name":"Alice","age":30},{"name":"Bob","age":25}]'
|
|
102
|
+
result = JQ.filter(json, '[.[] | .name]')
|
|
103
|
+
# => "[\"Alice\",\"Bob\"]"
|
|
104
|
+
|
|
105
|
+
# Select operation
|
|
106
|
+
result = JQ.filter(json, '[.[] | select(.age > 26)]')
|
|
107
|
+
# => "[{\"name\":\"Alice\",\"age\":30}]"
|
|
108
|
+
|
|
109
|
+
# Multiple transformations
|
|
110
|
+
result = JQ.filter('{"a":1,"b":2,"c":3}', 'to_entries | map(.value) | add')
|
|
111
|
+
# => "6"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Filter Validation
|
|
115
|
+
|
|
116
|
+
Validate a filter before using it:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
JQ.validate_filter!('.name')
|
|
120
|
+
# => true
|
|
121
|
+
|
|
122
|
+
JQ.validate_filter!('...invalid')
|
|
123
|
+
# raises JQ::CompileError
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Error Handling
|
|
127
|
+
|
|
128
|
+
The gem defines four exception classes:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
begin
|
|
132
|
+
JQ.filter('invalid json', '.')
|
|
133
|
+
rescue JQ::ParseError => e
|
|
134
|
+
puts "Invalid JSON: #{e.message}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
begin
|
|
138
|
+
JQ.filter('{}', '...invalid filter')
|
|
139
|
+
rescue JQ::CompileError => e
|
|
140
|
+
puts "Invalid filter: #{e.message}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
begin
|
|
144
|
+
JQ.filter('{}', '.nonexistent.deeply.nested')
|
|
145
|
+
rescue JQ::RuntimeError => e
|
|
146
|
+
puts "Runtime error: #{e.message}"
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Exception hierarchy:
|
|
151
|
+
|
|
152
|
+
- `JQ::Error` - Base class for all jq-related errors
|
|
153
|
+
- `JQ::ParseError` - Invalid JSON input
|
|
154
|
+
- `JQ::CompileError` - Invalid jq filter
|
|
155
|
+
- `JQ::RuntimeError` - Runtime execution error
|
|
156
|
+
|
|
157
|
+
## Thread Safety
|
|
158
|
+
|
|
159
|
+
**Status: Likely safe with jq 1.7+, but not officially guaranteed**
|
|
160
|
+
|
|
161
|
+
This gem creates an isolated `jq_state` for each call, and jq 1.7+ fixed a critical thread-safety bug (PR #2546). Multi-threaded use is **probably safe** in MRI Ruby where the GVL serializes execution, but jq hasn't made formal thread-safety guarantees.
|
|
162
|
+
|
|
163
|
+
**Recommendations:**
|
|
164
|
+
- ✅ Use with jq 1.7+ (check: `jq --version`)
|
|
165
|
+
- ✅ MRI Ruby (standard Ruby) - likely safe due to GVL
|
|
166
|
+
- ⚠️ JRuby/TruffleRuby - use with caution (true parallel threads)
|
|
167
|
+
- 🛡️ Safest for heavy parallel workloads: separate processes
|
|
168
|
+
|
|
169
|
+
See [SECURITY.md](SECURITY.md) for detailed thread safety information.
|
|
170
|
+
|
|
171
|
+
## Memory Considerations
|
|
172
|
+
|
|
173
|
+
- The gem uses proper jv lifecycle management to prevent memory leaks
|
|
174
|
+
- Very large JSON documents (>100MB) may cause high memory usage
|
|
175
|
+
- Deeply nested structures may hit stack limits
|
|
176
|
+
- Complex filters may be slow - there is no timeout mechanism in v1.0
|
|
177
|
+
|
|
178
|
+
## Security
|
|
179
|
+
|
|
180
|
+
See [SECURITY.md](SECURITY.md) for detailed security information.
|
|
181
|
+
|
|
182
|
+
Key points:
|
|
183
|
+
|
|
184
|
+
- JSON input is parsed by jq's parser (no eval/injection risk)
|
|
185
|
+
- Filter expressions cannot execute system commands (safe by design)
|
|
186
|
+
- All inputs are validated for type before processing
|
|
187
|
+
- The extension uses proper memory management to prevent buffer overflows
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
After checking out the repo, run:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
bundle install
|
|
195
|
+
bundle exec rake compile
|
|
196
|
+
bundle exec rake spec
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
To check for memory leaks:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
bundle exec rake compile CFLAGS="-fsanitize=address -g"
|
|
203
|
+
bundle exec rspec spec/memory_spec.rb
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Contributing
|
|
207
|
+
|
|
208
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/persona-id/jq-ruby.
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
213
|
+
|
|
214
|
+
jq itself is licensed under the MIT License. See https://github.com/jqlang/jq for details.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
require "rake/extensiontask"
|
|
6
|
+
|
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
8
|
+
|
|
9
|
+
Rake::ExtensionTask.new("jq_ext") do |ext|
|
|
10
|
+
ext.ext_dir = "ext/jq"
|
|
11
|
+
ext.lib_dir = "lib/jq"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
task spec: :compile
|
|
15
|
+
task default: :spec
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Thread Safety
|
|
4
|
+
|
|
5
|
+
**Status: Likely safe with jq 1.7+, but not officially guaranteed**
|
|
6
|
+
|
|
7
|
+
The jq C library was not originally designed for multi-threading. However, as of jq 1.7 (PR #2546), a critical segfault bug in multi-threaded environments was fixed.
|
|
8
|
+
|
|
9
|
+
**Current Safety Profile:**
|
|
10
|
+
- ✅ Each `JQ.filter` call creates its own isolated `jq_state` (no shared state between calls)
|
|
11
|
+
- ✅ jq 1.7+ fixed the thread-local storage segfault (included in your jq 1.8.1)
|
|
12
|
+
- ✅ Ruby's GVL (MRI) prevents true parallel execution, providing additional serialization
|
|
13
|
+
- ⚠️ jq developers have not made formal thread-safety guarantees beyond fixing the segfault
|
|
14
|
+
- ⚠️ Other global state issues may exist but are unconfirmed
|
|
15
|
+
|
|
16
|
+
**Practical Recommendation:**
|
|
17
|
+
- **Probably safe** to call `JQ.filter` from multiple Ruby threads in MRI Ruby with jq 1.7+
|
|
18
|
+
- **Use caution** with JRuby or TruffleRuby where threads can run truly in parallel
|
|
19
|
+
- **Safest approach** for heavy parallel processing: use separate processes (e.g., `parallel` gem)
|
|
20
|
+
|
|
21
|
+
**Version Requirements:**
|
|
22
|
+
- jq 1.7+ strongly recommended for any multi-threaded use
|
|
23
|
+
- Check your jq version: `jq --version`
|
|
24
|
+
|
|
25
|
+
## Input Validation
|
|
26
|
+
|
|
27
|
+
- **JSON input** is parsed by jq's built-in parser. There is no eval or code injection risk.
|
|
28
|
+
- **Filter expressions** are compiled by jq's compiler. They cannot execute system commands or access the filesystem (safe by design).
|
|
29
|
+
- All inputs are validated for type before processing in the Ruby C extension.
|
|
30
|
+
|
|
31
|
+
## Memory Safety
|
|
32
|
+
|
|
33
|
+
The extension uses proper jv lifecycle management to prevent memory leaks and buffer overflows:
|
|
34
|
+
|
|
35
|
+
- Every `jv` value is tracked and freed appropriately
|
|
36
|
+
- Error paths ensure cleanup before raising Ruby exceptions
|
|
37
|
+
- The jq library's "consume" pattern is followed correctly
|
|
38
|
+
|
|
39
|
+
### Testing for Memory Issues
|
|
40
|
+
|
|
41
|
+
Run tests with AddressSanitizer to detect memory errors:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bundle exec rake compile CFLAGS="-fsanitize=address -g"
|
|
45
|
+
bundle exec rspec
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or use valgrind:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
valgrind --leak-check=full bundle exec rspec
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Run the memory test suite:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
bundle exec rspec spec/memory_spec.rb
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Known Limitations
|
|
61
|
+
|
|
62
|
+
- **Very large JSON documents** (>100MB) may cause high memory usage. The entire document is parsed into memory before processing.
|
|
63
|
+
- **Deeply nested structures** may hit stack limits (typically ~1000 levels depending on system).
|
|
64
|
+
- **Complex filters** may be slow. There is no timeout mechanism in v1.0 - long-running filters will block.
|
|
65
|
+
- **No sandboxing** - While jq filters cannot execute arbitrary code, complex filters can consume significant CPU and memory.
|
|
66
|
+
|
|
67
|
+
## Security Best Practices
|
|
68
|
+
|
|
69
|
+
1. **Validate input size** - If processing untrusted JSON, check the size before passing to `JQ.filter`:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
MAX_JSON_SIZE = 10 * 1024 * 1024 # 10MB
|
|
73
|
+
raise "JSON too large" if json_string.bytesize > MAX_JSON_SIZE
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
2. **Validate filter** - If using user-provided filters, validate them first:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
begin
|
|
80
|
+
JQ.validate_filter!(user_filter)
|
|
81
|
+
rescue JQ::CompileError => e
|
|
82
|
+
# Handle invalid filter
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
3. **Set timeouts** - Use Ruby's `Timeout` module for long-running operations (note: this may not interrupt C code immediately):
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
require 'timeout'
|
|
90
|
+
|
|
91
|
+
begin
|
|
92
|
+
Timeout.timeout(5) do
|
|
93
|
+
JQ.filter(json, filter)
|
|
94
|
+
end
|
|
95
|
+
rescue Timeout::Error
|
|
96
|
+
# Handle timeout
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
4. **Limit nesting depth** - Pre-validate JSON nesting if processing untrusted input:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
def check_nesting_depth(json_string, max_depth = 100)
|
|
104
|
+
depth = 0
|
|
105
|
+
max_seen = 0
|
|
106
|
+
json_string.each_char do |c|
|
|
107
|
+
depth += 1 if c == '{' || c == '['
|
|
108
|
+
depth -= 1 if c == '}' || c == ']'
|
|
109
|
+
max_seen = [max_seen, depth].max
|
|
110
|
+
raise "Nesting too deep" if max_seen > max_depth
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Reporting Security Issues
|
|
116
|
+
|
|
117
|
+
If you discover a security vulnerability, please email security@persona.com with details. Do not open a public issue.
|
|
118
|
+
|
|
119
|
+
## Acknowledgments
|
|
120
|
+
|
|
121
|
+
This gem wraps the jq library (https://github.com/jqlang/jq), which is maintained by the jq authors and community.
|
data/ext/jq/extconf.rb
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
ENV["RC_ARCHS"] = "" if RUBY_PLATFORM.include?("darwin")
|
|
4
|
+
|
|
5
|
+
require 'mkmf'
|
|
6
|
+
require 'mini_portile2'
|
|
7
|
+
|
|
8
|
+
class JQRecipe < MiniPortile
|
|
9
|
+
JQ_VERSION = '1.8.1'
|
|
10
|
+
JQ_SHA256 = '2be64e7129cecb11d5906290eba10af694fb9e3e7f9fc208a311dc33ca837eb0'
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
super('jq', JQ_VERSION)
|
|
14
|
+
self.files << {
|
|
15
|
+
url: "https://github.com/jqlang/jq/releases/download/jq-#{JQ_VERSION}/jq-#{JQ_VERSION}.tar.gz",
|
|
16
|
+
sha256: JQ_SHA256
|
|
17
|
+
}
|
|
18
|
+
self.configure_options += [
|
|
19
|
+
# "--enable-shared",
|
|
20
|
+
"--enable-static",
|
|
21
|
+
"--enable-all-static",
|
|
22
|
+
"--disable-maintainer-mode",
|
|
23
|
+
"--with-oniguruma=builtin", # Use bundled oniguruma
|
|
24
|
+
"CFLAGS=-fPIC -DJQ_VERSION=\\\"#{JQRecipe::JQ_VERSION}\\\"",
|
|
25
|
+
]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def configure
|
|
29
|
+
return if configured?
|
|
30
|
+
|
|
31
|
+
execute('autoreconf', 'autoreconf -i')
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
recipe = JQRecipe.new
|
|
38
|
+
recipe.cook
|
|
39
|
+
recipe.activate
|
|
40
|
+
recipe.mkmf_config(pkg: 'oniguruma', static: 'onig')
|
|
41
|
+
recipe.mkmf_config(pkg: 'libjq', static: 'jq')
|
|
42
|
+
|
|
43
|
+
# Verify we can link against libjq with a test program
|
|
44
|
+
checking_for "ability to link against libjq" do
|
|
45
|
+
# Create a test program that uses core jq functions
|
|
46
|
+
src = <<~SRC
|
|
47
|
+
#include <jv.h>
|
|
48
|
+
#include <jq.h>
|
|
49
|
+
|
|
50
|
+
int main(void) {
|
|
51
|
+
// Test that we can link against jq_init, jq_compile, jv_parse, etc.
|
|
52
|
+
jq_state *jq = jq_init();
|
|
53
|
+
if (!jq) return 1;
|
|
54
|
+
|
|
55
|
+
// Test basic jv operations (thread-safe in jq 1.7+)
|
|
56
|
+
jv input = jv_parse("{}");
|
|
57
|
+
int valid = jv_is_valid(input);
|
|
58
|
+
jv_free(input);
|
|
59
|
+
|
|
60
|
+
jq_teardown(&jq);
|
|
61
|
+
return valid ? 0 : 1;
|
|
62
|
+
}
|
|
63
|
+
SRC
|
|
64
|
+
|
|
65
|
+
# Try to link the test program (don't need to run it)
|
|
66
|
+
try_link(src) or abort "Failed to link against libjq"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Add compiler flags
|
|
70
|
+
$CFLAGS << " -Wall -Wextra -Wno-unused-parameter -fPIC"
|
|
71
|
+
|
|
72
|
+
# Enable debug symbols if requested
|
|
73
|
+
if ENV['DEBUG']
|
|
74
|
+
$CFLAGS << " -g -O0"
|
|
75
|
+
else
|
|
76
|
+
$CFLAGS << " -O2"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
create_makefile('jq/jq_ext')
|