gdbmish 0.1.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: 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: []