browser.rb 0.16.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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +516 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +21 -0
  5. data/README.md +116 -0
  6. data/Rakefile +182 -0
  7. data/browser.rb.gemspec +54 -0
  8. data/lib/Browser/Base.rb +50 -0
  9. data/lib/Browser/Bookmark.rb +41 -0
  10. data/lib/Browser/Bookmarks.rb +112 -0
  11. data/lib/Browser/Brave.rb +16 -0
  12. data/lib/Browser/Chrome.rb +16 -0
  13. data/lib/Browser/Chromium.rb +16 -0
  14. data/lib/Browser/ChromiumBased.rb +48 -0
  15. data/lib/Browser/Firefox.rb +16 -0
  16. data/lib/Browser/FirefoxBased.rb +86 -0
  17. data/lib/Browser/History.rb +41 -0
  18. data/lib/Browser/Rendering.rb +108 -0
  19. data/lib/Browser/Safari.rb +48 -0
  20. data/lib/Browser/TorBrowser.rb +26 -0
  21. data/lib/Browser/Unreadable.rb +11 -0
  22. data/lib/Browser/VERSION.rb +6 -0
  23. data/lib/browser-classes.rb +12 -0
  24. data/lib/browser.rb +29 -0
  25. data/test/Browser/Bookmark_test.rb +72 -0
  26. data/test/Browser/Brave_test.rb +143 -0
  27. data/test/Browser/Chrome_test.rb +143 -0
  28. data/test/Browser/Chromium_test.rb +143 -0
  29. data/test/Browser/Firefox_test.rb +205 -0
  30. data/test/Browser/Rendering_test.rb +68 -0
  31. data/test/Browser/Safari_test.rb +152 -0
  32. data/test/Browser/TorBrowser_test.rb +147 -0
  33. data/test/browser_test.rb +33 -0
  34. data/test/fixtures/Brave_bookmarks.json +47 -0
  35. data/test/fixtures/ChromiumBased_history.sqlite +0 -0
  36. data/test/fixtures/Chromium_bookmarks.json +47 -0
  37. data/test/fixtures/FirefoxBased_places.sqlite +0 -0
  38. data/test/fixtures/Safari_bookmarks.plist +0 -0
  39. data/test/fixtures/Safari_history.sqlite +0 -0
  40. data/test/gemspec_test.rb +42 -0
  41. data/test/loading_test.rb +35 -0
  42. metadata +194 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6d021a1b082f2676924ac7934a2c07630e3e1b9acf0fc6cc5199c089fcdf04e8
4
+ data.tar.gz: de83b9b9a841bcd7b1c8c22184d9b1d2f2e2f8a1c1010c57a15a1122ec8b2984
5
+ SHA512:
6
+ metadata.gz: 2b57c7138e471c8bff5954f33d6252fe1584fd6514836d03dd412b1b260726003894e3d8455c02e4218e952a8344ba750821724370461a0bcd6e29e576ab1ea4
7
+ data.tar.gz: c59caa0712477eb8bf6ec7fe5731e41b50bfa8a5b4fa23b5e0e943ce056e70f41a1ed911551a716b391d25f01aa77bb777aacd812a781c0f1eceb77f7a0eadd8
data/CHANGELOG ADDED
@@ -0,0 +1,516 @@
1
+ # browser/CHANGELOG
2
+
3
+ ## How this history is kept
4
+
5
+ The revisions behind 0.0.0 to 0.6.0 are the record and are not rewritten; the entries
6
+ describing them are narration, and may be corrected toward that record at any time.
7
+ Once a version is published that licence ends: what is published is history someone
8
+ else may hold, and stays.
9
+
10
+ ## Capabilities, and when each arrived
11
+
12
+ | capability | version | date |
13
+ |---|---|---|
14
+ | The six browsers named, with their data locations: Brave, Chrome, Chromium, Firefox, Safari, Tor Browser | 0.0.0 | 2022-06-22 |
15
+ | Bookmarks and history locations as methods on each browser | 0.1.0 | 2022-12-03 |
16
+ | One class per file; top-level names for a program that wants `Chrome` rather than `Browser::Chrome` | 0.3.0 | 2023-01-02 |
17
+ | History read from SQLite, rendered to CSV, written to a dated file | 0.4.0 | 2023-01-02 |
18
+ | A profile name selects the locations | 0.5.0 | 2023-09-23 |
19
+ | The profile name appears in the dumped filename | 0.5.2 | 2023-09-24 |
20
+ | Bookmarks read: JSON for the Chromium family, plist for Safari, SQLite for the Firefox family | 0.6.1 | 2026-08-22 |
21
+ | Four renderers for both bookmarks and history: CSV, JSON, plist, objects; `dump` takes a format | 0.6.1 | 2026-08-22 |
22
+ | Safari's binary plist read natively, via CFPropertyList | 0.6.1 | 2026-08-22 |
23
+ | MIT licence, and a LICENSE file | 0.6.1 | 2026-08-22 |
24
+ | `Browser.new(:name)`, any case, string or symbol; `browser-classes` for the top-level names | 0.7.0 | 2026-08-23 |
25
+ | `dump(format:, filename:, path:)` returns the path written and writes nothing if the render fails | 0.7.0 | 2026-08-23 |
26
+ | `readable?`, and `Browser::Unreadable` for data that is absent or locked by a running browser | 0.8.0 | 2026-08-23 |
27
+ | A CHANGELOG | 0.9.0 | 2026-08-23 |
28
+ | `Browser::Bookmark`, a tree; folders carried; `to_objects` flattens to title, url, folder | 0.10.0 | 2026-08-25 |
29
+ | Installable as a gem, standing alone without a RUBYLIB | 0.10.1 | 2026-09-04 |
30
+ | A README | 0.11.0 | 2026-09-07 |
31
+ | One set of renderers, shared by bookmarks and history | 0.12.0 | 2026-09-09 |
32
+ | The Firefox family's default profile, the one profiles.ini names | 0.13.0 | 2026-09-09 |
33
+ | Firefox and Tor Browser bookmarks with their folders | 0.14.0 | 2026-09-09 |
34
+
35
+
36
+ ## Capabilities, per browser, and when each arrived
37
+
38
+ The README's table, with each cell holding the version at which it became true.
39
+
40
+ | browser | bookmarks | history | folders | profile named | profile default |
41
+ |---|---|---|---|---|---|
42
+ | Brave | 0.6.1 | 0.4.0 | 0.10.0 | 0.5.0 | 0.0.0 |
43
+ | Chrome | 0.6.1 | 0.4.0 | 0.10.0 | 0.5.0 | 0.0.0 |
44
+ | Chromium | 0.6.1 | 0.4.0 | 0.10.0 | 0.5.0 | 0.0.0 |
45
+ | Firefox | 0.7.0 | 0.7.0 | 0.14.0 | 0.5.0 | 0.13.0 |
46
+ | Safari | 0.6.1 | 0.4.0 | 0.10.0 | none | none |
47
+ | Tor Browser | 0.7.0 | 0.7.0 | 0.14.0 | 0.7.0 | 0.13.0 |
48
+
49
+ ## 20260925
50
+
51
+ 0.16.0: ~ Browser::Rendering::CSV: Use CSV from the standard library; ~ Browser::Bookmarks.from_sqlite: Build the Bookmark object directly; ~ ROADMAP.md and TODO tidied.
52
+
53
+ 1. ~ Browser::Rendering::CSV#render: ::CSV.generate, the column names pushed and then each row, in place of joining Array#to_csv_row's.
54
+ 2. The escaping is no longer ours to get wrong, and the library quotes the fields which need it rather than all of them, so an ordinary row is now unquoted.
55
+ 3. Taken from the side branch at the tag history-formatter-20250923, whose History::Formatter::CSV rendered this way: its four lines, the results pair become records and column names.
56
+ 4. + browser.rb.gemspec: csv, which Rendering now requires and which is no default gem from Ruby 3.4.
57
+ 5. Found by installing the built gem into an empty GEM_HOME.
58
+ 6. - lib/Array/to_csv_row.rb, - lib/String/wrap.rb, which nothing requires now.
59
+ 7. With String#ascii_only gone at 0.15.0 the gem ships no monkeypatch at all, and lib/Array and lib/String go with them.
60
+ 8. ~ test/Browser/Rendering_test.rb: the renderings expected unquoted where a field needs no quoting.
61
+ 9. The three cases 0.15.0 added each still render and read back as they were.
62
+ 10. ~ test/loading_test.rb: that the library loads nothing from lib/Array or lib/String, in place of naming a helper which resolves from lib.
63
+ 11. ~ test/gemspec_test.rb: csv among the runtime dependencies.
64
+ 12. ~ ROADMAP.md: - whether a CSV library should do the escaping, which this answers.
65
+ 13. ~ TODO: - the monkeypatches, done; ~ the renderer, to be revisited if a better writer arrives.
66
+ 14. ~ CHANGELOG: How this history is kept, cut to the two rules.
67
+ 15. ~ the same: the capabilities table, one version and one date a row, the rows which held several split.
68
+ 16. ~ the same: - the note on cells later than the entries suggest, which 0.7.0's items already carry.
69
+ 17. ~ TODO: + readers for the bookmark tree and the profile directory; a history representation; the bookmark roots, dates, guid and separators; Tor Browser's bundle; a Browser::Error for Unreadable; the orphaned SQLite bookmark.
70
+ 18. ~ Browser::Bookmarks.from_sqlite: builds the root itself, as .from_json and .from_plist do; - sqlite_tree, its only caller.
71
+ 19. ~ ROADMAP.md: themes of work, in place of open questions.
72
+ 20. ~ the same: + cookies, a client for scraping, driving a browser; - Tor Browser's install; tags and the history record folded into the representation.
73
+ 21. ~ Browser::VERSION: /0.15.0/0.16.0/
74
+
75
+
76
+ 0.15.0: ~ Array#to_csv_row: a quote within a field doubled, and nothing stripped.
77
+
78
+ 1. ~ Array#to_csv_row: a quote within a field doubled, as RFC 4180 carries one.
79
+ 2. It had wrapped each field in quotes and left a quote inside it alone, so a title holding one rendered a row no reader could parse.
80
+ 3. Present since the method arrived at 0.4.0, in History's CSV from then and in Bookmarks' from 0.12.0.
81
+ 4. No test had ever rendered a field with a quote in it.
82
+ 5. ~ the same: - String#ascii_only, which had stripped every byte outside printable ASCII.
83
+ 6. So Café rendered as Caf, and a title in Japanese as an empty field.
84
+ 7. Quoting every field is what makes a comma or a newline safe; nothing was gained by dropping the rest.
85
+ 8. - lib/String/ascii_only.rb, which nothing requires now, as lib/String/pascalcase.rb went when its last caller did.
86
+ 9. It shipped, and monkeypatched String for anyone who loaded the gem.
87
+ 10. + test/Browser/Rendering_test.rb: a quote, a comma and a newline within a field, and a title which is not ASCII.
88
+ 11. Each rendered and then read back with the stdlib CSV; all three fail against the old method.
89
+ 12. ~ test/loading_test.rb: String/wrap in place of String/ascii_only as the helper it checks resolves from lib.
90
+ 13. + ROADMAP.md: whether a CSV library should do the escaping.
91
+ 14. ~ TODO: ~ the monkeypatches, two now; + ROADMAP.md on the escaping.
92
+ 15. ~ Browser::VERSION: /0.14.6/0.15.0/
93
+
94
+
95
+ ## 20260909
96
+
97
+ 0.14.6: + test/gemspec_test.rb and test/loading_test.rb, the checks a publish wants; the Gemfile stays.
98
+
99
+ 1. + test/gemspec_test.rb: the gemspec valid, pinning no date, and taking its version from Browser::VERSION.
100
+ 2. ~ the same: the three runtime and four development dependencies declared, and no file listed which is not there, as impuri, monotonic.rb, bitget.rb and duration.rb have.
101
+ 3. ~ test/loading_test.rb: + require 'browser' resolves its helpers from lib, and require 'browser-classes' gives the top-level names.
102
+ 4. ~ the same: + both load without warning under -w, beside the Browser::VERSION example 0.14.4 began the file with.
103
+ 5. As monotonic.rb and duration.rb have. The ascii_only fault, resolved only from ~/lib/ruby until 0.10.1, would have failed here.
104
+ 6. ~ ROADMAP.md: - whether the Gemfile goes. It stays.
105
+ 7. Six of seven recent gems carry one reading gemspec, statistics.rb alone does not, and the question had taken the outlier for the rule.
106
+ 8. ~ Browser::VERSION: /0.14.5/0.14.6/
107
+
108
+
109
+ 0.14.5: Ruby 3.2 or later, which is what the dependencies require.
110
+
111
+ 1. ~ browser.rb.gemspec: required_ruby_version, '>= 2.7' --> '>= 3.2'.
112
+ 2. CFPropertyList 4.0.0 and sqlite3 2.9.6 each require Ruby 3.2, so on 2.7 the install failed on the dependencies.
113
+ 3. Found by installing for Ruby 2.7.8 into an empty GEM_HOME.
114
+ 4. Every file's syntax is fine under 2.7; the floor is the dependencies'.
115
+ 5. The suite passes under 3.4.1, the nearest Ruby to it here with an interpreter.
116
+ 6. ~ Browser::VERSION: /0.14.4/0.14.5/
117
+
118
+
119
+ 0.14.4: Browser::VERSION defined by requiring the library.
120
+
121
+ 1. ~ lib/browser.rb: + require_relative './Browser/VERSION'.
122
+ 2. Only the gemspec had required it, so the installed gem did not know its version: require 'browser', Browser::VERSION raised NameError.
123
+ 3. Found by installing the built gem into an empty GEM_HOME, which is the check a publish wants.
124
+ 4. + test/loading_test.rb: require 'browser' defines Browser::VERSION, in a process of its own with lib alone on the load path.
125
+ 5. Within the suite's own process everything is loaded already, so no test there could have failed.
126
+ 6. ~ Browser::VERSION: /0.14.3/0.14.4/
127
+
128
+
129
+ 0.14.3: The gemspec's summary and description say what the gem does, and the README carries the header's History.
130
+
131
+ 1. ~ browser.rb.gemspec: the summary, which claimed writing where there is none yet, now says reading.
132
+ 2. ~ the same: the description, the summary again and RubyGems warning of it, now names the six browsers and the three renderings.
133
+ 3. ~ README.md: + History, the paragraph the load file's header had carried since 0.0.0, in its author's words.
134
+ 4. ~ lib/browser.rb: - that paragraph from the header, as exitor's went into its README.
135
+ 5. ~ TODO: - the two, done.
136
+ 6. ~ Browser::VERSION: /0.14.2/0.14.3/
137
+
138
+
139
+ 0.14.2: Browser::Base#name public, and the TODO brought up to date.
140
+
141
+ 1. ~ Browser::Base#name: public, the renderers and the tests having reached for it with send; it was private for no reason anyone could name.
142
+ 2. ~ Browser::Rendering#dump_filename: asks the browser its name, in place of the send.
143
+ 3. ~ test/Browser/*_test.rb: #name called, in place of sent.
144
+ 4. ~ TODO: - dump_filename's sends, done; ~ the monkeypatches, a better writer taking the three, in place of the stdlib's CSV.
145
+ 5. ~ Browser::VERSION: /0.14.1/0.14.2/
146
+
147
+
148
+ 0.14.1: + ROADMAP.md and TODO, and the capabilities per browser pinned to their versions.
149
+
150
+ 1. ~ CHANGELOG: + How this history is kept, at the head.
151
+ 2. ~ the same: + Capabilities, per browser, and when each arrived.
152
+ 3. + ROADMAP.md
153
+ 4. + TODO
154
+ 5. ~ lib/browser.rb: - the header's Todo, its two items now in ROADMAP.md.
155
+ 6. ~ Browser::VERSION: /0.14.0/0.14.1/
156
+
157
+
158
+ 0.14.0: Firefox and Tor Browser bookmarks with their folders.
159
+
160
+ 1. ~ Browser::Bookmarks.from_sqlite: builds the tree, hanging children under parents from the root down, in position order.
161
+ 2. moz_bookmarks is one table of folders and bookmarks, each row naming its parent and its position within it.
162
+ 3. ~ the same: separators skipped, and the tags root left out, a tag being a folder holding a copy of every bookmark so tagged.
163
+ 4. ~ the same: menu, toolbar, unfiled and mobile shown as Bookmarks Menu, Bookmarks Toolbar, Other Bookmarks and Mobile Bookmarks.
164
+ 5. Until now the query fetched leaves alone, and every folder came back empty.
165
+ 6. ~ Browser::Bookmark: + name, what a browser stores, which is the title but for Firefox's roots.
166
+ 7. The title is what it shows, and what a folder path is made of; writing back into a browser would want the name.
167
+ 8. ~ Browser::FirefoxBased.bookmarks_sql: every row with its parent, position, type and guid, in place of the leaves ordered by date.
168
+ 9. ~ Rakefile, test/fixtures/FirefoxBased_places.sqlite: + a tag folder, a separator, and a toolbar bookmark, pinning the three rules.
169
+ 10. ~ test/Browser/Firefox_test.rb, TorBrowser_test.rb: the five bookmarks in tree order with their folders.
170
+ 11. ~ README.md: "yes" for Firefox and Tor Browser's folders; - the caveat that they were flat.
171
+ 12. ~ Browser::VERSION: /0.13.0/0.14.0/
172
+
173
+
174
+ 0.13.0: Firefox and Tor Browser's default profile, the one profiles.ini names.
175
+
176
+ 1. + Browser::FirefoxBased.default_profile_name: the profile Firefox itself would open, read from profiles.ini.
177
+ 2. The [Install] section's, as Firefox has written since 67; else the [Profile] flagged Default=1; else the only one there.
178
+ 3. No profiles.ini, or two installations each naming a default, raises Browser::Unreadable, which asks for profile_name:.
179
+ 4. - Browser::FirefoxBased::DEFAULT_PROFILE_NAME, the glob "**", which File.expand_path never expanded.
180
+ 5. An unnamed Firefox read a path with ** in it, and dumped to Firefox_**_Bookmarks_<date>.
181
+ 6. + Browser::TorBrowser.profiles_ini_path: beside its profiles, where Firefox keeps its own beside Profiles.
182
+ 7. + Browser::Base.default_profile_name, DEFAULT_PROFILE_NAME for the browsers whose default is a constant.
183
+ 8. ~ Browser::Base#profile_name, #bookmarks_location, #history_location: readers, each derived when first wanted.
184
+ 9. So making a browser reads nothing, and where no default can be named readable? answers false rather than construction raising.
185
+ 10. - Browser::Base: the attr_accessor, whose writers nothing used.
186
+ 11. ~ test/Browser/Firefox_test.rb, TorBrowser_test.rb: .default_profile_name over profiles.ini texts, [Install] winning over the flag.
187
+ 12. ~ the same two: the locations for a named profile, in place of the glob.
188
+ 13. ~ test/Browser/Brave_test.rb, Chrome_test.rb, Chromium_test.rb: the profile name read rather than assigned.
189
+ 14. ~ README.md: "default, or named" for Firefox and Tor Browser, and the usage and caveat to match.
190
+ 15. ~ Browser::VERSION: /0.12.0/0.13.0/
191
+
192
+
193
+ 0.12.0: + Browser::Rendering, the renderers once, shared by Bookmarks and History.
194
+
195
+ 1. + lib/Browser/Rendering.rb: the CSV, JSON and Plist renderers, over records and their column names.
196
+ 2. ~ the same: + to_csv, to_json, to_plist, dump and readable?.
197
+ 3. Bookmarks and History include it, each supplying results, to_objects and column_names.
198
+ 4. - Browser::Bookmarks::CSV, ::JSON, ::Plist; - Browser::History::CSV, ::JSON, ::Plist, ::Objects: the two sets it replaces.
199
+ 5. They had drifted: History's unpacked the column names and rows in each renderer and went through Objects for records, and Bookmarks' CSV wrote its three fields unquoted.
200
+ 6. ~ Browser::History#to_objects: the records made once, and what the renderers take.
201
+ 7. ~ Browser::History#results: memoises the query as it came.
202
+ 8. ~ Browser::Bookmarks#to_csv: every field quoted, as History's have been.
203
+ 9. The other 21 renderings across the three fixtures are byte for byte what they were.
204
+ 10. ~ Browser::Rendering#dump_filename: the parts joined, a nil profile dropping out, in place of two interpolations.
205
+ 11. + test/Browser/Rendering_test.rb
206
+ 12. ~ Browser::VERSION: /0.11.2/0.12.0/
207
+
208
+
209
+ ## 20260908
210
+
211
+ 0.11.2: + Capabilities, and when each arrived, at the head of the CHANGELOG.
212
+
213
+ 1. ~ CHANGELOG: + Capabilities, and when each arrived, at the head.
214
+ 2. ~ Browser::VERSION: /0.11.1/0.11.2/
215
+
216
+ 0.11.1: ~ browser.rb.gemspec, brought into line with the others.
217
+
218
+ 1. ~ browser.rb.gemspec: + the header line naming the file.
219
+ 2. ~ the same: + #dependencies= and #development_dependencies=, the gems as words, in place of an add_dependency line apiece.
220
+ 3. ~ the same: require_paths beside required_ruby_version, rather than after the files.
221
+ 4. ~ the same: the gemspec first among the files.
222
+ 5. ~ Browser::VERSION: /0.11.0/0.11.1/
223
+
224
+
225
+ ## 20260907
226
+
227
+ 0.11.0: + README.md
228
+
229
+ 1. + README.md
230
+ 2. ~ the Tor Browser caveat, corrected before it went in: it had said the library follows the install, where root_path is one fixed path.
231
+ 3. It now says where the library looks, and that an install elsewhere wants bookmarks_location: and history_location:.
232
+ 4. ~ browser.rb.gemspec: homepage, /browser.rb/browser/ and https, as http.rb's repository is http.
233
+ 5. ~ README.md: the fork link, the same.
234
+ 6. ~ Browser::VERSION: /0.10.1/0.11.0/
235
+
236
+
237
+ ## 20260904
238
+
239
+ 0.10.1: Ship String#ascii_only, so the gem loads without a RUBYLIB.
240
+
241
+ 1. + lib/String/ascii_only.rb, required by Array#to_csv_row and resolved until now from ~/lib/ruby alone.
242
+ 2. Requiring Browser::History therefore raised LoadError on any machine but the author's.
243
+ 3. ~ Browser::VERSION: /0.10.0/0.10.1/
244
+
245
+
246
+ ## 20260825
247
+
248
+ 0.10.0: + Browser::Bookmark, the internal representation, and a lowercase load file.
249
+
250
+ 1. + lib/Browser/Bookmark.rb: a bookmark which holds bookmarks; a folder has children, a leaf a url.
251
+ 2. ~ Browser::Bookmarks.from_json, .from_plist, .from_sqlite: a Browser::Bookmark tree, in place of the raw parse.
252
+ 3. ~ the same three: Chromium's and Safari's trees carry their folders.
253
+ 4. ~ Browser::Bookmarks.from_plist: the History proxy skipped, and a leaf sitting at the top level taken.
254
+ 5. ~ Browser::Bookmarks.from_sqlite: Firefox and Tor Browser flat still, the query fetching leaves alone.
255
+ 6. ~ Browser::Bookmarks#to_objects: the tree flattened to title, url and folder, which the three renderers take.
256
+ 7. ~ Browser::Bookmarks#to_csv: it had matched symbol keys against a string-keyed hash, so every row was empty commas.
257
+ 8. - Browser::Bookmarks::Objects, which the flatten replaced.
258
+ 9. - Browser::Bookmarks: require 'Array/to_csv_row', which nothing there used.
259
+ 10. /lib/Browser.rb/lib/browser.rb/, /test/Browser_test.rb/test/browser_test.rb/, require 'Browser' --> require 'browser'.
260
+ 11. The uppercase name folded together with the browser gem on a case-insensitive filesystem.
261
+ 12. ~ lib/browser.rb: - the header's version and Changes block, VERSION.rb and this CHANGELOG being the one of each.
262
+ 13. + test/Browser/Bookmark_test.rb
263
+ 14. ~ test/Browser/*_test.rb: #bookmarks checked through to_objects, in place of a raw round-trip.
264
+ 15. ~ Browser::VERSION: /0.9.0/0.10.0/
265
+
266
+
267
+ ## 20260823
268
+
269
+ 0.9.0: + CHANGELOG
270
+
271
+ 1. + CHANGELOG, written by changelogger 0.13.0, and into every earlier revision with it.
272
+ 2. ~ browser.rb.gemspec: the test glob widened to Dir['test/**/*'], so the fixtures ship.
273
+ 3. ~ browser.rb.gemspec: /'CHANGELOG.md'/'CHANGELOG'/, the name the file has.
274
+ 4. ~ lib/Browser.rb: /Changes since 0.7/Changes since 0.8/
275
+
276
+
277
+ 0.8.0: Make reading trustworthy, and say so when it is not.
278
+
279
+ 1. + lib/Browser/Unreadable.rb: Browser::Unreadable
280
+ 2. ~ Browser::Bookmarks#results, Browser::History#results: rescue IOError, SystemCallError, SQLite3::Exception into it.
281
+ 3. + Browser::Bookmarks#readable?, Browser::History#readable?
282
+ 4. + test/fixtures/FirefoxBased_places.sqlite
283
+ 5. + Rakefile: a task building it.
284
+ 6. ~ test/Browser/Firefox_test.rb, TorBrowser_test.rb
285
+ 7. + tests for #readable? and Browser::Unreadable
286
+ 8. ~ lib/Browser.rb: /Changes since 0.6/Changes since 0.7/
287
+
288
+
289
+ ## 20260822
290
+
291
+ 0.7.0: Settle how a browser is named and constructed.
292
+
293
+ 1. - Browser, the delegating class, which called class methods removed in 0.6.
294
+ 2. + Browser.new(name): any case, returning an instance of that browser's class.
295
+ 3. ~ Browser::Base: < Browser, so that a browser is_a? Browser.
296
+ 4. /lib/TopLevelBrowser.rb/lib/browser-classes.rb/
297
+ 5. ~ browser-classes.rb: assigns rather than subclasses.
298
+ 6. - Browser::*.bookmarks_location, .history_location: the location arguments.
299
+ 7. ~ Browser::Base#initialize takes them instead.
300
+ 8. ~ Browser::FirefoxBased.bookmarks_sql, .history_sql: + the join to moz_places, which holds the url.
301
+ 9. ~ the same two: + a real column to order by.
302
+ 10. ~ Browser::TorBrowser.profiles_path: Browser rather than Firefox's Profiles.
303
+ 11. ~ Browser::Bookmarks#dump, Browser::History#dump: + filename:, + path:
304
+ 12. ~ the same two: return the path written, and write nothing when the render raises.
305
+ 13. - Browser::History#dump_filename: String#pascalcase, a no-op for every browser name.
306
+ 14. - lib/String/pascalcase.rb, which nothing else required.
307
+
308
+
309
+ 0.6.1: The 0.6.0 design made to run, and its tests brought up to it.
310
+
311
+ 1. ~ Browser::Bookmarks, Browser::History: the location given was stored and ignored, in five places.
312
+ 2. ~ Browser::Bookmarks.from_json, .from_plist: JSON and Plist named the nested renderers.
313
+ 3. ~ Browser::Bookmarks.from_sqlite: opened the history database, ran the history query.
314
+ 4. ~ Browser::Bookmarks#to_objects: the instance where the class was meant.
315
+ 5. ~ browser.rb.gemspec: /plist/CFPropertyList/, plist reading XML alone, Safari writing binary.
316
+ 6. + lib/Browser/VERSION.rb, which the gemspec required and which was never there.
317
+ 7. + browser.rb.gemspec: sqlite3, required and undeclared, so a consumer met LoadError.
318
+ 8. ~ browser.rb.gemspec: /browser/browser.rb/, the shorter name taken.
319
+ 9. /browser.gemspec/browser.rb.gemspec/
320
+ 10. + Rakefile: the test task, and two building SQLite fixtures.
321
+ 11. + test/fixtures/ChromiumBased_history.sqlite, Safari_bookmarks.plist, Safari_history.sqlite
322
+ 12. ~ test/Browser/*_test.rb: converted from the pre-rework API.
323
+ 13. - Gemfile.lock, three years older than the gemspec, never resolved against it.
324
+ 14. - browser.rb.gemspec: spec.date, a release planned for 2025-09-27 which did not happen.
325
+ 15. ~ browser.rb.gemspec: /Ruby/MIT/; + LICENSE, in the files.
326
+ 16. + .gitignore: !test/fixtures/*.sqlite, data rather than build output.
327
+
328
+
329
+ ## 20250923
330
+
331
+ Renderers under Formatter, CSV through the stdlib, and a new dump.
332
+
333
+ 1. + Browser::History::Formatter, wrapping CSV, JSON, Plist and Objects.
334
+ 2. ~ Formatter::CSV#render: ::CSV.generate, in place of Array#to_csv_row.
335
+ 3. Only the fields which need it quoted, and a quote within one doubled.
336
+ 4. ~ Browser::History#dump: + filename:, + path:
337
+ 5. ~ the same: returns the path, and rescues Errno::ENOENT.
338
+ 6. + require 'csv'; - require 'Array/to_csv_row'
339
+ 7. A scratch copy, on 0.6.0's tree, written that evening, and it entered no version as it stands.
340
+ 8. JSON, Plist and Objects went into 0.6.0 the next day, byte for byte.
341
+ 9. dump into 0.7.0 eleven months later, byte for byte.
342
+ 10. The state is at the tag history-formatter-20250923.
343
+
344
+
345
+ ## 20250925
346
+
347
+ 0.6.0: + Bookmarks
348
+
349
+ 1. + Browser::Bookmarks.from_json, .from_plist, .from_sqlite
350
+ 2. + Browser::Bookmarks: CSV, JSON, Plist, Objects, nested
351
+ 3. + Browser::Bookmarks#to_csv, #to_json, #to_plist, #to_objects
352
+ 4. + Browser::Bookmarks#dump(format), #dump_filename
353
+ 5. + Browser::History: the same four renderers, nested
354
+ 6. + Browser::History#to_json, #to_plist, #to_objects
355
+ 7. + Browser::Base#bookmarks, #history
356
+ 8. - Browser::Base.bookmarks, .bookmarks_json, .bookmarks_plist, .bookmarks_sql
357
+ 9. - Browser::Base.bookmarks_location, .history_location, .history_sql, .history
358
+ 10. - Browser::Base#history_sql
359
+ 11. ~ Browser::Base#initialize: + DEFAULT_PROFILE_NAME where none is given
360
+ 12. + ChromiumBased, FirefoxBased, Safari: root_path, profiles_path, private
361
+ 13. + ChromiumBased, FirefoxBased, Safari: .bookmarks_format, .history_format
362
+ 14. + ChromiumBased, FirefoxBased, Safari: DEFAULT_PROFILE_NAME, Default, ** and nil
363
+ 15. - ChromiumBased, FirefoxBased, Safari: .bookmarks, .bookmarks_json, .bookmarks_plist
364
+ 16. ~ Brave, Chrome, Chromium, Firefox, TorBrowser: root_path, in place of PROFILE_ROOT_PATH
365
+ 17. ~ Safari: root_path, in place of BOOKMARKS_LOCATION and HISTORY_LOCATION
366
+ 18. private throughout lib: 20 in 12 files, where there had been 2 in 2
367
+ 19. + browser.gemspec
368
+ 20. ~ Gemfile: gemspec, in place of the three gems listed
369
+ 21. ~ test/Browser/Brave_test.rb: converted to the reworked API
370
+ 22. ~ the other five tests: .bookmarks_location, .history_location, in place of PROFILE_ROOT_PATH
371
+ 23. ~ lib/Browser.rb: /Changes since 0.4/Changes since 0.5/
372
+ 24. ~ Browser#history: + a bookmarks location, both words misspelt
373
+
374
+
375
+ ## 20250922
376
+
377
+ 0.5.3: Remove unnecessary delegate method.
378
+
379
+ 1. - Browser#delegate
380
+ 2. ~ Browser#bookmarks: /delegate.class/delegate_class/
381
+ 3. ~ Browser#bookmarks_json: /delegate.class/delegate_class/
382
+ 4. ~ Browser#bookmarks_plist: /delegate.class/delegate_class/
383
+ 5. ~ Browser#bookmarks_sql: /delegate.class/delegate_class/
384
+ 6. ~ Browser#history_sql: /delegate.class/delegate_class/
385
+ 7. ~ Browser#history: /delegate.class/delegate_class/
386
+
387
+
388
+ ## 20230924
389
+
390
+ Locations onto the family classes, neither pair reachable.
391
+
392
+ 1. + Browser::ChromiumBased.bookmarks_location, .history_location, each with binding.pry.
393
+ 2. + Browser::FirefoxBased.bookmarks_location, .history_location
394
+ 3. Neither pair is reachable, every browser defining its own.
395
+ 4. ChromiumBased's name profile_root_path and default_profile_name, which nothing defines.
396
+ 5. FirefoxBased's name PROFILE_ROOT_PATH and DEFAULT_PROFILE_NAME, defined in the browsers where a parent cannot see them.
397
+ 6. The copy which ran from ~/lib/ruby, on 0.5.2's tree.
398
+ 7. Two files of it, written that afternoon, and it entered no version.
399
+ 8. The state is at the tag family-locations-20230924.
400
+
401
+
402
+ 0.5.2: Add the profile_name to the dumped history filename.
403
+
404
+ 1. ~ Browser#history: + profile_name argument
405
+ 2. ~ Browser::Base#history: + profile_name argument
406
+ 3. ~ Browser::History#csv_filename: + profile_name in the filename
407
+
408
+
409
+ 0.5.1: Using the DEFAULT_PROFILE_NAME as the default value for the profile_name argument doesn't seem to work, so assigning the default value in a separate line.
410
+
411
+ 1. ~ Browser::Brave#bookmarks_location: /DEFAULT_PROFILE_NAME/nil/
412
+ 2. ~ Browser::Brave#history_location: /DEFAULT_PROFILE_NAME/nil/
413
+ 3. ~ Browser::Chrome#bookmarks_location: /DEFAULT_PROFILE_NAME/nil/
414
+ 4. ~ Browser::Chrome#history_location: /DEFAULT_PROFILE_NAME/nil/
415
+ 5. ~ Browser::Chromium#bookmarks_location: /DEFAULT_PROFILE_NAME/nil/
416
+ 6. ~ Browser::Chromium#history_location: /DEFAULT_PROFILE_NAME/nil/
417
+ 7. ~ Browser::Firefox#bookmarks_location: /DEFAULT_PROFILE_NAME/nil/
418
+ 8. ~ Browser::Firefox#history_location: /DEFAULT_PROFILE_NAME/nil/
419
+ 9. ~ Browser::Safari#bookmarks_location: /DEFAULT_PROFILE_NAME/nil/
420
+ 10. ~ Browser::Safari#history_location: /DEFAULT_PROFILE_NAME/nil/
421
+ 11. ~ Browser::TorBrowser#bookmarks_location: /DEFAULT_PROFILE_NAME/nil/
422
+ 12. ~ Browser::TorBrowser#history_location: /DEFAULT_PROFILE_NAME/nil/
423
+
424
+
425
+ ## 20230923
426
+
427
+ 0.5.0: Use a supplied profile name to determine bookmarks and history locations.
428
+
429
+ 1. ~ Browser#initialize: + profile_name
430
+ 2. + Browser#bookmarks_location, so that it may be derived from the profile name in the concrete classes.
431
+ 3. + Browser#history_location, so that it may be derived from the profile name in the concrete classes.
432
+ 4. ~ Browser#delegate: + profile_name
433
+ 5. ~ Browser::Base#initialize: + profile_name
434
+ 6. ~ Browser::Brave#bookmarks_location: + profile_name
435
+ 7. ~ Browser::Brave#history_location: + profile_name
436
+ 8. ~ Browser::Chrome#bookmarks_location: + profile_name
437
+ 9. ~ Browser::Chrome#history_location: + profile_name
438
+ 10. ~ Browser::Chromium#bookmarks_location: + profile_name
439
+ 11. ~ Browser::Chromium#history_location: + profile_name
440
+ 12. ~ Browser::Firefox#bookmarks_location: + profile_name
441
+ 13. ~ Browser::Firefox#history_location: + profile_name
442
+ 14. ~ Browser::Safari#bookmarks_location: + profile_name
443
+ 15. ~ Browser::Safari#history_location: + profile_name
444
+ 16. ~ Browser::TorBrowser#bookmarks_location: + profile_name
445
+ 17. ~ Browser::TorBrowser#history_location: + profile_name
446
+
447
+
448
+ ## 20230921
449
+
450
+ 0.4.1: Mark the location constants as defaults.
451
+
452
+ 1. ~ ChromiumBased subclasses: /BOOKMARKS_LOCATION/DEFAULT_BOOKMARKS_LOCATION/
453
+ 2. ~ ChromiumBased subclasses: /HISTORY_LOCATION/DEFAULT_HISTORY_LOCATION/
454
+
455
+
456
+ ## 20230102
457
+
458
+ 0.4.0: + Browser::History
459
+
460
+ 1. + Browser::History
461
+
462
+
463
+ 0.3.1: Made the tests uniform.
464
+
465
+ 1. Made the tests uniform.
466
+
467
+
468
+ 0.3.0: Move each class to its own file.
469
+
470
+ 1. Moved Browser::Base to own file.
471
+ 2. Moved Browser::Brave to own file.
472
+ 3. Moved Browser::Chrome to own file.
473
+ 4. Moved Browser::Chromium to own file.
474
+ 5. Moved Browser::ChromiumBased to own file.
475
+ 6. Moved Browser::Firefox to own file.
476
+ 7. Moved Browser::FirefoxBased to own file.
477
+ 8. Moved Browser::Safari to own file.
478
+ 9. Moved Browser::TorBrowser to own file.
479
+ 10. + TopLevelBrowser.rb.
480
+ 11. Moved tests to Browser directory.
481
+ 12. + Firefox test.
482
+ 13. + Safari test.
483
+ 14. + TorBrowser test.
484
+
485
+
486
+ ## 20221205
487
+
488
+ 0.2.0: Read a bookmarks file, and split the tests per browser.
489
+
490
+ 1. + Browser::Base.bookmarks, .bookmarks_json, .bookmarks_plist, each taking a location.
491
+ 2. + Browser::Base.bookmarks_location, .bookmarks_sql, .history_location, .history_sql
492
+ 3. - Browser::Base.bookmarks_path, .history_path
493
+ 4. ~ every browser: class << self in place of def self.
494
+ 5. ~ Brave, Chrome, Chromium: .bookmarks_location, .history_location default to the constants.
495
+ 6. + String#camelcase, in Browser.rb
496
+ 7. + require 'json', 'Object/to_const', 'shellwords'
497
+ 8. + test/Brave_test.rb, Chrome_test.rb, Chromium_test.rb, out of Browser_test.rb
498
+ 9. + test/fixtures/Brave_bookmarks.json, Chromium_bookmarks.json
499
+
500
+
501
+ ## 20221203
502
+
503
+ 0.1.0: Give every browser its own locations.
504
+
505
+ 1. + .bookmarks_location to all classes except Base
506
+ 2. + .history_location to call classes except Base
507
+
508
+
509
+ ## 20220622
510
+
511
+ 0.0.0: Extract Browser from dump_browser_history_to_csv.
512
+
513
+ 1. + Browser class from dump_browser_history_to_csv.
514
+ 2. + BOOKMARKS_LOCATION data from Bookmarks.
515
+ 3. + Browser::Base
516
+ 4. /Browser::AVAILABLE_BROSWERS/Browser::LIST/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022-2026 thoran
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # browser.rb
2
+
3
+ ## Description
4
+
5
+ Read the bookmarks and history of the web browsers on a machine — Brave, Chrome,
6
+ Chromium, Firefox, Safari and Tor Browser — and render them to CSV, JSON or a
7
+ property list. Bookmarks come through as one common representation whatever the
8
+ browser; history as each browser stores it.
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+ ```ruby
14
+ gem 'browser.rb'
15
+ ```
16
+
17
+ And then execute:
18
+ ```shell
19
+ $ bundle
20
+ ```
21
+
22
+ Or install directly:
23
+ ```shell
24
+ $ gem install browser.rb
25
+ ```
26
+
27
+ Note the `.rb`. The plain `browser` gem is someone else's — a user-agent parser —
28
+ so this one is `browser.rb`. The require is the plain name, as it is for `http.rb`:
29
+ ```ruby
30
+ require 'browser'
31
+ ```
32
+ The `gem 'browser.rb'` line is what makes that require load this gem rather than
33
+ the other; the two ship the same file name.
34
+
35
+ ## Usage
36
+
37
+ ```ruby
38
+ require 'browser'
39
+
40
+ chrome = Browser.new(:chrome)
41
+ chrome.bookmarks.to_objects # => [{title:, url:, folder:}, ...]
42
+ chrome.bookmarks.to_csv # title,url,folder rows
43
+ chrome.bookmarks.to_json
44
+ chrome.history.to_objects
45
+ ```
46
+
47
+ Name a browser by string or symbol, in any case:
48
+ ```ruby
49
+ Browser.new(:brave)
50
+ Browser.new('TorBrowser')
51
+ Browser.new('tor_browser')
52
+ ```
53
+
54
+ Firefox and Tor Browser open the profile their profiles.ini names as the default, so
55
+ none need be named. To reach another:
56
+ ```ruby
57
+ Browser.new(:firefox, profile_name: 'abcd1234.default-release').bookmarks.to_objects
58
+ ```
59
+
60
+ Or opt into top-level names:
61
+ ```ruby
62
+ require 'browser-classes'
63
+ Chrome.new.bookmarks
64
+ ```
65
+
66
+ The renderers are `to_objects` (an array of `{title:, url:, folder:}`), `to_csv`,
67
+ `to_json` and `to_plist`. `dump(format:, path:, filename:)` writes one to a file and
68
+ returns the path. `readable?` says whether the data can be read right now.
69
+
70
+ ## Capabilities
71
+
72
+ | browser | bookmarks read from | history read from | folders | profile |
73
+ |---|---|---|---|---|
74
+ | Brave | JSON | SQLite | yes | default, or named |
75
+ | Chrome | JSON | SQLite | yes | default, or named |
76
+ | Chromium | JSON | SQLite | yes | default, or named |
77
+ | Firefox | SQLite, places.sqlite | SQLite, the same file | yes | default, or named |
78
+ | Safari | binary plist | SQLite | yes | none |
79
+ | Tor Browser | SQLite, places.sqlite | SQLite, the same file | yes | default, or named |
80
+
81
+ ## Caveats
82
+
83
+ - **macOS only, for now.** Every location is a macOS path; Linux and Windows are on
84
+ the Todo.
85
+ - **A running browser locks its database.** History, and Firefox's bookmarks, are
86
+ read from SQLite, which the browser locks while it is open — so reading a browser
87
+ you are using raises `Browser::Unreadable`. Ask `readable?` first, or quit it.
88
+ - **Which Firefox profile.** Firefox and Tor Browser keep profiles in hashed
89
+ directories, and the default is the one profiles.ini names, which is the one
90
+ Firefox itself would open. A directory named plainly `default` is usually a
91
+ leftover, the live one being `<hash>.default-release`, and the install entry in
92
+ profiles.ini knows which. Two installations, say release and Nightly, each name a
93
+ default, and then `profile_name:` is required.
94
+ - **Tor Browser is looked for in `~/Library/Application Support/TorBrowser-Data`.**
95
+ That is where an install in `/Applications` keeps its data. One installed elsewhere
96
+ keeps it beside the application, and wants `bookmarks_location:` and
97
+ `history_location:` passed to the constructor.
98
+ - **Safari's Bookmarks.plist is a mirror.** With iCloud bookmark sync on, a sync
99
+ agent maintains it and writes it on change, so it can be absent while Safari is
100
+ working perfectly. It is a binary property list, read here via CFPropertyList.
101
+
102
+ ## History
103
+
104
+ I realised when wanting to dump all bookmarks from any browsers on one machine for import to another that I'd already written something of the sort for history called dump_browser_history_to_csv and that it would probably be a good idea to combine those efforts into a single browser library to handle both bookmarks and history, as well as any other similar browser data extraction effort.
105
+
106
+ ## Contributing
107
+
108
+ 1. Fork it (https://github.com/thoran/browser/fork)
109
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
110
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
111
+ 4. Push to the branch (`git push origin my-new-feature`)
112
+ 5. Create a new pull request
113
+
114
+ ## License
115
+
116
+ MIT