fontist 3.0.26 → 3.0.27
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/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4bca0e4898879c116eb90bccf581f82177caafe4b981ac97d2ed974dbb9c19d2
|
|
4
|
+
data.tar.gz: 409088146ecdb388d8efd008f0b7d1d908863fb8d27b6d58aa5371812f5b93bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49bbea30d8a19d69ad796f7dd7fe49c7c6bf940d38a9aeef07aae568f7700cb1b4a9d39e1427f1002dca02837ed41768975fdc6e9baa37d8a4921235a6a6071d
|
|
7
|
+
data.tar.gz: adc3b7fe62335de562352d1c516d0c9c71298bf0b5c5764ab2ae8bd64fc921dc55814602e78e40440c6c916e15a3b0ca6649af8ba5f9b88f1ff672ed31f95be3
|
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
|
data/lib/fontist/version.rb
CHANGED