stega 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 501c850e45a2cebf99cd7e9ca8189dccb25d0e9f4982dfe1e92402c43e93f989
4
+ data.tar.gz: 1b924447c42f6e2f6efb86d70bd1bec64f6a9bef6c0df6f9b86f37ff3aa8c527
5
+ SHA512:
6
+ metadata.gz: e56737e0a7cdc7e84c22db82ac4db40aa7633b18560f86e0a6dac3b1fad561df66f7f9a0f49d247fea1b2bae6485c97c09852c2f0d8e0fced16072ae0fe4eda7
7
+ data.tar.gz: 809b6c65037f0c8d8e308b7ce3ea29b987408200376a2adb5a70c8e8db3e0659a9e79c41091609677c95c136d3603036e989934471bbf3c4f2ac0173bfac051a
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # Change log
2
+
3
+ ## master
data/LICENSE.txt ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2026 Theodor Tonum
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ [![Gem Version](https://badge.fury.io/rb/stega.svg)](https://rubygems.org/gems/stega)
2
+
3
+ # Stega
4
+
5
+ TBD
6
+
7
+ ## Installation
8
+
9
+ Adding to a gem:
10
+
11
+ ```ruby
12
+ # my-cool-gem.gemspec
13
+ Gem::Specification.new do |spec|
14
+ # ...
15
+ spec.add_dependency "stega"
16
+ # ...
17
+ end
18
+ ```
19
+
20
+ Or adding to your project:
21
+
22
+ ```ruby
23
+ # Gemfile
24
+ gem "stega"
25
+ ```
26
+
27
+ ### Supported Ruby versions
28
+
29
+ - Ruby (MRI) >= 3.0
30
+
31
+ ## Usage
32
+
33
+ TBD
34
+
35
+ ## Contributing
36
+
37
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/rorkjop/stega](https://github.com/rorkjop/stega).
38
+
39
+ ## Credits
40
+
41
+ This gem is generated via [`newgem` template](https://github.com/palkan/newgem) by [@palkan](https://github.com/palkan).
42
+
43
+ ## License
44
+
45
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
46
+
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stega
4
+ module Decoder
5
+ # Reverse lookup: invisible character → base-4 digit
6
+ BASE4_REVERSE_MAP = Encoder::BASE4_CHARS.each_with_index.to_h { |char, idx| [char, idx] }.freeze
7
+
8
+ # Reverse lookup: invisible codepoint → hex digit
9
+ HEX_REVERSE_MAP = LegacyEncoder::HEX_CHAR_MAP.to_h { |hex_digit, codepoint| [codepoint, hex_digit] }.freeze
10
+
11
+ # Regex character class matching all invisible stega characters
12
+ STEGA_CHAR_CLASS = LegacyEncoder::HEX_CHAR_MAP.values.map { |cp| format("\\u{%x}", cp) }.join
13
+
14
+ # Regex to match encoded strings (4+ invisible characters)
15
+ REGEX = Regexp.new("[#{STEGA_CHAR_CLASS}]{4,}")
16
+
17
+ NULL_CHAR = "\x00"
18
+
19
+ # Decode the first steganographic payload from a string.
20
+ def self.decode(str)
21
+ match = str.match(REGEX)
22
+ return nil unless match
23
+
24
+ decode_payload(match[0], first_only: true).first
25
+ end
26
+
27
+ # Decode all steganographic payloads from a string.
28
+ def self.decode_all(str)
29
+ matches = str.scan(REGEX)
30
+ return nil if matches.empty?
31
+
32
+ matches.flat_map { |m| decode_payload(m) }
33
+ end
34
+
35
+ # Decode an invisible-character payload back into JSON value(s).
36
+ # Detects whether it's new (base-4 with prefix) or legacy (hex-pair) encoding.
37
+ def self.decode_payload(encoded, first_only: false)
38
+ chars = encoded.chars
39
+
40
+ # If even length but not divisible by 4 or missing prefix → legacy encoding
41
+ if chars.length.even?
42
+ if (chars.length % 4 != 0) || !encoded.start_with?(Encoder::STEGA_PREFIX)
43
+ return decode_legacy_payload(chars, first_only)
44
+ end
45
+ else
46
+ raise ArgumentError, "Encoded data has invalid length"
47
+ end
48
+
49
+ # New base-4 encoding: skip 4-char prefix
50
+ data_chars = chars[4..]
51
+ bytes = Array.new(data_chars.length / 4)
52
+
53
+ bytes.length.times do |i|
54
+ bytes[i] = (BASE4_REVERSE_MAP[data_chars[i * 4]] << 6) |
55
+ (BASE4_REVERSE_MAP[data_chars[i * 4 + 1]] << 4) |
56
+ (BASE4_REVERSE_MAP[data_chars[i * 4 + 2]] << 2) |
57
+ BASE4_REVERSE_MAP[data_chars[i * 4 + 3]]
58
+ end
59
+
60
+ decoded = bytes.pack("C*").force_encoding(Encoding::UTF_8)
61
+
62
+ if first_only
63
+ null_index = decoded.index(NULL_CHAR) || decoded.length
64
+ return [JSON.parse(decoded[0...null_index])]
65
+ end
66
+
67
+ decoded.split(NULL_CHAR).reject(&:empty?).map { |segment| JSON.parse(segment) }
68
+ end
69
+
70
+ # Decode legacy hex-pair encoded payload.
71
+ # Each pair of invisible characters represents one hex byte → one ASCII character.
72
+ def self.decode_legacy_payload(chars, first_only)
73
+ ascii_chars = []
74
+
75
+ (chars.length / 2).times do |i|
76
+ idx = (chars.length / 2) - 1 - i
77
+ hex_str = HEX_REVERSE_MAP[chars[idx * 2].ord].to_s +
78
+ HEX_REVERSE_MAP[chars[idx * 2 + 1].ord].to_s
79
+ ascii_chars.unshift(hex_str.to_i(16).chr)
80
+ end
81
+
82
+ results = []
83
+ queue = [ascii_chars.join]
84
+ max_retries = 10
85
+
86
+ # Try to parse concatenated JSON values by splitting at parse error positions
87
+ until queue.empty?
88
+ str = queue.shift
89
+ begin
90
+ results << JSON.parse(str)
91
+ return results if first_only
92
+ rescue JSON::ParserError => e
93
+ max_retries -= 1
94
+ raise e if max_retries <= 0
95
+
96
+ # Extract position from error message
97
+ pos_match = e.message.match(/at (?:line \d+, )?column (\d+)/)
98
+ error_pos = pos_match ? pos_match[1].to_i : nil
99
+
100
+ # Ruby's JSON parser reports 1-based column, convert to 0-based index
101
+ # Also try to find the actual position by looking for unexpected token position
102
+ if error_pos.nil? || error_pos == 0
103
+ # Try alternate pattern
104
+ pos_match = e.message.match(/position (\d+)/)
105
+ error_pos = pos_match ? pos_match[1].to_i : nil
106
+ end
107
+
108
+ raise e if error_pos.nil? || error_pos == 0
109
+
110
+ # For 1-based column, subtract 1 for 0-based index
111
+ error_pos -= 1 if error_pos > 0
112
+ queue.unshift(str[0...error_pos], str[error_pos..])
113
+ end
114
+ end
115
+
116
+ results
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stega
4
+ module Encoder
5
+ # Base-4 encoding uses 4 invisible characters
6
+ BASE4_CHARS = [
7
+ "\u200B", # ZERO WIDTH SPACE
8
+ "\u200C", # ZERO WIDTH NON-JOINER
9
+ "\u200D", # ZERO WIDTH JOINER
10
+ "\uFEFF" # ZERO WIDTH NO-BREAK SPACE (BOM)
11
+ ].freeze
12
+
13
+ # Prefix: 4 zero-width spaces used as a magic marker
14
+ STEGA_PREFIX = (BASE4_CHARS[0] * 4)
15
+
16
+ # Encode a value into an invisible steganographic string (base-4 encoding).
17
+ # Each byte is split into four 2-bit groups, each mapped to an invisible character.
18
+ def self.encode(value)
19
+ json = JSON.generate(value)
20
+ bytes = json.encode(Encoding::UTF_8).bytes
21
+
22
+ encoded = bytes.map do |byte|
23
+ BASE4_CHARS[(byte >> 6) & 3] +
24
+ BASE4_CHARS[(byte >> 4) & 3] +
25
+ BASE4_CHARS[(byte >> 2) & 3] +
26
+ BASE4_CHARS[byte & 3]
27
+ end.join
28
+
29
+ STEGA_PREFIX + encoded
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stega
4
+ module LegacyEncoder
5
+ # Unicode zero-width and invisible characters used for hex encoding
6
+ HEX_CHAR_MAP = {
7
+ "0" => 8203, # ZERO WIDTH SPACE
8
+ "1" => 8204, # ZERO WIDTH NON-JOINER
9
+ "2" => 8205, # ZERO WIDTH JOINER
10
+ "3" => 8290, # INVISIBLE TIMES
11
+ "4" => 8291, # INVISIBLE SEPARATOR
12
+ "5" => 8288, # WORD JOINER
13
+ "6" => 65279, # ZERO WIDTH NO-BREAK SPACE (BOM)
14
+ "7" => 8289, # INVISIBLE PLUS
15
+ "8" => 119_155, # MUSICAL SYMBOL BEGIN BEAM
16
+ "9" => 119_156, # MUSICAL SYMBOL END BEAM
17
+ "a" => 119_157, # MUSICAL SYMBOL BEGIN TIE
18
+ "b" => 119_158, # MUSICAL SYMBOL END TIE
19
+ "c" => 119_159, # MUSICAL SYMBOL BEGIN SLUR
20
+ "d" => 119_160, # MUSICAL SYMBOL END SLUR
21
+ "e" => 119_161, # MUSICAL SYMBOL BEGIN PHRASE
22
+ "f" => 119_162 # MUSICAL SYMBOL END PHRASE
23
+ }.freeze
24
+
25
+ # Legacy encoding: each ASCII character is encoded as two hex digits,
26
+ # each mapped to an invisible Unicode character.
27
+ def self.legacy_encode(value)
28
+ json = JSON.generate(value)
29
+
30
+ json.chars.map do |char|
31
+ code = char.ord
32
+ if code > 255
33
+ raise ArgumentError,
34
+ "Only ASCII edit info can be encoded. Error attempting to encode #{json} on character #{char} (#{code})"
35
+ end
36
+
37
+ hex = code.to_s(16).rjust(2, "0")
38
+ hex.chars.map { |hex_digit| [HEX_CHAR_MAP[hex_digit]].pack("U") }.join
39
+ end.join
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Stega
6
+ module Sanity
7
+ SKIP_KEYS = Set.new(%w[_id _type _ref _key _createdAt _updatedAt _rev _originalId _system slug]).freeze
8
+
9
+ class << self
10
+ def encode_source_map(result, source_map, config)
11
+ validate_config!(config)
12
+ return result unless source_map
13
+
14
+ encode_into_result(result, source_map, config)
15
+ end
16
+
17
+ private
18
+
19
+ def validate_config!(config)
20
+ raise TypeError, "enabled must be true" unless config[:enabled]
21
+ raise TypeError, "studio_url must be defined" unless config[:studio_url]
22
+ end
23
+
24
+ def encode_into_result(result, source_map, config)
25
+ documents = source_map[:documents] || []
26
+ paths = source_map[:paths] || []
27
+ mappings = (source_map[:mappings] || {}).transform_keys(&:to_s)
28
+
29
+ deep_transform(result, []) do |value, path|
30
+ json_path = to_json_path(path)
31
+ mapping, matched_path = resolve_mapping(json_path, mappings)
32
+
33
+ next value unless mapping && value.is_a?(String)
34
+ next value unless (mapping[:type] || "value") == "value"
35
+
36
+ source = mapping[:source]
37
+ next value unless source && source[:type] == "documentValue"
38
+
39
+ doc_index = source[:document]
40
+ path_index = source[:path]
41
+ document = documents[doc_index]
42
+
43
+ next value unless document
44
+
45
+ context = {
46
+ value: value,
47
+ path: path,
48
+ document: document
49
+ }
50
+
51
+ if config[:filter]
52
+ next value unless config[:filter].call(context)
53
+ end
54
+
55
+ source_path = paths[path_index]
56
+ full_path = resolve_full_source_path(source_path, json_path, matched_path)
57
+
58
+ edit_url = create_edit_url(
59
+ studio_url: config[:studio_url],
60
+ document: document,
61
+ path: full_path,
62
+ omit_cross_dataset: config[:omit_cross_dataset_reference_data]
63
+ )
64
+
65
+ payload = {"origin" => "sanity.io", "href" => edit_url}
66
+ Stega.combine(value, payload)
67
+ end
68
+ end
69
+
70
+ def deep_transform(obj, path, &block)
71
+ case obj
72
+ when Hash
73
+ obj.each_with_object({}) do |(key, value), result|
74
+ result[key] = if SKIP_KEYS.include?(key.to_s)
75
+ value
76
+ else
77
+ deep_transform(value, path + [key], &block)
78
+ end
79
+ end
80
+ when Array
81
+ obj.each_with_index.map do |value, index|
82
+ deep_transform(value, path + [index], &block)
83
+ end
84
+ else
85
+ yield(obj, path)
86
+ end
87
+ end
88
+
89
+ def create_edit_url(studio_url:, document:, path:, omit_cross_dataset: false)
90
+ doc_id = document[:_id]
91
+ doc_type = document[:_type]
92
+ project_id = document[:_projectId]
93
+ dataset = document[:_dataset]
94
+
95
+ studio_path = json_path_to_studio_path(path)
96
+
97
+ router_parts = [
98
+ "mode=presentation",
99
+ "id=#{doc_id}",
100
+ "type=#{doc_type}",
101
+ "path=#{URI.encode_www_form_component(studio_path)}"
102
+ ]
103
+ router_params = router_parts.join(";")
104
+
105
+ search_hash = {baseUrl: studio_url, id: doc_id, type: doc_type, path: studio_path, perspective: "previewDrafts"}
106
+ unless omit_cross_dataset
107
+ search_hash[:projectId] = project_id if project_id
108
+ search_hash[:dataset] = dataset if dataset
109
+ end
110
+ search_params = URI.encode_www_form(search_hash)
111
+
112
+ "#{studio_url}/intent/edit/#{router_params}?#{search_params}"
113
+ end
114
+
115
+ def json_path_to_studio_path(json_path)
116
+ rest = json_path.sub(/^\$/, "")
117
+ segments = rest.scan(/\['([^']*)'\]|\[(\d+)\]/)
118
+
119
+ result = +""
120
+ segments.each_with_index do |(key, index), i|
121
+ if index
122
+ result << "[#{index}]"
123
+ else
124
+ result << "." if i > 0
125
+ result << key
126
+ end
127
+ end
128
+ result
129
+ end
130
+
131
+ def resolve_mapping(json_path, mappings)
132
+ return [mappings[json_path], json_path] if mappings.key?(json_path)
133
+
134
+ segments = parse_path_segments(json_path)
135
+ (segments.length - 1).downto(1) do |i|
136
+ candidate = "$" + segments[0...i].join
137
+ return [mappings[candidate], candidate] if mappings.key?(candidate)
138
+ end
139
+
140
+ return [mappings["$"], "$"] if mappings.key?("$")
141
+
142
+ nil
143
+ end
144
+
145
+ def parse_path_segments(json_path)
146
+ json_path.sub(/^\$/, "").scan(/\['[^']*'\]|\[\d+\]/)
147
+ end
148
+
149
+ def resolve_full_source_path(source_path, result_path, matched_path)
150
+ matched_segments = parse_path_segments(matched_path)
151
+ result_segments = parse_path_segments(result_path)
152
+ suffix_segments = result_segments[matched_segments.length..]
153
+
154
+ if suffix_segments&.any?
155
+ source_path + suffix_segments.join
156
+ else
157
+ source_path
158
+ end
159
+ end
160
+
161
+ def to_json_path(path)
162
+ json_path = path.map do |segment|
163
+ if segment.is_a?(Integer) || segment =~ /^\d+$/
164
+ "[#{segment}]"
165
+ else
166
+ "['#{segment}']"
167
+ end
168
+ end.join("")
169
+ "$#{json_path}"
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stega # :nodoc:
4
+ VERSION = "1.0.0"
5
+ end
data/lib/stega.rb ADDED
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "uri"
5
+ require "date"
6
+
7
+ require "stega/version"
8
+ require "stega/encoder"
9
+ require "stega/legacy_encoder"
10
+ require "stega/decoder"
11
+ require "stega/sanity"
12
+
13
+ module Stega
14
+ # Regex to match encoded strings
15
+ REGEX = Decoder::REGEX
16
+
17
+ class << self
18
+ # Encode a value into an invisible steganographic string (base-4 encoding)
19
+ def encode(value)
20
+ Encoder.encode(value)
21
+ end
22
+
23
+ # Decode the first steganographic payload from a string
24
+ def decode(str)
25
+ Decoder.decode(str)
26
+ end
27
+
28
+ # Decode all steganographic payloads from a string
29
+ def decode_all(str)
30
+ Decoder.decode_all(str)
31
+ end
32
+
33
+ # Legacy encoding using hex pairs
34
+ def legacy_encode(value)
35
+ LegacyEncoder.legacy_encode(value)
36
+ end
37
+
38
+ # Combine a visible string with invisible encoded data
39
+ # mode can be :auto (default), :skip (true), or false
40
+ def combine(visible, data, mode = :auto)
41
+ skip = case mode
42
+ when true, :skip
43
+ true
44
+ when :auto
45
+ date_like?(visible) || url?(visible)
46
+ else
47
+ false
48
+ end
49
+
50
+ return visible if skip
51
+
52
+ "#{visible}#{encode(data)}"
53
+ end
54
+
55
+ # Split a string into its visible content and the encoded (invisible) portion
56
+ def split(str)
57
+ match = str.match(REGEX)
58
+ {
59
+ cleaned: str.gsub(REGEX, ""),
60
+ encoded: match ? match[0] : ""
61
+ }
62
+ end
63
+
64
+ # Deep-clean an object by removing all steganographic data
65
+ def clean(value)
66
+ return value unless value
67
+
68
+ JSON.parse(split(JSON.generate(value))[:cleaned])
69
+ end
70
+
71
+ private
72
+
73
+ # Check if a string looks like a date
74
+ def date_like?(str)
75
+ return false if str.nil? || str.empty?
76
+
77
+ # Not a date if it's a plain number
78
+ return false if numeric?(str)
79
+
80
+ # If it contains letters but doesn't match date pattern, not a date
81
+ if str.match?(/[a-z]/i) &&
82
+ !str.match?(/\d+(?:[-:\/]\d+){2}(?:T\d+(?:[-:\/]\d+){1,2}(\.\d+)?Z?)?/)
83
+ return false
84
+ end
85
+
86
+ # Try to parse as date
87
+ begin
88
+ Date.parse(str)
89
+ true
90
+ rescue ArgumentError, TypeError
91
+ false
92
+ end
93
+ end
94
+
95
+ # Check if a string is a valid URL
96
+ def url?(str)
97
+ return false if str.nil? || str.empty?
98
+
99
+ begin
100
+ if str.start_with?("/")
101
+ URI.parse("https://acme.com#{str}")
102
+ else
103
+ uri = URI.parse(str)
104
+ # Must have a scheme to be a valid URL
105
+ return false unless uri.scheme
106
+
107
+ uri
108
+ end
109
+ true
110
+ rescue URI::InvalidURIError
111
+ false
112
+ end
113
+ end
114
+
115
+ # Check if a string is numeric
116
+ def numeric?(str)
117
+ Float(str)
118
+ true
119
+ rescue ArgumentError, TypeError
120
+ false
121
+ end
122
+ end
123
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stega
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Theodor Tonum
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-01 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.15'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '1.15'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '3.9'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.9'
54
+ description: A Ruby implementation of Vercel's stega encoding for embedding invisible
55
+ data in strings, with Sanity source map support.
56
+ email:
57
+ - theodor@tonum.no
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - CHANGELOG.md
63
+ - LICENSE.txt
64
+ - README.md
65
+ - lib/stega.rb
66
+ - lib/stega/decoder.rb
67
+ - lib/stega/encoder.rb
68
+ - lib/stega/legacy_encoder.rb
69
+ - lib/stega/sanity.rb
70
+ - lib/stega/version.rb
71
+ homepage: https://github.com/rorkjop/stega
72
+ licenses:
73
+ - MIT
74
+ metadata:
75
+ bug_tracker_uri: https://github.com/rorkjop/stega/issues
76
+ changelog_uri: https://github.com/rorkjop/stega/blob/main/CHANGELOG.md
77
+ documentation_uri: https://github.com/rorkjop/stega
78
+ homepage_uri: https://github.com/rorkjop/stega
79
+ source_code_uri: https://github.com/rorkjop/stega
80
+ rubygems_mfa_required: 'true'
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '3.2'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubygems_version: 3.7.2
96
+ specification_version: 4
97
+ summary: Steganographic encoding and decoding for strings
98
+ test_files: []