pogo 2.31.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.
Files changed (93) hide show
  1. data/README.md +73 -0
  2. data/bin/pogo +22 -0
  3. data/data/cacert.pem +3988 -0
  4. data/lib/heroku.rb +22 -0
  5. data/lib/heroku/auth.rb +320 -0
  6. data/lib/heroku/cli.rb +38 -0
  7. data/lib/heroku/client.rb +764 -0
  8. data/lib/heroku/client/heroku_postgresql.rb +111 -0
  9. data/lib/heroku/client/pgbackups.rb +113 -0
  10. data/lib/heroku/client/rendezvous.rb +105 -0
  11. data/lib/heroku/client/ssl_endpoint.rb +25 -0
  12. data/lib/heroku/command.rb +273 -0
  13. data/lib/heroku/command/account.rb +23 -0
  14. data/lib/heroku/command/accounts.rb +34 -0
  15. data/lib/heroku/command/addons.rb +305 -0
  16. data/lib/heroku/command/apps.rb +311 -0
  17. data/lib/heroku/command/auth.rb +86 -0
  18. data/lib/heroku/command/base.rb +230 -0
  19. data/lib/heroku/command/certs.rb +148 -0
  20. data/lib/heroku/command/config.rb +137 -0
  21. data/lib/heroku/command/db.rb +218 -0
  22. data/lib/heroku/command/domains.rb +85 -0
  23. data/lib/heroku/command/drains.rb +46 -0
  24. data/lib/heroku/command/git.rb +65 -0
  25. data/lib/heroku/command/help.rb +163 -0
  26. data/lib/heroku/command/keys.rb +115 -0
  27. data/lib/heroku/command/labs.rb +161 -0
  28. data/lib/heroku/command/logs.rb +98 -0
  29. data/lib/heroku/command/maintenance.rb +61 -0
  30. data/lib/heroku/command/pg.rb +277 -0
  31. data/lib/heroku/command/pgbackups.rb +289 -0
  32. data/lib/heroku/command/plugins.rb +110 -0
  33. data/lib/heroku/command/ps.rb +232 -0
  34. data/lib/heroku/command/releases.rb +124 -0
  35. data/lib/heroku/command/run.rb +179 -0
  36. data/lib/heroku/command/sharing.rb +89 -0
  37. data/lib/heroku/command/ssl.rb +61 -0
  38. data/lib/heroku/command/stack.rb +62 -0
  39. data/lib/heroku/command/status.rb +51 -0
  40. data/lib/heroku/command/update.rb +47 -0
  41. data/lib/heroku/command/version.rb +23 -0
  42. data/lib/heroku/deprecated.rb +5 -0
  43. data/lib/heroku/deprecated/help.rb +38 -0
  44. data/lib/heroku/distribution.rb +9 -0
  45. data/lib/heroku/helpers.rb +517 -0
  46. data/lib/heroku/helpers/heroku_postgresql.rb +104 -0
  47. data/lib/heroku/plugin.rb +161 -0
  48. data/lib/heroku/updater.rb +158 -0
  49. data/lib/heroku/version.rb +3 -0
  50. data/lib/vendor/heroku/okjson.rb +598 -0
  51. data/spec/helper/legacy_help.rb +16 -0
  52. data/spec/heroku/auth_spec.rb +246 -0
  53. data/spec/heroku/client/heroku_postgresql_spec.rb +34 -0
  54. data/spec/heroku/client/pgbackups_spec.rb +43 -0
  55. data/spec/heroku/client/rendezvous_spec.rb +62 -0
  56. data/spec/heroku/client/ssl_endpoint_spec.rb +48 -0
  57. data/spec/heroku/client_spec.rb +564 -0
  58. data/spec/heroku/command/addons_spec.rb +585 -0
  59. data/spec/heroku/command/apps_spec.rb +351 -0
  60. data/spec/heroku/command/auth_spec.rb +38 -0
  61. data/spec/heroku/command/base_spec.rb +109 -0
  62. data/spec/heroku/command/certs_spec.rb +178 -0
  63. data/spec/heroku/command/config_spec.rb +144 -0
  64. data/spec/heroku/command/db_spec.rb +110 -0
  65. data/spec/heroku/command/domains_spec.rb +87 -0
  66. data/spec/heroku/command/drains_spec.rb +34 -0
  67. data/spec/heroku/command/git_spec.rb +116 -0
  68. data/spec/heroku/command/help_spec.rb +93 -0
  69. data/spec/heroku/command/keys_spec.rb +120 -0
  70. data/spec/heroku/command/labs_spec.rb +99 -0
  71. data/spec/heroku/command/logs_spec.rb +60 -0
  72. data/spec/heroku/command/maintenance_spec.rb +51 -0
  73. data/spec/heroku/command/pg_spec.rb +223 -0
  74. data/spec/heroku/command/pgbackups_spec.rb +280 -0
  75. data/spec/heroku/command/plugins_spec.rb +104 -0
  76. data/spec/heroku/command/ps_spec.rb +195 -0
  77. data/spec/heroku/command/releases_spec.rb +130 -0
  78. data/spec/heroku/command/run_spec.rb +86 -0
  79. data/spec/heroku/command/sharing_spec.rb +59 -0
  80. data/spec/heroku/command/ssl_spec.rb +32 -0
  81. data/spec/heroku/command/stack_spec.rb +46 -0
  82. data/spec/heroku/command/status_spec.rb +48 -0
  83. data/spec/heroku/command/version_spec.rb +16 -0
  84. data/spec/heroku/command_spec.rb +211 -0
  85. data/spec/heroku/helpers/heroku_postgresql_spec.rb +109 -0
  86. data/spec/heroku/helpers_spec.rb +48 -0
  87. data/spec/heroku/plugin_spec.rb +172 -0
  88. data/spec/heroku/updater_spec.rb +44 -0
  89. data/spec/spec.opts +1 -0
  90. data/spec/spec_helper.rb +209 -0
  91. data/spec/support/display_message_matcher.rb +49 -0
  92. data/spec/support/openssl_mock_helper.rb +8 -0
  93. metadata +220 -0
@@ -0,0 +1,104 @@
1
+ require "heroku/helpers"
2
+
3
+ module Heroku::Helpers::HerokuPostgresql
4
+
5
+ extend self
6
+ extend Heroku::Helpers
7
+
8
+ def app_config_vars
9
+ @app_config_vars ||= api.get_config_vars(app).body
10
+ end
11
+
12
+ def hpg_addon_name
13
+ ENV['HEROKU_POSTGRESQL_ADDON_NAME'] || 'heroku-postgresql'
14
+ end
15
+
16
+ def hpg_addon_prefix
17
+ ENV["HEROKU_POSTGRESQL_ADDON_PREFIX"] || "HEROKU_POSTGRESQL"
18
+ end
19
+
20
+ def hpg_databases
21
+ @hpg_databases ||= app_config_vars.inject({}) do |hash, (name, url)|
22
+ if name =~ /^(#{hpg_addon_prefix}\w+)_URL$/
23
+ hash.update($1 => url)
24
+ elsif name == 'SHARED_DATABASE_URL'
25
+ hash.update('SHARED_DATABASE' => url)
26
+ end
27
+ hash
28
+ end
29
+ end
30
+
31
+ def forget_config!
32
+ @hpg_databases = nil
33
+ @app_config_vars = nil
34
+ end
35
+
36
+ def hpg_resolve(name, default=nil)
37
+ uri = URI.parse(name) rescue nil
38
+ if uri && uri.scheme
39
+ [nil, name]
40
+ else
41
+ if app_config_vars["DATABASE_URL"]
42
+ hpg_databases["DATABASE"] = app_config_vars["DATABASE_URL"]
43
+ end
44
+ if hpg_databases.empty?
45
+ error("Your app has no databases.")
46
+ end
47
+
48
+ name = name.to_s.upcase.gsub(/_URL$/, "")
49
+
50
+ if hpg_databases[name]
51
+ [hpg_pretty_name(name), hpg_databases[name]]
52
+ elsif (config_var = "HEROKU_POSTGRESQL_#{name}") && hpg_databases[config_var]
53
+ [hpg_pretty_name(config_var), hpg_databases[config_var]]
54
+ elsif default && name.empty? && app_config_vars[default]
55
+ [hpg_pretty_name(default), app_config_vars[default]]
56
+ elsif name.empty?
57
+ error("Unknown database. Valid options are: #{hpg_databases.keys.sort.join(", ")}")
58
+ else
59
+ error("Unknown database: #{name}. Valid options are: #{hpg_databases.keys.sort.join(", ")}")
60
+ end
61
+ end
62
+ end
63
+
64
+ def hpg_translate_fork_and_follow(addon, config)
65
+ if addon =~ /^#{hpg_addon_name}/
66
+ %w[fork follow].each do |opt|
67
+ if val = config[opt]
68
+ unless val.is_a?(String)
69
+ error("--#{opt} requires a database argument")
70
+ end
71
+ name, url = hpg_resolve(val)
72
+ config[opt] = url
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def hpg_pretty_name(name)
81
+ if ['DATABASE', 'DATABASE_URL'].include?(name)
82
+ if key = app_config_vars.keys.detect do |key|
83
+ if key == 'DATABASE_URL'
84
+ next
85
+ else
86
+ app_config_vars[key] == app_config_vars['DATABASE_URL']
87
+ end
88
+ end
89
+ "#{key.gsub(/_URL$/, '')} (DATABASE_URL)"
90
+ else
91
+ name.gsub(/_URL$/, '')
92
+ end
93
+ elsif hpg_databases[name] == hpg_databases['DATABASE']
94
+ "#{name} (DATABASE_URL)"
95
+ else
96
+ name
97
+ end
98
+ end
99
+
100
+ def hpg_promote(url)
101
+ api.put_config_vars(app, "DATABASE_URL" => url)
102
+ end
103
+
104
+ end
@@ -0,0 +1,161 @@
1
+ # based on the Rails Plugin
2
+
3
+ module Heroku
4
+ class Plugin
5
+ include Heroku::Helpers
6
+ extend Heroku::Helpers
7
+
8
+ class ErrorUpdatingSymlinkPlugin < StandardError; end
9
+
10
+ DEPRECATED_PLUGINS = %w(
11
+ heroku-cedar
12
+ heroku-certs
13
+ heroku-credentials
14
+ heroku-kill
15
+ heroku-labs
16
+ heroku-logging
17
+ heroku-netrc
18
+ heroku-pgdumps
19
+ heroku-postgresql
20
+ heroku-releases
21
+ heroku-shared-postgresql
22
+ heroku-status
23
+ heroku-stop
24
+ heroku-suggest
25
+ pgbackups-automate
26
+ pgcmd
27
+ )
28
+
29
+ attr_reader :name, :uri
30
+
31
+ def self.directory
32
+ File.expand_path("#{home_directory}/.heroku/plugins")
33
+ end
34
+
35
+ def self.list
36
+ Dir["#{directory}/*"].sort.map do |folder|
37
+ File.basename(folder)
38
+ end
39
+ end
40
+
41
+ def self.load!
42
+ list.each do |plugin|
43
+ check_for_deprecation(plugin)
44
+ next if skip_plugins.include?(plugin)
45
+ load_plugin(plugin)
46
+ end
47
+ # check to see if we are using ddollar/heroku-accounts
48
+ if list.include?('heroku-accounts') && Heroku::Auth.methods.include?(:fetch_from_account)
49
+ # setup netrc to match the default, if one exists
50
+ if default_account = %x{ git config heroku.account }.chomp
51
+ account = Heroku::Auth.extract_account rescue nil
52
+ if account && Heroku::Auth.read_credentials != [Heroku::Auth.user, Heroku::Auth.password]
53
+ Heroku::Auth.credentials = [Heroku::Auth.user, Heroku::Auth.password]
54
+ Heroku::Auth.write_credentials
55
+ load("#{File.dirname(__FILE__)}/command/accounts.rb")
56
+ # kill memoization in case '--account' was passed
57
+ Heroku::Auth.instance_variable_set(:@account, nil)
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ def self.load_plugin(plugin)
64
+ begin
65
+ folder = "#{self.directory}/#{plugin}"
66
+ $: << "#{folder}/lib" if File.directory? "#{folder}/lib"
67
+ load "#{folder}/init.rb" if File.exists? "#{folder}/init.rb"
68
+ rescue ScriptError, StandardError => error
69
+ styled_error(error, "Unable to load plugin #{plugin}.")
70
+ false
71
+ end
72
+ end
73
+
74
+ def self.remove_plugin(plugin)
75
+ FileUtils.rm_rf("#{self.directory}/#{plugin}")
76
+ end
77
+
78
+ def self.check_for_deprecation(plugin)
79
+ return unless STDIN.isatty
80
+
81
+ if DEPRECATED_PLUGINS.include?(plugin)
82
+ if confirm "The plugin #{plugin} has been deprecated. Would you like to remove it? (y/N)"
83
+ remove_plugin(plugin)
84
+ end
85
+ end
86
+ end
87
+
88
+ def self.skip_plugins
89
+ @skip_plugins ||= ENV["SKIP_PLUGINS"].to_s.split(/[ ,]/)
90
+ end
91
+
92
+ def initialize(uri)
93
+ @uri = uri
94
+ guess_name(uri)
95
+ end
96
+
97
+ def to_s
98
+ name
99
+ end
100
+
101
+ def path
102
+ "#{self.class.directory}/#{name}"
103
+ end
104
+
105
+ def install
106
+ if File.directory?(path)
107
+ uninstall
108
+ end
109
+ FileUtils.mkdir_p(self.class.directory)
110
+ Dir.chdir(self.class.directory) do
111
+ git("clone #{uri}")
112
+ unless $?.success?
113
+ FileUtils.rm_rf path
114
+ return false
115
+ end
116
+ end
117
+ true
118
+ end
119
+
120
+ def uninstall
121
+ ensure_plugin_exists
122
+ FileUtils.rm_r(path)
123
+ end
124
+
125
+ def update
126
+ ensure_plugin_exists
127
+ if File.symlink?(path)
128
+ raise Heroku::Plugin::ErrorUpdatingSymlinkPlugin
129
+ else
130
+ Dir.chdir(path) do
131
+ unless git('config --get branch.master.remote').empty?
132
+ message = git("pull")
133
+ unless $?.success?
134
+ error("Unable to update #{name}.\n" + message)
135
+ end
136
+ else
137
+ error(<<-ERROR)
138
+ #{name} is a legacy plugin installation.
139
+ Enable updating by reinstalling with `heroku plugins:install`.
140
+ ERROR
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def ensure_plugin_exists
149
+ unless File.directory?(path)
150
+ error("#{name} plugin not found.")
151
+ end
152
+ end
153
+
154
+ def guess_name(url)
155
+ @name = File.basename(url)
156
+ @name = File.basename(File.dirname(url)) if @name.empty?
157
+ @name.gsub!(/\.git$/, '') if @name =~ /\.git$/
158
+ end
159
+
160
+ end
161
+ end
@@ -0,0 +1,158 @@
1
+ require "fileutils"
2
+
3
+ require 'heroku/helpers'
4
+
5
+ module Heroku
6
+ module Updater
7
+
8
+ def self.autoupdating_path
9
+ File.join(Heroku::Helpers.home_directory, ".heroku", "autoupdating")
10
+ end
11
+
12
+ def self.installed_client_path
13
+ File.expand_path("../../..", __FILE__)
14
+ end
15
+
16
+ def self.updated_client_path
17
+ File.join(Heroku::Helpers.home_directory, ".heroku", "client")
18
+ end
19
+
20
+ def self.latest_local_version
21
+ installed_version = client_version_from_path(installed_client_path)
22
+ updated_version = client_version_from_path(updated_client_path)
23
+ if compare_versions(updated_version, installed_version) > 0
24
+ updated_version
25
+ else
26
+ installed_version
27
+ end
28
+ end
29
+
30
+ def self.client_version_from_path(path)
31
+ version_file = File.join(path, "lib/heroku/version.rb")
32
+ if File.exists?(version_file)
33
+ File.read(version_file).match(/VERSION = "([^"]+)"/)[1]
34
+ else
35
+ '0.0.0'
36
+ end
37
+ end
38
+
39
+ def self.disable(message=nil)
40
+ @disable = message if message
41
+ @disable
42
+ end
43
+
44
+ def self.check_disabled!
45
+ if disable
46
+ Heroku::Helpers.error(disable)
47
+ end
48
+ end
49
+
50
+ def self.update(url, autoupdate=false)
51
+ if ENV['HEROKU_AUTOUPDATE'] == 'true'
52
+ if File.exists?(autoupdating_path)
53
+ Heroku::Helpers.error('autoupdate in progress')
54
+ end
55
+ end
56
+
57
+ require "excon"
58
+ require "heroku"
59
+ require "tmpdir"
60
+ require "zip/zip"
61
+
62
+ latest_version = Heroku::Helpers.json_decode(Excon.get('http://rubygems.org/api/v1/gems/heroku.json', :nonblock => false).body)['version']
63
+
64
+ if compare_versions(latest_version, latest_local_version) > 0
65
+ Dir.mktmpdir do |download_dir|
66
+
67
+ # follow redirect, if one exists
68
+ headers = Excon.head(
69
+ url,
70
+ :headers => {
71
+ 'User-Agent' => Heroku.user_agent
72
+ },
73
+ :nonblack => false
74
+ ).headers
75
+ if headers['Location']
76
+ url = headers['Location']
77
+ end
78
+
79
+ File.open("#{download_dir}/heroku.zip", "wb") do |file|
80
+ file.print Excon.get(url, :nonblock => false).body
81
+ end
82
+
83
+ Zip::ZipFile.open("#{download_dir}/heroku.zip") do |zip|
84
+ zip.each do |entry|
85
+ target = File.join(download_dir, entry.to_s)
86
+ FileUtils.mkdir_p File.dirname(target)
87
+ zip.extract(entry, target) { true }
88
+ end
89
+ end
90
+
91
+ FileUtils.rm "#{download_dir}/heroku.zip"
92
+
93
+ old_version = latest_local_version
94
+ new_version = client_version_from_path(download_dir)
95
+
96
+ if compare_versions(new_version, old_version) < 0 && !autoupdate
97
+ Heroku::Helpers.error("Installed version (#{old_version}) is newer than the latest available update (#{new_version})")
98
+ end
99
+
100
+ FileUtils.rm_rf updated_client_path
101
+ FileUtils.mkdir_p File.dirname(updated_client_path)
102
+ FileUtils.cp_r download_dir, updated_client_path
103
+
104
+ new_version
105
+ end
106
+ else
107
+ false # already up to date
108
+ end
109
+ ensure
110
+ FileUtils.rm_f(autoupdating_path)
111
+ end
112
+
113
+ def self.compare_versions(first_version, second_version)
114
+ first_version.split('.').map {|part| Integer(part) rescue part} <=> second_version.split('.').map {|part| Integer(part) rescue part}
115
+ end
116
+
117
+ def self.inject_libpath
118
+ old_version = client_version_from_path(installed_client_path)
119
+ new_version = client_version_from_path(updated_client_path)
120
+
121
+ if compare_versions(new_version, old_version) > 0
122
+ $:.unshift File.join(updated_client_path, "lib")
123
+ vendored_gems = Dir[File.join(updated_client_path, "vendor", "gems", "*")]
124
+ vendored_gems.each do |vendored_gem|
125
+ $:.unshift File.join(vendored_gem, "lib")
126
+ end
127
+ load('heroku/updater.rb') # reload updated updater
128
+ end
129
+
130
+ background_update!
131
+ end
132
+
133
+ def self.autoupdate?
134
+ !@disable && File.exists?(File.join(Heroku::Helpers.home_directory, ".heroku", "autoupdate"))
135
+ end
136
+
137
+ def self.background_update!
138
+ # default autoupdate for users with no plugins who haven't updated before
139
+ unless File.exists?(File.join(Heroku::Helpers.home_directory, '.heroku'))
140
+ FileUtils.mkdir_p(File.join(Heroku::Helpers.home_directory, '.heroku'))
141
+ FileUtils.touch(File.join(Heroku::Helpers.home_directory, '.heroku', 'autoupdate'))
142
+ end
143
+ if autoupdate? && !File.exists?(autoupdating_path)
144
+ FileUtils.touch(autoupdating_path)
145
+ log_path = File.join(Heroku::Helpers.home_directory, '.heroku', 'autoupdate.log')
146
+ pid = if defined?(RUBY_VERSION) and RUBY_VERSION =~ /^1\.8\.\d+/
147
+ fork do
148
+ ENV['HEROKU_AUTOUPDATE'] = 'true'
149
+ exec("heroku update &> #{log_path}")
150
+ end
151
+ else
152
+ spawn(ENV.to_hash.merge({'HEROKU_AUTOUPDATE' => 'true'}), "heroku update", {:err => :out, :out => log_path})
153
+ end
154
+ Process.detach(pid)
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,3 @@
1
+ module Heroku
2
+ VERSION = "2.31.2"
3
+ end
@@ -0,0 +1,598 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright 2011, 2012 Keith Rarick
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
13
+ # all 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
21
+ # THE SOFTWARE.
22
+
23
+ # See https://github.com/kr/okjson for updates.
24
+
25
+ require 'stringio'
26
+
27
+ # Some parts adapted from
28
+ # http://golang.org/src/pkg/json/decode.go and
29
+ # http://golang.org/src/pkg/utf8/utf8.go
30
+ module Heroku
31
+ module OkJson
32
+ extend self
33
+
34
+
35
+ # Decodes a json document in string s and
36
+ # returns the corresponding ruby value.
37
+ # String s must be valid UTF-8. If you have
38
+ # a string in some other encoding, convert
39
+ # it first.
40
+ #
41
+ # String values in the resulting structure
42
+ # will be UTF-8.
43
+ def decode(s)
44
+ ts = lex(s)
45
+ v, ts = textparse(ts)
46
+ if ts.length > 0
47
+ raise Error, 'trailing garbage'
48
+ end
49
+ v
50
+ end
51
+
52
+
53
+ # Parses a "json text" in the sense of RFC 4627.
54
+ # Returns the parsed value and any trailing tokens.
55
+ # Note: this is almost the same as valparse,
56
+ # except that it does not accept atomic values.
57
+ def textparse(ts)
58
+ if ts.length < 0
59
+ raise Error, 'empty'
60
+ end
61
+
62
+ typ, _, val = ts[0]
63
+ case typ
64
+ when '{' then objparse(ts)
65
+ when '[' then arrparse(ts)
66
+ else
67
+ raise Error, "unexpected #{val.inspect}"
68
+ end
69
+ end
70
+
71
+
72
+ # Parses a "value" in the sense of RFC 4627.
73
+ # Returns the parsed value and any trailing tokens.
74
+ def valparse(ts)
75
+ if ts.length < 0
76
+ raise Error, 'empty'
77
+ end
78
+
79
+ typ, _, val = ts[0]
80
+ case typ
81
+ when '{' then objparse(ts)
82
+ when '[' then arrparse(ts)
83
+ when :val,:str then [val, ts[1..-1]]
84
+ else
85
+ raise Error, "unexpected #{val.inspect}"
86
+ end
87
+ end
88
+
89
+
90
+ # Parses an "object" in the sense of RFC 4627.
91
+ # Returns the parsed value and any trailing tokens.
92
+ def objparse(ts)
93
+ ts = eat('{', ts)
94
+ obj = {}
95
+
96
+ if ts[0][0] == '}'
97
+ return obj, ts[1..-1]
98
+ end
99
+
100
+ k, v, ts = pairparse(ts)
101
+ obj[k] = v
102
+
103
+ if ts[0][0] == '}'
104
+ return obj, ts[1..-1]
105
+ end
106
+
107
+ loop do
108
+ ts = eat(',', ts)
109
+
110
+ k, v, ts = pairparse(ts)
111
+ obj[k] = v
112
+
113
+ if ts[0][0] == '}'
114
+ return obj, ts[1..-1]
115
+ end
116
+ end
117
+ end
118
+
119
+
120
+ # Parses a "member" in the sense of RFC 4627.
121
+ # Returns the parsed values and any trailing tokens.
122
+ def pairparse(ts)
123
+ (typ, _, k), ts = ts[0], ts[1..-1]
124
+ if typ != :str
125
+ raise Error, "unexpected #{k.inspect}"
126
+ end
127
+ ts = eat(':', ts)
128
+ v, ts = valparse(ts)
129
+ [k, v, ts]
130
+ end
131
+
132
+
133
+ # Parses an "array" in the sense of RFC 4627.
134
+ # Returns the parsed value and any trailing tokens.
135
+ def arrparse(ts)
136
+ ts = eat('[', ts)
137
+ arr = []
138
+
139
+ if ts[0][0] == ']'
140
+ return arr, ts[1..-1]
141
+ end
142
+
143
+ v, ts = valparse(ts)
144
+ arr << v
145
+
146
+ if ts[0][0] == ']'
147
+ return arr, ts[1..-1]
148
+ end
149
+
150
+ loop do
151
+ ts = eat(',', ts)
152
+
153
+ v, ts = valparse(ts)
154
+ arr << v
155
+
156
+ if ts[0][0] == ']'
157
+ return arr, ts[1..-1]
158
+ end
159
+ end
160
+ end
161
+
162
+
163
+ def eat(typ, ts)
164
+ if ts[0][0] != typ
165
+ raise Error, "expected #{typ} (got #{ts[0].inspect})"
166
+ end
167
+ ts[1..-1]
168
+ end
169
+
170
+
171
+ # Scans s and returns a list of json tokens,
172
+ # excluding white space (as defined in RFC 4627).
173
+ def lex(s)
174
+ ts = []
175
+ while s.length > 0
176
+ typ, lexeme, val = tok(s)
177
+ if typ == nil
178
+ raise Error, "invalid character at #{s[0,10].inspect}"
179
+ end
180
+ if typ != :space
181
+ ts << [typ, lexeme, val]
182
+ end
183
+ s = s[lexeme.length..-1]
184
+ end
185
+ ts
186
+ end
187
+
188
+
189
+ # Scans the first token in s and
190
+ # returns a 3-element list, or nil
191
+ # if s does not begin with a valid token.
192
+ #
193
+ # The first list element is one of
194
+ # '{', '}', ':', ',', '[', ']',
195
+ # :val, :str, and :space.
196
+ #
197
+ # The second element is the lexeme.
198
+ #
199
+ # The third element is the value of the
200
+ # token for :val and :str, otherwise
201
+ # it is the lexeme.
202
+ def tok(s)
203
+ case s[0]
204
+ when ?{ then ['{', s[0,1], s[0,1]]
205
+ when ?} then ['}', s[0,1], s[0,1]]
206
+ when ?: then [':', s[0,1], s[0,1]]
207
+ when ?, then [',', s[0,1], s[0,1]]
208
+ when ?[ then ['[', s[0,1], s[0,1]]
209
+ when ?] then [']', s[0,1], s[0,1]]
210
+ when ?n then nulltok(s)
211
+ when ?t then truetok(s)
212
+ when ?f then falsetok(s)
213
+ when ?" then strtok(s)
214
+ when Spc then [:space, s[0,1], s[0,1]]
215
+ when ?\t then [:space, s[0,1], s[0,1]]
216
+ when ?\n then [:space, s[0,1], s[0,1]]
217
+ when ?\r then [:space, s[0,1], s[0,1]]
218
+ else numtok(s)
219
+ end
220
+ end
221
+
222
+
223
+ def nulltok(s); s[0,4] == 'null' ? [:val, 'null', nil] : [] end
224
+ def truetok(s); s[0,4] == 'true' ? [:val, 'true', true] : [] end
225
+ def falsetok(s); s[0,5] == 'false' ? [:val, 'false', false] : [] end
226
+
227
+
228
+ def numtok(s)
229
+ m = /-?([1-9][0-9]+|[0-9])([.][0-9]+)?([eE][+-]?[0-9]+)?/.match(s)
230
+ if m && m.begin(0) == 0
231
+ if m[3] && !m[2]
232
+ [:val, m[0], Integer(m[1])*(10**Integer(m[3][1..-1]))]
233
+ elsif m[2]
234
+ [:val, m[0], Float(m[0])]
235
+ else
236
+ [:val, m[0], Integer(m[0])]
237
+ end
238
+ else
239
+ []
240
+ end
241
+ end
242
+
243
+
244
+ def strtok(s)
245
+ m = /"([^"\\]|\\["\/\\bfnrt]|\\u[0-9a-fA-F]{4})*"/.match(s)
246
+ if ! m
247
+ raise Error, "invalid string literal at #{abbrev(s)}"
248
+ end
249
+ [:str, m[0], unquote(m[0])]
250
+ end
251
+
252
+
253
+ def abbrev(s)
254
+ t = s[0,10]
255
+ p = t['`']
256
+ t = t[0,p] if p
257
+ t = t + '...' if t.length < s.length
258
+ '`' + t + '`'
259
+ end
260
+
261
+
262
+ # Converts a quoted json string literal q into a UTF-8-encoded string.
263
+ # The rules are different than for Ruby, so we cannot use eval.
264
+ # Unquote will raise an error if q contains control characters.
265
+ def unquote(q)
266
+ q = q[1...-1]
267
+ rubydoesenc = false
268
+ # In ruby >= 1.9, a[w] is a codepoint, not a byte.
269
+ if q.class.method_defined?(:force_encoding)
270
+ q.force_encoding('UTF-8')
271
+ rubydoesenc = true
272
+ end
273
+ a = q.dup # allocate a big enough string
274
+ r, w = 0, 0
275
+ while r < q.length
276
+ c = q[r]
277
+ case true
278
+ when c == ?\\
279
+ r += 1
280
+ if r >= q.length
281
+ raise Error, "string literal ends with a \"\\\": \"#{q}\""
282
+ end
283
+
284
+ case q[r]
285
+ when ?",?\\,?/,?'
286
+ a[w] = q[r]
287
+ r += 1
288
+ w += 1
289
+ when ?b,?f,?n,?r,?t
290
+ a[w] = Unesc[q[r]]
291
+ r += 1
292
+ w += 1
293
+ when ?u
294
+ r += 1
295
+ uchar = begin
296
+ hexdec4(q[r,4])
297
+ rescue RuntimeError => e
298
+ raise Error, "invalid escape sequence \\u#{q[r,4]}: #{e}"
299
+ end
300
+ r += 4
301
+ if surrogate? uchar
302
+ if q.length >= r+6
303
+ uchar1 = hexdec4(q[r+2,4])
304
+ uchar = subst(uchar, uchar1)
305
+ if uchar != Ucharerr
306
+ # A valid pair; consume.
307
+ r += 6
308
+ end
309
+ end
310
+ end
311
+ if rubydoesenc
312
+ a[w] = '' << uchar
313
+ w += 1
314
+ else
315
+ w += ucharenc(a, w, uchar)
316
+ end
317
+ else
318
+ raise Error, "invalid escape char #{q[r]} in \"#{q}\""
319
+ end
320
+ when c == ?", c < Spc
321
+ raise Error, "invalid character in string literal \"#{q}\""
322
+ else
323
+ # Copy anything else byte-for-byte.
324
+ # Valid UTF-8 will remain valid UTF-8.
325
+ # Invalid UTF-8 will remain invalid UTF-8.
326
+ # In ruby >= 1.9, c is a codepoint, not a byte,
327
+ # in which case this is still what we want.
328
+ a[w] = c
329
+ r += 1
330
+ w += 1
331
+ end
332
+ end
333
+ a[0,w]
334
+ end
335
+
336
+
337
+ # Encodes unicode character u as UTF-8
338
+ # bytes in string a at position i.
339
+ # Returns the number of bytes written.
340
+ def ucharenc(a, i, u)
341
+ case true
342
+ when u <= Uchar1max
343
+ a[i] = (u & 0xff).chr
344
+ 1
345
+ when u <= Uchar2max
346
+ a[i+0] = (Utag2 | ((u>>6)&0xff)).chr
347
+ a[i+1] = (Utagx | (u&Umaskx)).chr
348
+ 2
349
+ when u <= Uchar3max
350
+ a[i+0] = (Utag3 | ((u>>12)&0xff)).chr
351
+ a[i+1] = (Utagx | ((u>>6)&Umaskx)).chr
352
+ a[i+2] = (Utagx | (u&Umaskx)).chr
353
+ 3
354
+ else
355
+ a[i+0] = (Utag4 | ((u>>18)&0xff)).chr
356
+ a[i+1] = (Utagx | ((u>>12)&Umaskx)).chr
357
+ a[i+2] = (Utagx | ((u>>6)&Umaskx)).chr
358
+ a[i+3] = (Utagx | (u&Umaskx)).chr
359
+ 4
360
+ end
361
+ end
362
+
363
+
364
+ def hexdec4(s)
365
+ if s.length != 4
366
+ raise Error, 'short'
367
+ end
368
+ (nibble(s[0])<<12) | (nibble(s[1])<<8) | (nibble(s[2])<<4) | nibble(s[3])
369
+ end
370
+
371
+
372
+ def subst(u1, u2)
373
+ if Usurr1 <= u1 && u1 < Usurr2 && Usurr2 <= u2 && u2 < Usurr3
374
+ return ((u1-Usurr1)<<10) | (u2-Usurr2) + Usurrself
375
+ end
376
+ return Ucharerr
377
+ end
378
+
379
+
380
+ def surrogate?(u)
381
+ Usurr1 <= u && u < Usurr3
382
+ end
383
+
384
+
385
+ def nibble(c)
386
+ case true
387
+ when ?0 <= c && c <= ?9 then c.ord - ?0.ord
388
+ when ?a <= c && c <= ?z then c.ord - ?a.ord + 10
389
+ when ?A <= c && c <= ?Z then c.ord - ?A.ord + 10
390
+ else
391
+ raise Error, "invalid hex code #{c}"
392
+ end
393
+ end
394
+
395
+
396
+ # Encodes x into a json text. It may contain only
397
+ # Array, Hash, String, Numeric, true, false, nil.
398
+ # (Note, this list excludes Symbol.)
399
+ # X itself must be an Array or a Hash.
400
+ # No other value can be encoded, and an error will
401
+ # be raised if x contains any other value, such as
402
+ # Nan, Infinity, Symbol, and Proc, or if a Hash key
403
+ # is not a String.
404
+ # Strings contained in x must be valid UTF-8.
405
+ def encode(x)
406
+ case x
407
+ when Hash then objenc(x)
408
+ when Array then arrenc(x)
409
+ else
410
+ raise Error, 'root value must be an Array or a Hash'
411
+ end
412
+ end
413
+
414
+
415
+ def valenc(x)
416
+ case x
417
+ when Hash then objenc(x)
418
+ when Array then arrenc(x)
419
+ when String then strenc(x)
420
+ when Numeric then numenc(x)
421
+ when true then "true"
422
+ when false then "false"
423
+ when nil then "null"
424
+ else
425
+ raise Error, "cannot encode #{x.class}: #{x.inspect}"
426
+ end
427
+ end
428
+
429
+
430
+ def objenc(x)
431
+ '{' + x.map{|k,v| keyenc(k) + ':' + valenc(v)}.join(',') + '}'
432
+ end
433
+
434
+
435
+ def arrenc(a)
436
+ '[' + a.map{|x| valenc(x)}.join(',') + ']'
437
+ end
438
+
439
+
440
+ def keyenc(k)
441
+ case k
442
+ when String then strenc(k)
443
+ else
444
+ raise Error, "Hash key is not a string: #{k.inspect}"
445
+ end
446
+ end
447
+
448
+
449
+ def strenc(s)
450
+ t = StringIO.new
451
+ t.putc(?")
452
+ r = 0
453
+
454
+ # In ruby >= 1.9, s[r] is a codepoint, not a byte.
455
+ rubydoesenc = s.class.method_defined?(:encoding)
456
+
457
+ while r < s.length
458
+ case s[r]
459
+ when ?" then t.print('\\"')
460
+ when ?\\ then t.print('\\\\')
461
+ when ?\b then t.print('\\b')
462
+ when ?\f then t.print('\\f')
463
+ when ?\n then t.print('\\n')
464
+ when ?\r then t.print('\\r')
465
+ when ?\t then t.print('\\t')
466
+ else
467
+ c = s[r]
468
+ case true
469
+ when rubydoesenc
470
+ begin
471
+ c.ord # will raise an error if c is invalid UTF-8
472
+ t.write(c)
473
+ rescue
474
+ t.write(Ustrerr)
475
+ end
476
+ when Spc <= c && c <= ?~
477
+ t.putc(c)
478
+ else
479
+ n = ucharcopy(t, s, r) # ensure valid UTF-8 output
480
+ r += n - 1 # r is incremented below
481
+ end
482
+ end
483
+ r += 1
484
+ end
485
+ t.putc(?")
486
+ t.string
487
+ end
488
+
489
+
490
+ def numenc(x)
491
+ if ((x.nan? || x.infinite?) rescue false)
492
+ raise Error, "Numeric cannot be represented: #{x}"
493
+ end
494
+ "#{x}"
495
+ end
496
+
497
+
498
+ # Copies the valid UTF-8 bytes of a single character
499
+ # from string s at position i to I/O object t, and
500
+ # returns the number of bytes copied.
501
+ # If no valid UTF-8 char exists at position i,
502
+ # ucharcopy writes Ustrerr and returns 1.
503
+ def ucharcopy(t, s, i)
504
+ n = s.length - i
505
+ raise Utf8Error if n < 1
506
+
507
+ c0 = s[i].ord
508
+
509
+ # 1-byte, 7-bit sequence?
510
+ if c0 < Utagx
511
+ t.putc(c0)
512
+ return 1
513
+ end
514
+
515
+ raise Utf8Error if c0 < Utag2 # unexpected continuation byte?
516
+
517
+ raise Utf8Error if n < 2 # need continuation byte
518
+ c1 = s[i+1].ord
519
+ raise Utf8Error if c1 < Utagx || Utag2 <= c1
520
+
521
+ # 2-byte, 11-bit sequence?
522
+ if c0 < Utag3
523
+ raise Utf8Error if ((c0&Umask2)<<6 | (c1&Umaskx)) <= Uchar1max
524
+ t.putc(c0)
525
+ t.putc(c1)
526
+ return 2
527
+ end
528
+
529
+ # need second continuation byte
530
+ raise Utf8Error if n < 3
531
+
532
+ c2 = s[i+2].ord
533
+ raise Utf8Error if c2 < Utagx || Utag2 <= c2
534
+
535
+ # 3-byte, 16-bit sequence?
536
+ if c0 < Utag4
537
+ u = (c0&Umask3)<<12 | (c1&Umaskx)<<6 | (c2&Umaskx)
538
+ raise Utf8Error if u <= Uchar2max
539
+ t.putc(c0)
540
+ t.putc(c1)
541
+ t.putc(c2)
542
+ return 3
543
+ end
544
+
545
+ # need third continuation byte
546
+ raise Utf8Error if n < 4
547
+ c3 = s[i+3].ord
548
+ raise Utf8Error if c3 < Utagx || Utag2 <= c3
549
+
550
+ # 4-byte, 21-bit sequence?
551
+ if c0 < Utag5
552
+ u = (c0&Umask4)<<18 | (c1&Umaskx)<<12 | (c2&Umaskx)<<6 | (c3&Umaskx)
553
+ raise Utf8Error if u <= Uchar3max
554
+ t.putc(c0)
555
+ t.putc(c1)
556
+ t.putc(c2)
557
+ t.putc(c3)
558
+ return 4
559
+ end
560
+
561
+ raise Utf8Error
562
+ rescue Utf8Error
563
+ t.write(Ustrerr)
564
+ return 1
565
+ end
566
+
567
+
568
+ class Utf8Error < ::StandardError
569
+ end
570
+
571
+
572
+ class Error < ::StandardError
573
+ end
574
+
575
+
576
+ Utagx = 0x80 # 1000 0000
577
+ Utag2 = 0xc0 # 1100 0000
578
+ Utag3 = 0xe0 # 1110 0000
579
+ Utag4 = 0xf0 # 1111 0000
580
+ Utag5 = 0xF8 # 1111 1000
581
+ Umaskx = 0x3f # 0011 1111
582
+ Umask2 = 0x1f # 0001 1111
583
+ Umask3 = 0x0f # 0000 1111
584
+ Umask4 = 0x07 # 0000 0111
585
+ Uchar1max = (1<<7) - 1
586
+ Uchar2max = (1<<11) - 1
587
+ Uchar3max = (1<<16) - 1
588
+ Ucharerr = 0xFFFD # unicode "replacement char"
589
+ Ustrerr = "\xef\xbf\xbd" # unicode "replacement char"
590
+ Usurrself = 0x10000
591
+ Usurr1 = 0xd800
592
+ Usurr2 = 0xdc00
593
+ Usurr3 = 0xe000
594
+
595
+ Spc = ' '[0]
596
+ Unesc = {?b=>?\b, ?f=>?\f, ?n=>?\n, ?r=>?\r, ?t=>?\t}
597
+ end
598
+ end