logstash-core 2.2.4.snapshot1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of logstash-core might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/lib/logstash-core.rb +1 -0
- data/lib/logstash-core/logstash-core.rb +3 -0
- data/lib/logstash-core/version.rb +8 -0
- data/lib/logstash/agent.rb +391 -0
- data/lib/logstash/codecs/base.rb +50 -0
- data/lib/logstash/config/config_ast.rb +550 -0
- data/lib/logstash/config/cpu_core_strategy.rb +32 -0
- data/lib/logstash/config/defaults.rb +12 -0
- data/lib/logstash/config/file.rb +39 -0
- data/lib/logstash/config/grammar.rb +3503 -0
- data/lib/logstash/config/mixin.rb +518 -0
- data/lib/logstash/config/registry.rb +13 -0
- data/lib/logstash/environment.rb +98 -0
- data/lib/logstash/errors.rb +12 -0
- data/lib/logstash/filters/base.rb +205 -0
- data/lib/logstash/inputs/base.rb +116 -0
- data/lib/logstash/inputs/threadable.rb +18 -0
- data/lib/logstash/java_integration.rb +116 -0
- data/lib/logstash/json.rb +61 -0
- data/lib/logstash/logging.rb +91 -0
- data/lib/logstash/namespace.rb +13 -0
- data/lib/logstash/output_delegator.rb +172 -0
- data/lib/logstash/outputs/base.rb +91 -0
- data/lib/logstash/patches.rb +5 -0
- data/lib/logstash/patches/bugfix_jruby_2558.rb +51 -0
- data/lib/logstash/patches/cabin.rb +35 -0
- data/lib/logstash/patches/profile_require_calls.rb +47 -0
- data/lib/logstash/patches/rubygems.rb +38 -0
- data/lib/logstash/patches/stronger_openssl_defaults.rb +68 -0
- data/lib/logstash/pipeline.rb +499 -0
- data/lib/logstash/pipeline_reporter.rb +114 -0
- data/lib/logstash/plugin.rb +120 -0
- data/lib/logstash/program.rb +14 -0
- data/lib/logstash/runner.rb +124 -0
- data/lib/logstash/shutdown_watcher.rb +100 -0
- data/lib/logstash/util.rb +203 -0
- data/lib/logstash/util/buftok.rb +139 -0
- data/lib/logstash/util/charset.rb +35 -0
- data/lib/logstash/util/decorators.rb +52 -0
- data/lib/logstash/util/defaults_printer.rb +31 -0
- data/lib/logstash/util/filetools.rb +186 -0
- data/lib/logstash/util/java_version.rb +66 -0
- data/lib/logstash/util/password.rb +25 -0
- data/lib/logstash/util/plugin_version.rb +56 -0
- data/lib/logstash/util/prctl.rb +10 -0
- data/lib/logstash/util/retryable.rb +40 -0
- data/lib/logstash/util/socket_peer.rb +7 -0
- data/lib/logstash/util/unicode_trimmer.rb +81 -0
- data/lib/logstash/util/worker_threads_default_printer.rb +29 -0
- data/lib/logstash/util/wrapped_synchronous_queue.rb +41 -0
- data/lib/logstash/version.rb +14 -0
- data/locales/en.yml +204 -0
- data/logstash-core.gemspec +58 -0
- data/spec/conditionals_spec.rb +429 -0
- data/spec/logstash/agent_spec.rb +85 -0
- data/spec/logstash/config/config_ast_spec.rb +146 -0
- data/spec/logstash/config/cpu_core_strategy_spec.rb +123 -0
- data/spec/logstash/config/defaults_spec.rb +10 -0
- data/spec/logstash/config/mixin_spec.rb +158 -0
- data/spec/logstash/environment_spec.rb +56 -0
- data/spec/logstash/filters/base_spec.rb +251 -0
- data/spec/logstash/inputs/base_spec.rb +74 -0
- data/spec/logstash/java_integration_spec.rb +304 -0
- data/spec/logstash/json_spec.rb +96 -0
- data/spec/logstash/output_delegator_spec.rb +144 -0
- data/spec/logstash/outputs/base_spec.rb +40 -0
- data/spec/logstash/patches_spec.rb +90 -0
- data/spec/logstash/pipeline_reporter_spec.rb +85 -0
- data/spec/logstash/pipeline_spec.rb +455 -0
- data/spec/logstash/plugin_spec.rb +169 -0
- data/spec/logstash/runner_spec.rb +68 -0
- data/spec/logstash/shutdown_watcher_spec.rb +113 -0
- data/spec/logstash/util/buftok_spec.rb +31 -0
- data/spec/logstash/util/charset_spec.rb +74 -0
- data/spec/logstash/util/defaults_printer_spec.rb +50 -0
- data/spec/logstash/util/java_version_spec.rb +79 -0
- data/spec/logstash/util/plugin_version_spec.rb +64 -0
- data/spec/logstash/util/unicode_trimmer_spec.rb +55 -0
- data/spec/logstash/util/worker_threads_default_printer_spec.rb +45 -0
- data/spec/logstash/util/wrapped_synchronous_queue_spec.rb +28 -0
- data/spec/logstash/util_spec.rb +35 -0
- metadata +364 -0
@@ -0,0 +1,186 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "net/http"
|
3
|
+
require "uri"
|
4
|
+
require "digest/sha1"
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module LogStash::Util::FileTools
|
8
|
+
extend self
|
9
|
+
|
10
|
+
def fetch(url, sha1, output)
|
11
|
+
|
12
|
+
puts "Downloading #{url}"
|
13
|
+
actual_sha1 = download(url, output)
|
14
|
+
|
15
|
+
if actual_sha1 != sha1
|
16
|
+
fail "SHA1 does not match (expected '#{sha1}' but got '#{actual_sha1}')"
|
17
|
+
end
|
18
|
+
end # def fetch
|
19
|
+
|
20
|
+
def file_fetch(url, sha1, target)
|
21
|
+
filename = File.basename( URI(url).path )
|
22
|
+
output = "#{target}/#{filename}"
|
23
|
+
begin
|
24
|
+
actual_sha1 = file_sha1(output)
|
25
|
+
if actual_sha1 != sha1
|
26
|
+
fetch(url, sha1, output)
|
27
|
+
end
|
28
|
+
rescue Errno::ENOENT
|
29
|
+
fetch(url, sha1, output)
|
30
|
+
end
|
31
|
+
return output
|
32
|
+
end
|
33
|
+
|
34
|
+
def file_sha1(path)
|
35
|
+
digest = Digest::SHA1.new
|
36
|
+
fd = File.new(path, "r")
|
37
|
+
while true
|
38
|
+
begin
|
39
|
+
digest << fd.sysread(16384)
|
40
|
+
rescue EOFError
|
41
|
+
break
|
42
|
+
end
|
43
|
+
end
|
44
|
+
return digest.hexdigest
|
45
|
+
ensure
|
46
|
+
fd.close if fd
|
47
|
+
end
|
48
|
+
|
49
|
+
def download(url, output)
|
50
|
+
uri = URI(url)
|
51
|
+
digest = Digest::SHA1.new
|
52
|
+
tmp = "#{output}.tmp"
|
53
|
+
Net::HTTP.start(uri.host, uri.port, :use_ssl => (uri.scheme == "https")) do |http|
|
54
|
+
request = Net::HTTP::Get.new(uri.path)
|
55
|
+
http.request(request) do |response|
|
56
|
+
fail "HTTP fetch failed for #{url}. #{response}" if [200, 301].include?(response.code)
|
57
|
+
size = (response["content-length"].to_i || -1).to_f
|
58
|
+
count = 0
|
59
|
+
File.open(tmp, "w") do |fd|
|
60
|
+
response.read_body do |chunk|
|
61
|
+
fd.write(chunk)
|
62
|
+
digest << chunk
|
63
|
+
if size > 0 && $stdout.tty?
|
64
|
+
count += chunk.bytesize
|
65
|
+
$stdout.write(sprintf("\r%0.2f%%", count/size * 100))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
$stdout.write("\r \r") if $stdout.tty?
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
File.rename(tmp, output)
|
74
|
+
|
75
|
+
return digest.hexdigest
|
76
|
+
rescue SocketError => e
|
77
|
+
puts "Failure while downloading #{url}: #{e}"
|
78
|
+
raise
|
79
|
+
ensure
|
80
|
+
File.unlink(tmp) if File.exist?(tmp)
|
81
|
+
end # def download
|
82
|
+
|
83
|
+
def untar(tarball, &block)
|
84
|
+
require "archive/tar/minitar"
|
85
|
+
tgz = Zlib::GzipReader.new(File.open(tarball))
|
86
|
+
# Pull out typesdb
|
87
|
+
tar = Archive::Tar::Minitar::Input.open(tgz)
|
88
|
+
tar.each do |entry|
|
89
|
+
path = block.call(entry)
|
90
|
+
next if path.nil?
|
91
|
+
parent = File.dirname(path)
|
92
|
+
|
93
|
+
FileUtils.mkdir_p(parent) unless File.directory?(parent)
|
94
|
+
|
95
|
+
# Skip this file if the output file is the same size
|
96
|
+
if entry.directory?
|
97
|
+
FileUtils.mkdir_p(path) unless File.directory?(path)
|
98
|
+
else
|
99
|
+
entry_mode = entry.instance_eval { @mode } & 0777
|
100
|
+
if File.exists?(path)
|
101
|
+
stat = File.stat(path)
|
102
|
+
# TODO(sissel): Submit a patch to archive-tar-minitar upstream to
|
103
|
+
# expose headers in the entry.
|
104
|
+
entry_size = entry.instance_eval { @size }
|
105
|
+
# If file sizes are same, skip writing.
|
106
|
+
next if stat.size == entry_size && (stat.mode & 0777) == entry_mode
|
107
|
+
end
|
108
|
+
puts "Extracting #{entry.full_name} from #{tarball} #{entry_mode.to_s(8)}"
|
109
|
+
File.open(path, "w") do |fd|
|
110
|
+
# eof? check lets us skip empty files. Necessary because the API provided by
|
111
|
+
# Archive::Tar::Minitar::Reader::EntryStream only mostly acts like an
|
112
|
+
# IO object. Something about empty files in this EntryStream causes
|
113
|
+
# IO.copy_stream to throw "can't convert nil into String" on JRuby
|
114
|
+
# TODO(sissel): File a bug about this.
|
115
|
+
while !entry.eof?
|
116
|
+
chunk = entry.read(16384)
|
117
|
+
fd.write(chunk)
|
118
|
+
end
|
119
|
+
#IO.copy_stream(entry, fd)
|
120
|
+
end
|
121
|
+
File.chmod(entry_mode, path)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
tar.close
|
125
|
+
File.unlink(tarball) if File.file?(tarball)
|
126
|
+
end # def untar
|
127
|
+
|
128
|
+
def do_ungz(file)
|
129
|
+
|
130
|
+
outpath = file.gsub('.gz', '')
|
131
|
+
tgz = Zlib::GzipReader.new(File.open(file))
|
132
|
+
begin
|
133
|
+
File.open(outpath, "w") do |out|
|
134
|
+
IO::copy_stream(tgz, out)
|
135
|
+
end
|
136
|
+
File.unlink(file)
|
137
|
+
rescue
|
138
|
+
File.unlink(outpath) if File.file?(outpath)
|
139
|
+
raise
|
140
|
+
end
|
141
|
+
tgz.close
|
142
|
+
end
|
143
|
+
|
144
|
+
def eval_file(entry, files, prefix)
|
145
|
+
return false if entry.full_name =~ /PaxHeaders/
|
146
|
+
if !files.nil?
|
147
|
+
if files.is_a?(Array)
|
148
|
+
return false unless files.include?(entry.full_name.gsub(prefix, ''))
|
149
|
+
entry.full_name.split("/").last
|
150
|
+
elsif files.is_a?(String)
|
151
|
+
return false unless entry.full_name =~ Regexp.new(files)
|
152
|
+
entry.full_name.split("/").last
|
153
|
+
end
|
154
|
+
else
|
155
|
+
entry.full_name.gsub(prefix, '')
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def process_downloads(files,target='')
|
160
|
+
|
161
|
+
FileUtils.mkdir_p(target) unless File.directory?(target)
|
162
|
+
|
163
|
+
files.each do |file|
|
164
|
+
download = file_fetch(file['url'], file['sha1'],target)
|
165
|
+
|
166
|
+
if download =~ /.tar.gz/
|
167
|
+
prefix = download.gsub('.tar.gz', '').gsub("#{target}/", '')
|
168
|
+
untar(download) do |entry|
|
169
|
+
next unless out = eval_file(entry, file['files'], prefix)
|
170
|
+
File.join(target, out)
|
171
|
+
end
|
172
|
+
|
173
|
+
elsif download =~ /.tgz/
|
174
|
+
prefix = download.gsub('.tgz', '').gsub("#{target}/", '')
|
175
|
+
untar(download) do |entry|
|
176
|
+
next unless out = eval_file(entry, file['files'], prefix)
|
177
|
+
File.join(target, out)
|
178
|
+
end
|
179
|
+
|
180
|
+
elsif download =~ /.gz/
|
181
|
+
do_ungz(download)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'cabin'
|
3
|
+
|
4
|
+
module LogStash::Util::JavaVersion
|
5
|
+
def self.logger
|
6
|
+
@logger ||= Cabin::Channel.get(LogStash)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Print a warning if we're on a bad version of java
|
10
|
+
def self.warn_on_bad_java_version
|
11
|
+
if self.bad_java_version?(self.version)
|
12
|
+
msg = "!!! Please upgrade your java version, the current version '#{self.version}' may cause problems. We recommend a minimum version of 1.7.0_51"
|
13
|
+
STDERR.puts(msg)
|
14
|
+
logger.warn(msg)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Return the current java version string. Returns nil if this is a non-java platform (e.g. MRI).
|
19
|
+
def self.version
|
20
|
+
return nil unless LogStash::Environment.jruby?
|
21
|
+
java.lang.System.getProperty("java.runtime.version")
|
22
|
+
end
|
23
|
+
|
24
|
+
# Takes a string of a java version ex: "1.8.0_24-beta"
|
25
|
+
# and returns a parsed map of the components.
|
26
|
+
# nil inputs will be returned as nil.
|
27
|
+
def self.parse_java_version(version_string)
|
28
|
+
return nil if version_string.nil?
|
29
|
+
|
30
|
+
# Crazy java versioning rules @ http://www.oracle.com/technetwork/java/javase/versioning-naming-139433.html
|
31
|
+
# The regex below parses this all correctly http://rubular.com/r/sInQc3Nc7f
|
32
|
+
|
33
|
+
match = version_string.match(/\A(\d+)\.(\d+)\.(\d+)(_(\d+))?(-(.+))?\Z/)
|
34
|
+
|
35
|
+
return nil unless match
|
36
|
+
|
37
|
+
major, minor, patch, ufull, update, bfull, build = match.captures
|
38
|
+
|
39
|
+
{
|
40
|
+
:full => version_string,
|
41
|
+
:major => major.to_i,
|
42
|
+
:minor => minor.to_i,
|
43
|
+
:patch => patch.to_i,
|
44
|
+
:update => update.to_i, # this is always coerced to an int (a nil will be zero) to make comparisons easier
|
45
|
+
:build => build # not an integer, could be b06 for instance!,
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Determine if the given java version string is a bad version of java
|
50
|
+
# If it is, return true, if it isn't return false.
|
51
|
+
# Accepts nil, returning nil.
|
52
|
+
def self.bad_java_version?(version_string)
|
53
|
+
return nil if version_string.nil?
|
54
|
+
|
55
|
+
parsed = parse_java_version(version_string)
|
56
|
+
return false unless parsed
|
57
|
+
|
58
|
+
if parsed[:major] == 1 && parsed[:minor] == 7 && parsed[:patch] == 0 && parsed[:update] < 51
|
59
|
+
true
|
60
|
+
elsif parsed[:major] == 1 && parsed[:minor] < 7
|
61
|
+
true
|
62
|
+
else
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "logstash/namespace"
|
3
|
+
require "logstash/util"
|
4
|
+
|
5
|
+
# This class exists to quietly wrap a password string so that, when printed or
|
6
|
+
# logged, you don't accidentally print the password itself.
|
7
|
+
class LogStash::Util::Password
|
8
|
+
attr_reader :value
|
9
|
+
|
10
|
+
public
|
11
|
+
def initialize(password)
|
12
|
+
@value = password
|
13
|
+
end # def initialize
|
14
|
+
|
15
|
+
public
|
16
|
+
def to_s
|
17
|
+
return "<password>"
|
18
|
+
end # def to_s
|
19
|
+
|
20
|
+
public
|
21
|
+
def inspect
|
22
|
+
return to_s
|
23
|
+
end # def inspect
|
24
|
+
end # class LogStash::Util::Password
|
25
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'logstash/errors'
|
3
|
+
require 'rubygems/version'
|
4
|
+
require 'forwardable'
|
5
|
+
|
6
|
+
module LogStash::Util
|
7
|
+
class PluginVersion
|
8
|
+
extend Forwardable
|
9
|
+
include Comparable
|
10
|
+
|
11
|
+
GEM_NAME_PREFIX = 'logstash'
|
12
|
+
|
13
|
+
def_delegators :@version, :to_s
|
14
|
+
attr_reader :version
|
15
|
+
|
16
|
+
def initialize(*options)
|
17
|
+
if options.size == 1 && options.first.is_a?(Gem::Version)
|
18
|
+
@version = options.first
|
19
|
+
else
|
20
|
+
@version = Gem::Version.new(options.join('.'))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.find_version!(name)
|
25
|
+
begin
|
26
|
+
spec = Gem::Specification.find_by_name(name)
|
27
|
+
if spec.nil?
|
28
|
+
# Checking for nil? is a workaround for situations where find_by_name
|
29
|
+
# is not able to find the real spec, as for example with pre releases
|
30
|
+
# of plugins
|
31
|
+
spec = Gem::Specification.find_all_by_name(name).first
|
32
|
+
end
|
33
|
+
new(spec.version)
|
34
|
+
rescue Gem::LoadError
|
35
|
+
# Rescuing the LoadError and raise a Logstash specific error.
|
36
|
+
# Likely we can't find the gem in the current GEM_PATH
|
37
|
+
raise LogStash::PluginNoVersionError
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.find_plugin_version!(type, name)
|
42
|
+
plugin_name = [GEM_NAME_PREFIX, type, name].join('-')
|
43
|
+
find_version!(plugin_name)
|
44
|
+
end
|
45
|
+
|
46
|
+
def <=>(other)
|
47
|
+
version <=> other.version
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def self.build_from_spec(spec)
|
53
|
+
new(spec.version)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module LogStash
|
3
|
+
module Retryable
|
4
|
+
# execute retryable code block
|
5
|
+
# @param [Hash] options retryable options
|
6
|
+
# @option options [Fixnum] :tries retries to perform, default 1, set to 0 for infite retries. 1 means that upon exception the block will be retried once
|
7
|
+
# @option options [Fixnum] :base_sleep seconds to sleep on first retry, default 1
|
8
|
+
# @option options [Fixnum] :max_sleep max seconds to sleep upon exponential backoff, default 1
|
9
|
+
# @option options [Exception] :rescue exception class list to retry on, defaults is Exception, which retries on any Exception.
|
10
|
+
# @option options [Proc] :on_retry call the given Proc/lambda before each retry with the raised exception as parameter
|
11
|
+
def retryable(options = {}, &block)
|
12
|
+
options = {
|
13
|
+
:tries => 1,
|
14
|
+
:rescue => Exception,
|
15
|
+
:on_retry => nil,
|
16
|
+
:base_sleep => 1,
|
17
|
+
:max_sleep => 1,
|
18
|
+
}.merge(options)
|
19
|
+
|
20
|
+
rescue_classes = Array(options[:rescue])
|
21
|
+
max_sleep_retry = Math.log2(options[:max_sleep] / options[:base_sleep])
|
22
|
+
retry_count = 0
|
23
|
+
|
24
|
+
begin
|
25
|
+
return yield(retry_count)
|
26
|
+
rescue *rescue_classes => e
|
27
|
+
raise e if options[:tries] > 0 && retry_count >= options[:tries]
|
28
|
+
|
29
|
+
options[:on_retry].call(retry_count + 1, e) if options[:on_retry]
|
30
|
+
|
31
|
+
# dont compute and maybe overflow exponent on too big a retry count
|
32
|
+
seconds = retry_count < max_sleep_retry ? options[:base_sleep] * (2 ** retry_count) : options[:max_sleep]
|
33
|
+
sleep(seconds)
|
34
|
+
|
35
|
+
retry_count += 1
|
36
|
+
retry
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module LogStash::Util::UnicodeTrimmer
|
3
|
+
# The largest possible unicode chars are 4 bytes
|
4
|
+
# http://stackoverflow.com/questions/9533258/what-is-the-maximum-number-of-bytes-for-a-utf-8-encoded-character
|
5
|
+
# http://tools.ietf.org/html/rfc3629
|
6
|
+
MAX_CHAR_BYTES = 4
|
7
|
+
|
8
|
+
# Takes a unicode string and makes sure it fits in a max of `desired_bytes`
|
9
|
+
# This aims to be somewhat efficient about this for the average case and get as close to
|
10
|
+
# O(1) as possible. Given certain distributions of multi-byte characters it'll be slower
|
11
|
+
# It tries to find the point the truncation *should* happen based on the average byte size.
|
12
|
+
# If that snips it in the wrong place it'll try to add or remove chars to get it to the right
|
13
|
+
# spot and preserve as much data as possible.
|
14
|
+
public
|
15
|
+
def self.trim_bytes(orig_str, desired_bytes)
|
16
|
+
return orig_str if orig_str.bytesize <= desired_bytes
|
17
|
+
|
18
|
+
pre_shortened = pre_shorten(orig_str, desired_bytes)
|
19
|
+
|
20
|
+
case pre_shortened.bytesize <=> desired_bytes
|
21
|
+
when 0
|
22
|
+
pre_shortened
|
23
|
+
when 1
|
24
|
+
shrink_bytes(pre_shortened, orig_str, desired_bytes)
|
25
|
+
when -1
|
26
|
+
grow_bytes(pre_shortened, orig_str, desired_bytes)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
# Try to cut the string at the right place based on the avg. byte size
|
32
|
+
def self.pre_shorten(orig_str, desired_bytes)
|
33
|
+
# Compute the average size to get an idea of where should chop
|
34
|
+
orig_len = orig_str.length
|
35
|
+
orig_bs = orig_str.bytesize
|
36
|
+
avg_size = (orig_bs.to_f / orig_len.to_f)
|
37
|
+
|
38
|
+
# Try to do an initial shortening based on the average char size
|
39
|
+
# The goal here is to get us somewhere above or below the boundary quickly
|
40
|
+
orig_extra_bytes = orig_bs - desired_bytes
|
41
|
+
pre_shorten_by = (orig_extra_bytes / avg_size).to_i
|
42
|
+
orig_str.slice(0, orig_len - pre_shorten_by)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def self.grow_bytes(pre_shortened, orig_str, desired_bytes)
|
47
|
+
res_str = pre_shortened.clone()
|
48
|
+
|
49
|
+
loop do
|
50
|
+
bs = res_str.bytesize
|
51
|
+
deficit = desired_bytes - bs
|
52
|
+
lengthen_by = deficit / MAX_CHAR_BYTES
|
53
|
+
lengthen_by = 1 if lengthen_by < 1
|
54
|
+
append = orig_str.slice(res_str.length, lengthen_by)
|
55
|
+
|
56
|
+
break if (bs + append.bytesize) > desired_bytes
|
57
|
+
|
58
|
+
res_str << append
|
59
|
+
end
|
60
|
+
|
61
|
+
res_str
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
def self.shrink_bytes(pre_shortened, orig_str, desired_bytes)
|
66
|
+
res_str = pre_shortened.clone()
|
67
|
+
|
68
|
+
loop do
|
69
|
+
bs = res_str.bytesize
|
70
|
+
break if bs <= desired_bytes
|
71
|
+
|
72
|
+
extra = bs - desired_bytes
|
73
|
+
shorten_by = extra / MAX_CHAR_BYTES
|
74
|
+
shorten_by = 1 if shorten_by < 1
|
75
|
+
|
76
|
+
res_str.slice!(res_str.length - shorten_by)
|
77
|
+
end
|
78
|
+
|
79
|
+
res_str
|
80
|
+
end
|
81
|
+
end
|