fontist 3.0.26 → 3.0.28
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 +4 -4
- data/lib/fontist/cache/store.rb +66 -52
- data/lib/fontist/indexes/directory_snapshot.rb +6 -2
- data/lib/fontist/indexes/index_mixin.rb +51 -3
- data/lib/fontist/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cdbd031efa6ca583a610246930ceddc3cc365f3b6047d49ccc9287efee8223ec
|
|
4
|
+
data.tar.gz: a5bc28594e55dc5e7a2050293338d9f19597edc44801dfe3bb5c98b9adb900ea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d7ec1b13570f62ebe344a5b0e11d18b13e4160ed1e76f4ccbdf93fe3f5d40d376b4af457ee5e5e3f658afbe6c9df9a144a0f011f98cbc2eb39163c15f77f014
|
|
7
|
+
data.tar.gz: 8ebaacfb87497c01fbc41c9074e4febbca0f788f7d81ab6c1d409ae0b7123cdc9d78dbd24501fb03cfa824dcc4932cb10d64f203039185582456a0a466e00410
|
data/lib/fontist/cache/store.rb
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
require "fileutils"
|
|
2
2
|
require "digest"
|
|
3
|
+
require "json"
|
|
3
4
|
|
|
4
5
|
module Fontist
|
|
5
6
|
module Cache
|
|
6
|
-
# Backend store
|
|
7
|
+
# Backend store persisting entries as inert JSON.
|
|
8
|
+
#
|
|
9
|
+
# The previous Marshal backend was removed deliberately: Marshal
|
|
10
|
+
# load of a file from a user-writable path executes arbitrary
|
|
11
|
+
# object instantiation on tampered content, and fontist runs
|
|
12
|
+
# embedded in long-lived host processes where that escalation
|
|
13
|
+
# lands. Values must be
|
|
14
|
+
# JSON-native (hashes with string keys, arrays, strings, numbers,
|
|
15
|
+
# booleans, nil); anything else skips the write and reads back as
|
|
16
|
+
# a miss. Files written by the previous Marshal backend carry a
|
|
17
|
+
# different extension and are simply never read; #clear removes
|
|
18
|
+
# them.
|
|
7
19
|
class Store
|
|
20
|
+
FORMAT_VERSION = 1
|
|
21
|
+
|
|
8
22
|
def initialize(cache_dir)
|
|
9
23
|
@cache_dir = cache_dir.to_s
|
|
10
24
|
ensure_cache_dir
|
|
@@ -26,6 +40,10 @@ module Fontist
|
|
|
26
40
|
ensure_cache_dir
|
|
27
41
|
entry = CacheEntry.new(value, ttl)
|
|
28
42
|
write_entry(key, entry)
|
|
43
|
+
rescue TypeError, JSON::GeneratorError
|
|
44
|
+
# Values outside the JSON type system (Time, Date, arbitrary
|
|
45
|
+
# objects) skip the write and read back as a miss
|
|
46
|
+
false
|
|
29
47
|
end
|
|
30
48
|
|
|
31
49
|
def delete(key)
|
|
@@ -35,20 +53,29 @@ module Fontist
|
|
|
35
53
|
end
|
|
36
54
|
|
|
37
55
|
def clear
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
nil
|
|
42
|
-
end
|
|
56
|
+
glob_delete("*.json")
|
|
57
|
+
# Sweep entries written by the pre-JSON Marshal backend
|
|
58
|
+
glob_delete("*.marshal")
|
|
43
59
|
end
|
|
44
60
|
|
|
45
61
|
def cleanup_temp_files
|
|
46
|
-
# Clean up orphaned .tmp files from interrupted writes
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
# Clean up orphaned .tmp files from interrupted writes: this
|
|
63
|
+
# happens when a process crashes between write and rename
|
|
64
|
+
glob_delete("*.tmp")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Cache entry with TTL support
|
|
68
|
+
class CacheEntry
|
|
69
|
+
attr_reader :value, :expires_at
|
|
70
|
+
|
|
71
|
+
def initialize(value, ttl = nil, expires_at: nil)
|
|
72
|
+
@value = value
|
|
73
|
+
@expires_at = expires_at
|
|
74
|
+
@expires_at ||= (Time.now + ttl).to_i if ttl
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def expired?
|
|
78
|
+
!!(@expires_at && Time.now.to_i > @expires_at)
|
|
52
79
|
end
|
|
53
80
|
end
|
|
54
81
|
|
|
@@ -61,7 +88,7 @@ module Fontist
|
|
|
61
88
|
end
|
|
62
89
|
|
|
63
90
|
def cache_path(key)
|
|
64
|
-
File.join(@cache_dir, "#{sanitize_key(key)}.
|
|
91
|
+
File.join(@cache_dir, "#{sanitize_key(key)}.json")
|
|
65
92
|
end
|
|
66
93
|
|
|
67
94
|
def sanitize_key(key)
|
|
@@ -75,57 +102,44 @@ module Fontist
|
|
|
75
102
|
end
|
|
76
103
|
end
|
|
77
104
|
|
|
105
|
+
def glob_delete(pattern)
|
|
106
|
+
Dir.glob(File.join(@cache_dir, pattern)).each do |f|
|
|
107
|
+
File.delete(f)
|
|
108
|
+
rescue StandardError
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
78
113
|
def read_entry(key)
|
|
79
114
|
return nil unless File.exist?(cache_path(key))
|
|
80
115
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
File.delete(cache_path(key))
|
|
90
|
-
rescue StandardError
|
|
91
|
-
nil
|
|
92
|
-
end
|
|
93
|
-
nil
|
|
94
|
-
end
|
|
116
|
+
payload = JSON.parse(File.read(cache_path(key)))
|
|
117
|
+
CacheEntry.new(payload["value"], expires_at: payload["expires_at"])
|
|
118
|
+
rescue JSON::ParserError, TypeError, EOFError
|
|
119
|
+
# Corrupted file - delete it and return nil. This can happen
|
|
120
|
+
# on Windows when a file is read while being written, or when
|
|
121
|
+
# cache files from previous runs are corrupted
|
|
122
|
+
delete(key)
|
|
123
|
+
nil
|
|
95
124
|
end
|
|
96
125
|
|
|
97
126
|
def write_entry(key, entry)
|
|
98
|
-
|
|
99
|
-
# This ensures readers never see partial writes, even on Windows
|
|
100
|
-
temp_path = "#{cache_path(key)}.tmp"
|
|
127
|
+
payload = entry_payload(entry)
|
|
101
128
|
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# File.rename is atomic on all platforms for same filesystem
|
|
129
|
+
# Temp file + atomic rename: readers never see partial writes
|
|
130
|
+
temp_path = "#{cache_path(key)}.tmp"
|
|
131
|
+
File.write(temp_path, payload)
|
|
106
132
|
File.rename(temp_path, cache_path(key))
|
|
107
133
|
rescue StandardError => e
|
|
108
|
-
# Clean up temp file if rename fails
|
|
109
|
-
|
|
110
|
-
File.delete(temp_path)
|
|
111
|
-
rescue StandardError
|
|
112
|
-
nil
|
|
113
|
-
end
|
|
134
|
+
# Clean up temp file if the write or rename fails
|
|
135
|
+
delete(temp_path)
|
|
114
136
|
raise e
|
|
115
137
|
end
|
|
116
138
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def initialize(value, ttl)
|
|
122
|
-
@value = value
|
|
123
|
-
@expires_at = ttl ? (Time.now + ttl).to_i : nil
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def expired?
|
|
127
|
-
!!(@expires_at && Time.now.to_i > @expires_at)
|
|
128
|
-
end
|
|
139
|
+
def entry_payload(entry)
|
|
140
|
+
JSON.dump("version" => FORMAT_VERSION,
|
|
141
|
+
"expires_at" => entry.expires_at,
|
|
142
|
+
"value" => entry.value)
|
|
129
143
|
end
|
|
130
144
|
end
|
|
131
145
|
end
|
|
@@ -13,11 +13,15 @@ module Fontist
|
|
|
13
13
|
new(directory_path, files, Time.now.to_i)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
# Create from existing data (for cache restoration)
|
|
16
|
+
# Create from existing data (for cache restoration). The cache
|
|
17
|
+
# store serializes to JSON, so keys arrive as strings; normalize
|
|
18
|
+
# them back to the symbol-keyed shape used throughout this class
|
|
17
19
|
def self.from_hash(hash)
|
|
20
|
+
hash = (hash || {}).transform_keys(&:to_sym)
|
|
21
|
+
files = Array(hash[:files]).map { |file| file.transform_keys(&:to_sym) }
|
|
18
22
|
new(
|
|
19
23
|
hash[:directory_path].to_s,
|
|
20
|
-
|
|
24
|
+
files,
|
|
21
25
|
hash[:scanned_at],
|
|
22
26
|
)
|
|
23
27
|
end
|
|
@@ -75,6 +75,7 @@ module Fontist
|
|
|
75
75
|
# Drop the memoized parsed instance along with the index file,
|
|
76
76
|
# to force a fresh parse on next access
|
|
77
77
|
Fontist::FromFileCache.clear
|
|
78
|
+
reset_sidecar_cache
|
|
78
79
|
|
|
79
80
|
# Delete the index file and its lookup sidecar to force a
|
|
80
81
|
# rebuild on next access. This is important for tests to
|
|
@@ -96,10 +97,12 @@ module Fontist
|
|
|
96
97
|
def sidecar_entries
|
|
97
98
|
sidecar_path = "#{path}.json"
|
|
98
99
|
return unless File.exist?(sidecar_path)
|
|
99
|
-
return if
|
|
100
|
-
File.mtime(sidecar_path) < File.mtime(path)
|
|
100
|
+
return if sidecar_stale?(sidecar_path)
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
entries = cached_sidecar_entries(sidecar_path)
|
|
103
|
+
return entries if entries
|
|
104
|
+
|
|
105
|
+
parse_sidecar_entries(sidecar_path)
|
|
103
106
|
rescue JSON::ParserError, TypeError
|
|
104
107
|
nil
|
|
105
108
|
end
|
|
@@ -109,6 +112,51 @@ module Fontist
|
|
|
109
112
|
File.write("#{path}.json", JSON.dump("entries" => entries_hash))
|
|
110
113
|
entries_hash
|
|
111
114
|
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def sidecar_stale?(sidecar_path)
|
|
119
|
+
File.exist?(path) && File.mtime(sidecar_path) < File.mtime(path)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def cached_sidecar_entries(sidecar_path)
|
|
123
|
+
mtime = File.mtime(sidecar_path)
|
|
124
|
+
entry = nil
|
|
125
|
+
sidecar_cache_mutex.synchronize do
|
|
126
|
+
entry = sidecar_cache[sidecar_path]
|
|
127
|
+
end
|
|
128
|
+
entry[:entries] if entry&.fetch(:mtime, nil) == mtime
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Parsing the sidecar costs tens of milliseconds per call and
|
|
132
|
+
# lookups re-enter many times per operation (every printed font
|
|
133
|
+
# path consults the filename sidecar), so parsed entries are
|
|
134
|
+
# memoized per sidecar mtime and shared between callers; treat
|
|
135
|
+
# as read-only
|
|
136
|
+
def parse_sidecar_entries(sidecar_path)
|
|
137
|
+
mtime = File.mtime(sidecar_path)
|
|
138
|
+
entries = JSON.parse(File.read(sidecar_path))["entries"]
|
|
139
|
+
|
|
140
|
+
sidecar_cache_mutex.synchronize do
|
|
141
|
+
sidecar_cache[sidecar_path] = { mtime: mtime, entries: entries }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
entries
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Class-level cache of parsed sidecar entries, invalidated by
|
|
148
|
+
# sidecar mtime so `fontist update` rewrites are picked up
|
|
149
|
+
def sidecar_cache
|
|
150
|
+
@sidecar_cache ||= {}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def sidecar_cache_mutex
|
|
154
|
+
@sidecar_cache_mutex ||= Mutex.new
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def reset_sidecar_cache
|
|
158
|
+
sidecar_cache_mutex.synchronize { sidecar_cache.clear }
|
|
159
|
+
end
|
|
112
160
|
end
|
|
113
161
|
|
|
114
162
|
# Build index by loading all formulas from disk.
|
data/lib/fontist/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontist
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0.
|
|
4
|
+
version: 3.0.28
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: down
|