gdbmish 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 670c5f7ee70fdebc740db0ac5c14a0615d80288e5049d1456ffc65b33677d639
4
+ data.tar.gz: 707565737415a2dcb4f4ff4293d124f27e6824ad10b59751e829ab6575006cfe
5
+ SHA512:
6
+ metadata.gz: d449655a416a969c09b2c8d55cf198d6177ff9b621d946aac1b54c6ec91e40fb37823c98f8af07168aea77d37b3b0d4508ff68ab5dd04b64fcb1feca1b26fffd
7
+ data.tar.gz: 817c74ee802e6376b4e921a692f7ef747ed98bc19009d132571f4d9b48ca9317f848b749d2a2fb7d8a1e94b4dda13f5353eff9acffca5eda8944dfb8b973faad
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.0
data/.yardopts ADDED
@@ -0,0 +1,4 @@
1
+ --protected
2
+ --embed-mixins
3
+ --markup markdown
4
+ --files LICENSE.txt,CHANGELOG.md,README.md
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-09-18
4
+
5
+ - Initial release
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Robert Schulze
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.
data/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # GDBMish
2
+
3
+ Create and read GDBM dump ASCII files.
4
+
5
+ Citing [gdbm](https://git.gnu.org.ua/gdbm.git/tree/NOTE-WARNING):
6
+ > Gdbm files have never been `portable' between different operating systems,
7
+ > system architectures, or potentially even different compilers. Differences
8
+ > in byte order, the size of file offsets, and even structure packing make
9
+ > gdbm files non-portable.
10
+ >
11
+ > Therefore, if you intend to send your database to somebody over the wire,
12
+ > please dump it into a portable format using gdbm_dump and send the resulting
13
+ > file instead. The receiving party will be able to recreate the database from
14
+ > the dump using the gdbm_load command.
15
+
16
+ GDBMish does that by reimplementing the `gdbm_dump` ASCII format without compiling against `gdbm`
17
+
18
+ ## Installation
19
+
20
+ Install the gem and add to the application's Gemfile by executing:
21
+
22
+ ```bash
23
+ bundle add gdbmish
24
+ ```
25
+
26
+ If bundler is not being used to manage dependencies, install the gem by executing:
27
+
28
+ ```bash
29
+ gem install gdbmish
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```ruby
35
+ require 'gdbmish'
36
+
37
+ # Create a dump into a StringIO and read it back
38
+ io = StringIO.new
39
+ dumper = Gdbmish::Dump::Ascii.new(file: "test.db", uid: "1000", user: "ziggy", gid: "1000", group: "staff", mode: 0o640)
40
+ dumper.dump(io) do |dump|
41
+ dump.push("some_key", "Some Value")
42
+ dump.push("otherKey", "Other\nValue")
43
+ end
44
+
45
+ io.rewind
46
+ reader = Gdbmish::Read::Ascii.new(io, load_meta: :count)
47
+ reader.data.to_h # => {"some_key"=>"Some Value", "otherKey"=>"Other\nValue"}
48
+ reader.meta.count # => 2
49
+ reader.meta.file # => "test.db"
50
+ reader.meta.uid # => "1000"
51
+
52
+ # Dumping a Hash
53
+
54
+ data = {"key1" => "value", "key2" => "value2"}
55
+
56
+ # Get dump as String
57
+ string = Gdbmish::Dump.ascii(data)
58
+
59
+ # Write directly into File (or any other IO)
60
+ File.open("my_db.dump", "w") do |file|
61
+ Gdbmish::Dump.ascii(data, file)
62
+ end
63
+
64
+ # Provide an original filename
65
+ Gdbmish::Dump.ascii(data, file: "my.db")
66
+
67
+ # Provide an original filename and file permissions
68
+ Gdbmish::Dump.ascii(data, file: "my.db", uid: "1000", gid: "1000", mode: 0o600)
69
+
70
+ # Iterate over a data source and push onto an IO
71
+ fileoptions = {file: "my.db", uid: "1000", user: "ziggy", gid: "1000", group: "staff", mode: 0o600}
72
+ File.open("my.dump", "w") do |file|
73
+ Gdbmish::Dump::Ascii.new(**fileoptions).dump(io) do |dump|
74
+ MyDataSource.each do |key, value|
75
+ dump.push(key.to_s, value.to_s)
76
+ end
77
+ end
78
+ end
79
+
80
+ # Read from a file
81
+
82
+ file = File.open("my.dump")
83
+ # The file is read lazily, don't close it before you're done reading from it
84
+ reader = Gdbmish::Read::Ascii.new(file)
85
+
86
+ # get meta data
87
+ reader.meta.file # => "my.db"
88
+
89
+ # either iterate over data:
90
+ reader.data do |key, value|
91
+ puts "#{key.inspect} => #{value.inspect}"
92
+ end
93
+
94
+ # or use the Iterator to transform into Hash
95
+ reader.data.to_h
96
+
97
+ file.close
98
+ ```
99
+
100
+ ### Shenanigans
101
+
102
+ Reading data from a dump file is a lazy one way street. Once data is read, it's read. You can't seek or peak.
103
+ However, you can rewind the `IO` and start over.
104
+
105
+ ```ruby
106
+ File.open("my.dump") do |io|
107
+ reader = Gdbmish::Read::Ascii.new(io)
108
+ reader.data.to_h # => {"key1"=>"value1", "key2"=>"value2"}
109
+ reader.data.to_h # => {}
110
+
111
+ io.rewind
112
+ reader.data.to_h # => {"key1"=>"value1", "key2"=>"value2"}
113
+
114
+ reader.data.first # => ["key1", "value1"]
115
+ reader.data.first # => ["key2", "value2"]
116
+ io.rewind
117
+ reader.data.first # => ["key1", "value1"]
118
+ end
119
+ ```
120
+
121
+ ## Development
122
+
123
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
124
+
125
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
126
+
127
+ ## Contributing
128
+
129
+ Bug reports and pull requests are welcome on GitHub at https://github.com/fnordfih/gdbmish.rb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/fnordfih/gdbmish.rb/blob/main/CODE_OF_CONDUCT.md).
130
+
131
+ ## License
132
+
133
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
134
+
135
+ ## Code of Conduct
136
+
137
+ Everyone interacting in the GDBMish project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/fnordfih/gdbmish.rb/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "standard/rake"
6
+ require "yard"
7
+
8
+ YARD::Rake::YardocTask.new
9
+
10
+ RSpec::Core::RakeTask.new(:spec)
11
+
12
+ task default: %i[spec standard]
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gdbmish
4
+ # Wrapper for different dump formats, providing various shortcut methods.
5
+ # Currently, there is only `Ascii` mode.
6
+ #
7
+ # Ascii mode optionally dumps file information such as filename, owner, mode.
8
+ # See {Gdbmish::Dump::Ascii#new} on how they are used.
9
+ module Dump
10
+ # Dumping GDBM data as ASCII (aka default) format.
11
+ class Ascii
12
+ # Appends and counts {#push}ed data as ASCII dump format onto the given `io`.
13
+ #
14
+ # @note
15
+ # Users should not use this class directly, as it only represents the
16
+ # data part of an dump, without header and footer.
17
+ #
18
+ # An instance of it gets yielded when using `Ascii#dump(io) { |appender| }`
19
+ # @see Ascii#dump Gdbmish::Dump::Ascii#dump(io) { |appender| }
20
+ class Appender
21
+ # @return [Integer] The number of key/value pairs pushed
22
+ attr_reader :count
23
+
24
+ # @param io [IO] The IO to append to
25
+ def initialize(io)
26
+ @io = io
27
+ @count = 0
28
+ end
29
+
30
+ # Push a *key*, *value* pair onto the dump
31
+ # @param key [String] The key
32
+ # @param value [String] The value
33
+ def push(key, value)
34
+ @count += 1
35
+ @io << dump_datum(key)
36
+ @io << dump_datum(value)
37
+ nil
38
+ end
39
+
40
+ private
41
+
42
+ def dump_datum(datum)
43
+ result = sprintf("#:len=%d\n", datum.bytesize)
44
+ encoded = Base64.strict_encode64(datum)
45
+ if encoded.size > GDBM_MAX_DUMP_LINE_LEN
46
+ encoded.gsub!(/(.{#{GDBM_MAX_DUMP_LINE_LEN}})/o, "\\1\n")
47
+ end
48
+ result << encoded << "\n"
49
+ end
50
+ end
51
+
52
+ # GDBM does not split base64 strings at 60 encoded characters (as defined by RFC 2045).
53
+ #
54
+ # See `_GDBM_MAX_DUMP_LINE_LEN` in [gdbmdefs.h](https://git.gnu.org.ua/gdbm.git/tree/src/gdbmdefs.h)
55
+ GDBM_MAX_DUMP_LINE_LEN = 76
56
+
57
+ # Builds a new Ascii format dumper
58
+ #
59
+ # Dumping file information is optional.
60
+ #
61
+ # - *uid*, *user*, *gid*, *group* and *mode* will only be used when *file* is given
62
+ # - *user* will only be used when *uid* is given
63
+ # - *group* will only be used when *gid* is given
64
+ #
65
+ # @example
66
+ # fileoptions = {file: "test.db", uid: "1000", user: "ziggy", gid: "1000", group: "staff", mode: 0o600}
67
+ # File.open("test.dump", "w") do |file|
68
+ # Gdbmish::Dump::Ascii.new(**fileoptions).dump(file) do |appender|
69
+ # MyDataSource.each do |key, value|
70
+ # appender.push(key.to_s, value.to_s)
71
+ # end
72
+ # end
73
+ # end
74
+ def initialize(file: nil, uid: nil, user: nil, gid: nil, group: nil, mode: nil)
75
+ @file = file
76
+ @uid = uid
77
+ @user = user
78
+ @gid = gid
79
+ @group = group
80
+ @mode = mode
81
+ end
82
+
83
+ # @overload dump(io)
84
+ # Dump only the header and footer
85
+ # @param io [IO] The IO to dump to
86
+ # @return [IO] The IO written to
87
+ # @overload dump(io, data)
88
+ # Dump *data* to *io*
89
+ # @param io [IO] The IO to dump to
90
+ # @param data [#each_pair] The data to dump
91
+ # @return [IO] The IO written to
92
+ # @overload dump(io, &block)
93
+ # Yield an {Appender} to call {Appender#push push} on.
94
+ # @param io [IO] The IO to dump to
95
+ # @yieldparam appender [Appender] The appender to push data onto the dump
96
+ # @return [IO] The IO written to
97
+ # @overload dump(io, data, &block)
98
+ # Dump *data* to *io* and then yield an {Appender} to call {Appender#push push} on.
99
+ # @param io [IO] The IO to dump to
100
+ # @param data [#each_pair] The data to dump
101
+ # @yieldparam appender [Appender] The appender to push data onto the dump
102
+ # @return [IO] The IO written to
103
+ def dump(io, data = nil, &block)
104
+ appender = Appender.new(io)
105
+
106
+ dump_header!(io)
107
+ data&.each_pair do |k, v|
108
+ appender.push(k.to_s, v.to_s)
109
+ end
110
+ yield appender if block
111
+ dump_footer!(io, appender.count)
112
+
113
+ io
114
+ end
115
+
116
+ private
117
+
118
+ def dump_header!(io)
119
+ io.printf("# GDBM dump file created by GDBMish version %s on %s\n", Gdbmish::VERSION, Time.now.rfc2822)
120
+ io.puts("#:version=1.1")
121
+
122
+ if @file
123
+ io.printf("#:file=%s\n", @file)
124
+ l = []
125
+
126
+ if @uid
127
+ l << sprintf("uid=%d", @uid)
128
+ l << sprintf("user=%s", @user) if @user
129
+ end
130
+
131
+ if @gid
132
+ l << sprintf("gid=%d", @gid)
133
+ l << sprintf("group=%s", @group) if @group
134
+ end
135
+
136
+ l << sprintf("mode=%03o", @mode & 0o777) if @mode
137
+
138
+ unless l.empty?
139
+ io << "#:"
140
+ io.puts(l.join(","))
141
+ end
142
+ end
143
+
144
+ io.puts("#:format=standard")
145
+ io.puts("# End of header")
146
+ end
147
+
148
+ def dump_footer!(io, count)
149
+ io.printf("#:count=%d\n", count)
150
+ io.puts("# End of data")
151
+ end
152
+ end
153
+
154
+ # Dump *data* as standard ASCII format.
155
+ # When an *io* is given, the dump will be written to it. Otherwise, a new `String` will be returned.
156
+ # See {Dump::Ascii#initialize} for *fileoptions*.
157
+ # See {Dump::Ascii#dump} for *data* and *block* behaviour.
158
+ #
159
+ # @param data [#each_pair,nil] The data to dump
160
+ # @param io [IO,nil] The IO to dump to
161
+ # @param fileoptions [Hash] Options for the dump file
162
+ # @yield [appender] The appender to push data onto the dump
163
+ # @return [String] The dump as a string when no *io* is given
164
+ # @return [IO] The IO written to when *io* is given
165
+ #
166
+ # @example Dumping simple Hash into an IO
167
+ # File.open("test.dump", "w") do |file|
168
+ # Gdbmish::Dump.ascii({some: "data"}, file)
169
+ # end
170
+ #
171
+ # @example Dumping into a String
172
+ # string_dump = Gdbmish::Dump.ascii({some: "data"})
173
+ #
174
+ # @example Dumping into IO with file information and block
175
+ # Gdbmish::Dump.ascii(io, file: "test.db", uid: "1000", user: "ziggy", gid: "1000", group: "staff", mode: 0o600) do |appender|
176
+ # MyData.each do |object|
177
+ # appender.push(object.id, object.dump)
178
+ # end
179
+ # end
180
+ def self.ascii(data = nil, io = nil, **fileoptions, &block)
181
+ if io.nil?
182
+ io = StringIO.new
183
+ to_string = true
184
+ end
185
+
186
+ io = Ascii.new(**fileoptions).dump(io, data, &block)
187
+ to_string ? io.string : io
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,225 @@
1
+ require "base64"
2
+
3
+ module Gdbmish
4
+ # Wrapper for reading GDBM dump files. Currently, only Ascii (aka standard) format is supported.
5
+ #
6
+ # See {Gdbmish::Read::Ascii} for the main high level interface.
7
+ module Read
8
+ # Main abstraction to read an GDBM Ascii dump file (aka "standard format")
9
+ #
10
+ # @example
11
+ # File.open("path/to/file.dump") do |io|
12
+ # file = Gdbmish::Read::Ascii.new(io)
13
+ #
14
+ # file.data do |key, value|
15
+ # puts "#{key.inspect} => #{value.inspect}"
16
+ # end
17
+ # pp file.meta
18
+ #
19
+ # # Note: your `io`'s file pointer posistion has changed.
20
+ # io.pos # => 317
21
+ # end
22
+ #
23
+ # # Produces:
24
+ # # "some_key" => "Some Value"
25
+ # # "otherKey" => "Other\nValue"
26
+ # #<Gdbmish::Read::AsciiMetaData @count=nil, @file="test.db", @gid="1000", @group="staff", @mode=384, @uid="1000", @user="ziggy", @version="1.1">
27
+ class Ascii
28
+ # Create a new Ascii reader.
29
+ #
30
+ # *load_meta* can be:
31
+ #
32
+ # - `true` (default) load meta data, but skip `count` for preformance reasons.
33
+ # - `false` skip loading meta data.
34
+ # - `:count` load meta data, including `count`.
35
+ #
36
+ # @param io [IO] The IO to read from. Assumed to point to the beginning of the GDBM dump.
37
+ # @param load_meta [Boolean, Symbol] Whether to load meta data.
38
+ # @param encoding [String,Encoding] The encoding to use for key/value pairs.
39
+ def initialize(io, load_meta: true, encoding: Encoding::UTF_8)
40
+ @io = io
41
+ @load_meta = load_meta
42
+ @encoding = encoding
43
+ end
44
+
45
+ # Returns an Enumerator over key/value pairs.
46
+ # Given a block, it will yield each key/value pair, which is a shortcut for `#data.each`.
47
+ #
48
+ # @note This will consume the IO, so you can only iterate once. After that, you need to rewind the IO.
49
+ #
50
+ # Depending on the size of the dataset, you might want to read everything into an Array or Hash:
51
+ # ```
52
+ # Ascii.new(io1).data.to_a # => [["some_key", "Some Value"], ["otherKey", "Other\nValue"]]
53
+ # Ascii.new(io2).data.to_h # => {"some_key" => "Some Value", "otherKey" => "Other\nValue"}
54
+ # ```
55
+ #
56
+ # @yield [key, value] for each key/value pair
57
+ # @yieldparam key [String] the key
58
+ # @yieldparam value [String] the value
59
+ # @return [AsciiDataIterator] if no block is given
60
+ def data(&block)
61
+ load_meta!
62
+ @data_iterator ||= AsciiDataIterator.new(@io, encoding: @encoding)
63
+ block ? @data_iterator.each(&block) : @data_iterator
64
+ end
65
+
66
+ # Parses for meta data, depending on the *load_meta* value in {#initialize}
67
+ # @return [AsciiMetaData,nil] the meta data if loaded
68
+ def meta
69
+ load_meta!
70
+ end
71
+
72
+ private
73
+
74
+ def load_meta!
75
+ return unless @load_meta
76
+
77
+ @meta ||= AsciiMetaData.parse(@io, ignore_count: @load_meta != :count)
78
+ end
79
+ end
80
+
81
+ # Header and footer meta data from a GDBM Ascii dump file.
82
+ class AsciiMetaData
83
+ attr_reader :version, :file, :uid, :user, :gid, :group, :mode, :count
84
+
85
+ def initialize(version: nil, file: nil, uid: nil, user: nil, gid: nil, group: nil, mode: nil, count: nil)
86
+ @version = version
87
+ @file = file
88
+ @uid = uid
89
+ @user = user
90
+ @gid = gid
91
+ @group = group
92
+ @mode = mode
93
+ @count = count
94
+ end
95
+
96
+ # Parse given IO for meta data.
97
+ # Reads from +io+ until a `"# End of header"` line is found (enhancing its `pos`).
98
+ # By default, ignores reading the `count` (indecating the amount of datasets in the file)
99
+ # because it is written at the end of the file.
100
+ def self.parse(io, ignore_count: true)
101
+ version = nil
102
+ file = nil
103
+ uid = nil
104
+ user = nil
105
+ gid = nil
106
+ group = nil
107
+ mode = nil
108
+ count = nil
109
+
110
+ while (line = io.gets(chomp: true))
111
+ break if line == "# End of header"
112
+
113
+ next unless line.start_with?("#:")
114
+
115
+ line[2..].split(",") do |e|
116
+ k, v = e.split("=")
117
+ case k
118
+ when "version"
119
+ version = v
120
+ when "file"
121
+ file = v
122
+ when "uid"
123
+ uid = v
124
+ when "user"
125
+ user = v
126
+ when "gid"
127
+ gid = v
128
+ when "group"
129
+ group = v
130
+ when "mode"
131
+ mode = v.to_i(8)
132
+ end
133
+ end
134
+ end
135
+
136
+ count = read_count(io) unless ignore_count
137
+
138
+ new(version: version, file: file, uid: uid, user: user, gid: gid, group: group, mode: mode, count: count)
139
+ end
140
+
141
+ def self.read_count(io)
142
+ count = nil
143
+ end_of_header_pos = begin
144
+ io.pos
145
+ rescue
146
+ # ignore error, this io does not support pos
147
+ nil
148
+ end
149
+
150
+ return if end_of_header_pos.nil?
151
+
152
+ while (line = io.gets(chomp: true))
153
+ next unless line.start_with?("#:count")
154
+ count = line.split("=")[1].to_i
155
+ end
156
+ io.pos = end_of_header_pos
157
+
158
+ count
159
+ end
160
+
161
+ private_class_method :read_count
162
+ end
163
+
164
+ # Iterates over lines, skipping comments, joining wrapped lines.
165
+ # Lines are alternating key or value in encoded form.
166
+ # @note Users should not need to use this directly, but rather {Gdbmish::Read::Ascii#data}.
167
+ class AsciiLineIterator
168
+ include Enumerable
169
+
170
+ # Comment lines start with '#'
171
+ COMMENT_BYTE = "#".ord
172
+
173
+ def initialize(io)
174
+ @io = io
175
+ end
176
+
177
+ # Iterates over encoded lines, skipping comments, joining wrapped lines.
178
+ # @yield [String] The still encoded, joined line
179
+ def each
180
+ while (line = @io.gets(chomp: true))
181
+ next if line.bytes.first == COMMENT_BYTE
182
+
183
+ data = line.dup
184
+
185
+ until next_is_comment?
186
+ data << @io.gets(chomp: true)
187
+ end
188
+
189
+ yield data
190
+ end
191
+ end
192
+
193
+ private
194
+
195
+ def next_is_comment?
196
+ next_byte = @io.readbyte
197
+ return false if next_byte.nil?
198
+
199
+ @io.seek(-1, IO::SEEK_CUR)
200
+ next_byte == COMMENT_BYTE
201
+ end
202
+ end
203
+
204
+ # Iterates over data and returns decoded key/value pairs.
205
+ # @note Users should not need to use this directly, but rather {Gdbmish::Read::Ascii#data}.
206
+ class AsciiDataIterator
207
+ include Enumerable
208
+
209
+ def initialize(io, encoding: Encoding::UTF_8)
210
+ @iterator = AsciiLineIterator.new(io)
211
+ @encoding = encoding
212
+ end
213
+
214
+ # Iterates over key/value pairs, decoding them.
215
+ # @yield [String,String] The decoded key and value
216
+ def each
217
+ @iterator.each_slice(2) do |k, v|
218
+ k = Base64.decode64(k).force_encoding(@encoding)
219
+ v = Base64.decode64(v).force_encoding(@encoding) unless v.nil?
220
+ yield [k, v]
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gdbmish
4
+ VERSION = "0.1.0"
5
+ end
data/lib/gdbmish.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "base64"
5
+ require "stringio"
6
+
7
+ module Gdbmish
8
+ end
9
+
10
+ require_relative "gdbmish/version"
11
+ require_relative "gdbmish/read"
12
+ require_relative "gdbmish/dump"
data/test.dump ADDED
File without changes
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gdbmish
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Robert Schulze
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-09-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: stringio
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: base64
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: time
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: GDBM database files are not portable between different architectures.
56
+ This gem reimplements the `gdbm_dump` and `gdbm_load` ASCII format in pure Ruby
57
+ to allow for easy creation and reading of portable GDBM dump files.
58
+ email:
59
+ - robert@dotless.de
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".rspec"
65
+ - ".standard.yml"
66
+ - ".yardopts"
67
+ - CHANGELOG.md
68
+ - CODE_OF_CONDUCT.md
69
+ - LICENSE.txt
70
+ - README.md
71
+ - Rakefile
72
+ - lib/gdbmish.rb
73
+ - lib/gdbmish/dump.rb
74
+ - lib/gdbmish/read.rb
75
+ - lib/gdbmish/version.rb
76
+ - test.dump
77
+ homepage: https://github.com/fnordfish/gdbmish.rb
78
+ licenses:
79
+ - MIT
80
+ metadata:
81
+ homepage_uri: https://github.com/fnordfish/gdbmish.rb
82
+ source_code_uri: https://github.com/fnordfish/gdbmish.rb
83
+ changelog_uri: https://github.com/fnordfish/gdbmish.rb/blob/main/CHANGELOG.md
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 3.0.0
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.5.17
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Create and read GDBM dump files.
103
+ test_files: []