Skip to content

Commit 1cbd2c7

Browse files
committed
Finish 0.1.0
2 parents 2523666 + 7bd884e commit 1cbd2c7

26 files changed

+419
-3
lines changed

.gitignore

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.DS_Store
12
*.gem
23
*.rbc
34
/.config
@@ -9,6 +10,7 @@
910
/test/tmp/
1011
/test/version_tmp/
1112
/tmp/
13+
benchmark/
1214

1315
# Used by dotenv library to load environment variables.
1416
# .env
@@ -42,9 +44,11 @@ build-iPhoneSimulator/
4244

4345
# for a library or gem, you might want to ignore these files since the code is
4446
# intended to run in multiple environments; otherwise, check them in:
45-
# Gemfile.lock
46-
# .ruby-version
47-
# .ruby-gemset
47+
Gemfile.lock
48+
.ruby-version
49+
.ruby-gemset
4850

4951
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
5052
.rvmrc
53+
/.byebug_history
54+
/.yardopts

.travis.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
language: ruby
2+
bundler_args: --without debug
3+
script: "bundle exec rspec spec"
4+
before_install:
5+
- 'gem update --system --conservative || (gem i "rubygems-update:~>2.7" --no-document && update_rubygems)'
6+
- 'gem update bundler --conservative'
7+
env:
8+
- CI=true
9+
rvm:
10+
- 2.2.2
11+
- 2.3
12+
- 2.4
13+
- 2.5
14+
- 2.6
15+
- jruby
16+
- rbx-3
17+
cache: bundler
18+
sudo: false
19+
matrix:
20+
allow_failures:
21+
- rvm: jruby
22+
- rvm: rbx-3
23+
dist: trusty

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Gregg Kellogg <[email protected]>

Gemfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
source "https://rubygems.org"
2+
gemspec
3+
4+
group :development, :test do
5+
gem 'simplecov', require: false, platform: :mri
6+
gem 'coveralls', require: false, platform: :mri
7+
gem 'benchmark-ips'
8+
gem 'rake'
9+
end
10+
11+
group :debug do
12+
gem "byebug", platforms: :mri
13+
end

README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,92 @@
11
# json-canonicalization
22
An implementation of the JSON Canonicalization Scheme for Ruby
3+
4+
Implements version 5 of [draft-rundgren-json-canonicalization-scheme-05](https://tools.ietf.org/html/draft-rundgren-json-canonicalization-scheme-05#page-5).
5+
6+
[![Gem Version](https://badge.fury.io/rb/json-canonicalization.png)](http://badge.fury.io/rb/json-canonicalization)
7+
[![Build Status](https://travis-ci.org/dryruby/json-canonicalization.png?branch=master)](http://travis-ci.org/dryruby/json-canonicalization)
8+
[![Coverage Status](https://coveralls.io/repos/dryruby/json-canonicalization/badge.svg)](https://coveralls.io/r/dryruby/json-canonicalization)
9+
10+
# Description
11+
12+
Cryptographic operations like hashing and signing depend on that the target
13+
data does not change during serialization, transport, or parsing.
14+
By applying the rules defined by JCS (JSON Canonicalization Scheme),
15+
data provided in the JSON [[RFC8259](https://tools.ietf.org/html/rfc8259)]
16+
format can be exchanged "as is", while still being subject to secure cryptographic operations.
17+
JCS achieves this by building on the serialization formats for JSON
18+
primitives as defined by ECMAScript [[ES6](https://www.ecma-international.org/ecma-262/6.0/index.html)],
19+
constraining JSON data to the<br>I-JSON [[RFC7493](https://tools.ietf.org/html//rfc7493)] subset,
20+
and through a platform independent property sorting scheme.
21+
22+
Working document: https://cyberphone.github.io/ietf-json-canon<br>
23+
Published IETF Draft: https://tools.ietf.org/html/draft-rundgren-json-canonicalization-scheme-05
24+
25+
The JSON Canonicalization Scheme concept in a nutshell:
26+
- Serialization of primitive JSON data types using methods compatible with ECMAScript's `JSON.stringify()`
27+
- Lexicographic sorting of JSON `Object` properties in a *recursive* process
28+
- JSON `Array` data is also subject to canonicalization, *but element order remains untouched*
29+
30+
### Sample Input:
31+
```code
32+
{
33+
"numbers": [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001],
34+
"string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
35+
"literals": [null, true, false]
36+
}
37+
```
38+
### Expected Output:
39+
```code
40+
{"literals":[null,true,false],"numbers":[333333333.3333333,1e+30,4.5,0.002,1e-27],"string":"€$\u000f\nA'B\"\\\\\"/"}
41+
```
42+
## Usage
43+
The library accepts Ruby input and generates canonical JSON via the `#to_json_c14n` method. This is based on the standard JSON gem's version of `#to_json` with overloads for `Hash`, `String` and `Numeric`
44+
45+
```ruby
46+
data = {
47+
"numbers" => [
48+
333333333.3333333,
49+
1.0e+30,
50+
4.5,
51+
0.002,
52+
1.0e-27
53+
],
54+
"string" => "€$\u000F\nA'B\"\\\\\"/",
55+
"literals" => [nil, true, false]
56+
}
57+
58+
puts data.to_json_c14n
59+
=>
60+
```
61+
62+
## Documentation
63+
Full documentation available on [RubyDoc](http://rubydoc.info/gems/json-canonicalization/file/README.md)
64+
65+
### Principal Classes
66+
* {JSON::Canonicalization}
67+
68+
## Dependencies
69+
* [Ruby](http://ruby-lang.org/) (>= 2.2.2)
70+
* [JSON](https://rubygems.org/gems/json) (>= 2.1)
71+
72+
## Author
73+
* [Gregg Kellogg](http://github.com/gkellogg) - <http://kellogg-assoc.com/>
74+
75+
## Contributing
76+
* Do your best to adhere to the existing coding conventions and idioms.
77+
* Don't use hard tabs, and don't leave trailing whitespace on any line.
78+
* Do document every method you add using [YARD][] annotations. Read the
79+
[tutorial][YARD-GS] or just look at the existing code for examples.
80+
* Don't touch the `json-ld.gemspec`, `VERSION` or `AUTHORS` files. If you need to
81+
change them, do so on your private branch only.
82+
* Do feel free to add yourself to the `CREDITS` file and the corresponding
83+
list in the the `README`. Alphabetical order applies.
84+
* Do note that in order for us to merge any non-trivial changes (as a rule
85+
of thumb, additions larger than about 15 lines of code), we need an
86+
explicit [public domain dedication][PDD] on record from you.
87+
88+
##License
89+
90+
This is free and unencumbered public domain software. For more information,
91+
see <http://unlicense.org/> or the accompanying {file:UNLICENSE} file.
92+

Rakefile

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env ruby
2+
$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), 'lib')))
3+
require 'rubygems'
4+
5+
namespace :gem do
6+
desc "Build the json-canonicalization-#{File.read('VERSION').chomp}.gem file"
7+
task :build do
8+
sh "gem build json-canonicalization.gemspec && mv json-canonicalization-#{File.read('VERSION').chomp}.gem pkg/"
9+
end
10+
11+
desc "Release the json-canonicalization-#{File.read('VERSION').chomp}.gem file"
12+
task :release do
13+
sh "gem push pkg/json-canonicalization-#{File.read('VERSION').chomp}.gem"
14+
end
15+
end
16+
17+
desc 'Default: run specs.'
18+
task default: :spec
19+
task specs: :spec
20+
21+
require 'rspec/core/rake_task'
22+
desc 'Run specifications'
23+
RSpec::Core::RakeTask.new do |spec|
24+
spec.rspec_opts = %w(--options spec/spec.opts) if File.exists?('spec/spec.opts')
25+
end
26+
27+
desc "Run specifications for continuous integration"
28+
RSpec::Core::RakeTask.new("spec:ci") do |spec|
29+
spec.rspec_opts = %w(--options spec/spec.opts) if File.exists?('spec/spec.opts')
30+
end

VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.1.0

examples/c14n.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"numbers": [333333333.33333329, 1E30, 4.50,
3+
2e-3, 0.000000000000000000000000001],
4+
"string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
5+
"literals": [null, true, false]
6+
}

json-canonicalization.gemspec

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env ruby -rubygems
2+
# -*- encoding: utf-8 -*-
3+
4+
Gem::Specification.new do |gem|
5+
gem.version = File.read('VERSION').chomp
6+
gem.date = File.mtime('VERSION').strftime('%Y-%m-%d')
7+
8+
gem.name = "json-canonicalization"
9+
gem.homepage = "http://github.com/dryruby/json-canonicalization"
10+
gem.license = 'Unlicense'
11+
gem.summary = "JSON Canonicalization for Ruby."
12+
gem.description = "JSON::Canonicalization generates canonical JSON output from Ruby objects."
13+
14+
gem.authors = ['Gregg Kellogg']
15+
16+
gem.platform = Gem::Platform::RUBY
17+
gem.files = %w(AUTHORS README.md LICENSE VERSION) + Dir.glob('lib/**/*.rb')
18+
gem.test_files = Dir.glob('spec/**/*.rb') + Dir.glob('spec/**/*.json')
19+
20+
gem.required_ruby_version = '>= 2.2.2'
21+
gem.requirements = []
22+
gem.add_development_dependency 'rspec', '~> 3.8'
23+
gem.add_development_dependency 'yard' , '~> 0.9'
24+
25+
gem.post_install_message = nil
26+
end

lib/json/canonicalization.rb

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# -*- encoding: utf-8 -*-
2+
# frozen_string_literal: true
3+
$:.unshift(File.expand_path("../ld", __FILE__))
4+
require 'json'
5+
6+
module JSON
7+
##
8+
# `JSON::Canonicalization` generates canonical JSON output from Ruby objects
9+
module Canonicalization
10+
autoload :VERSION, 'json/ld/version'
11+
end
12+
end
13+
14+
class Object
15+
# Default canonicalization output for Ruby objects
16+
# @return [String]
17+
def to_json_c14n
18+
self.to_json
19+
end
20+
end
21+
22+
class Array
23+
def to_json_c14n
24+
'[' + self.map(&:to_json_c14n).join(',') + ']'
25+
end
26+
end
27+
28+
class Numeric
29+
def to_json_c14n
30+
raise RangeError if self.is_a?(Float) && (self.nan? || self.infinite?)
31+
return "0" if self.zero?
32+
num = self
33+
if num < 0
34+
num, sign = -num, '-'
35+
end
36+
native_rep = "%.15E" % num
37+
decimal, exponential = native_rep.split('E')
38+
exp_val = exponential.to_i
39+
exponential = exp_val > 0 ? ('+' + exp_val.to_s) : exp_val.to_s
40+
41+
integral, fractional = decimal.split('.')
42+
fractional = fractional.sub(/0+$/, '') # Remove trailing zeros
43+
44+
if exp_val > 0 && exp_val < 21
45+
while exp_val > 0
46+
integral += fractional.to_s[0] || '0'
47+
fractional = fractional.to_s[1..-1]
48+
exp_val -= 1
49+
end
50+
exponential = nil
51+
elsif exp_val == 0
52+
exponential = nil
53+
elsif exp_val < 0 && exp_val > -7
54+
# Small numbers are shown as 0.etc with e-6 as lower limit
55+
fractional, integral, exponential = integral + fractional.to_s, '0', nil
56+
fractional = ("0" * (-exp_val - 1)) + fractional
57+
end
58+
59+
fractional = nil if fractional.to_s.empty?
60+
sign.to_s + integral + (fractional ? ".#{fractional}" : '') + (exponential ? "e#{exponential}" : '')
61+
end
62+
end
63+
64+
class Hash
65+
# Output JSON with keys sorted lexicographically
66+
# @return [String]
67+
def to_json_c14n
68+
"{" + self.
69+
keys.
70+
sort_by {|k| k.encode(Encoding::UTF_16)}.
71+
map {|k| k.to_json_c14n + ':' + self[k].to_json_c14n}
72+
.join(',') +
73+
'}'
74+
end
75+
end
76+
77+
class String
78+
# Output JSON with control characters escaped
79+
# @return [String]
80+
def to_json_c14n
81+
self.to_json
82+
end
83+
end

0 commit comments

Comments
 (0)