fontist 1.7.3 → 1.8.5

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +38 -0
  3. data/.github/workflows/rspec.yml +58 -0
  4. data/README.md +48 -4
  5. data/{bin → exe}/fontist +0 -0
  6. data/fontist.gemspec +10 -7
  7. data/lib/fontist.rb +9 -2
  8. data/lib/fontist/cli.rb +64 -55
  9. data/lib/fontist/errors.rb +63 -12
  10. data/lib/fontist/font.rb +33 -64
  11. data/lib/fontist/font_installer.rb +118 -0
  12. data/lib/fontist/font_path.rb +29 -0
  13. data/lib/fontist/fontist_font.rb +3 -49
  14. data/lib/fontist/formula.rb +101 -35
  15. data/lib/fontist/formula_paths.rb +43 -0
  16. data/lib/fontist/helpers.rb +7 -0
  17. data/lib/fontist/import/create_formula.rb +3 -2
  18. data/lib/fontist/import/extractors.rb +4 -0
  19. data/lib/fontist/import/extractors/cpio_extractor.rb +39 -0
  20. data/lib/fontist/import/extractors/gzip_extractor.rb +27 -0
  21. data/lib/fontist/import/extractors/rpm_extractor.rb +45 -0
  22. data/lib/fontist/import/extractors/tar_extractor.rb +47 -0
  23. data/lib/fontist/import/google/skiplist.yml +3 -0
  24. data/lib/fontist/import/google_check.rb +1 -1
  25. data/lib/fontist/import/google_import.rb +3 -4
  26. data/lib/fontist/import/otfinfo_generate.rb +1 -1
  27. data/lib/fontist/import/recursive_extraction.rb +26 -8
  28. data/lib/fontist/import/sil_import.rb +99 -0
  29. data/lib/fontist/index.rb +11 -0
  30. data/lib/fontist/indexes/base_index.rb +82 -0
  31. data/lib/fontist/indexes/filename_index.rb +19 -0
  32. data/lib/fontist/indexes/font_index.rb +21 -0
  33. data/lib/fontist/indexes/index_formula.rb +36 -0
  34. data/lib/fontist/manifest/install.rb +4 -5
  35. data/lib/fontist/manifest/locations.rb +9 -1
  36. data/lib/fontist/system_font.rb +32 -62
  37. data/lib/fontist/system_index.rb +47 -5
  38. data/lib/fontist/utils.rb +5 -0
  39. data/lib/fontist/utils/cache.rb +12 -4
  40. data/lib/fontist/utils/cpio/cpio.rb +199 -0
  41. data/lib/fontist/utils/cpio_extractor.rb +47 -0
  42. data/lib/fontist/utils/exe_extractor.rb +1 -1
  43. data/lib/fontist/utils/gzip_extractor.rb +24 -0
  44. data/lib/fontist/utils/locking.rb +17 -0
  45. data/lib/fontist/utils/rpm_extractor.rb +37 -0
  46. data/lib/fontist/utils/tar_extractor.rb +61 -0
  47. data/lib/fontist/utils/zip_extractor.rb +1 -1
  48. data/lib/fontist/version.rb +1 -1
  49. metadata +74 -26
  50. data/.github/workflows/macosx.yml +0 -33
  51. data/.github/workflows/ubuntu.yml +0 -30
  52. data/.github/workflows/windows.yml +0 -32
  53. data/bin/check_google +0 -8
  54. data/bin/console +0 -11
  55. data/bin/convert_formulas +0 -8
  56. data/bin/generate_otfinfo +0 -8
  57. data/bin/import_google +0 -8
  58. data/bin/rspec +0 -29
  59. data/bin/setup +0 -7
  60. data/lib/fontist/font_formula.rb +0 -169
  61. data/lib/fontist/formula_template.rb +0 -122
  62. data/lib/fontist/formulas.rb +0 -56
  63. data/lib/fontist/registry.rb +0 -43
@@ -2,8 +2,18 @@ require "ttfunk"
2
2
 
3
3
  module Fontist
4
4
  class SystemIndex
5
+ include Utils::Locking
6
+
5
7
  attr_reader :font_paths
6
8
 
9
+ def self.find(font, style)
10
+ new(SystemFont.font_paths).find(font, style)
11
+ end
12
+
13
+ def self.rebuild
14
+ new(SystemFont.font_paths).rebuild
15
+ end
16
+
7
17
  def initialize(font_paths)
8
18
  @font_paths = font_paths
9
19
  end
@@ -17,6 +27,10 @@ module Fontist
17
27
  fonts.empty? ? nil : fonts
18
28
  end
19
29
 
30
+ def rebuild
31
+ build_system_index
32
+ end
33
+
20
34
  private
21
35
 
22
36
  def system_index
@@ -24,28 +38,56 @@ module Fontist
24
38
  end
25
39
 
26
40
  def build_system_index
41
+ lock(lock_path) do
42
+ do_build_system_index
43
+ end
44
+ end
45
+
46
+ def lock_path
47
+ Fontist.system_index_path.to_s + ".lock"
48
+ end
49
+
50
+ def do_build_system_index
27
51
  previous_index = load_system_index
28
52
  updated_index = detect_paths(font_paths, previous_index)
29
53
  updated_index.tap do |index|
30
- save_index(index)
54
+ save_index(index) if changed?(updated_index, previous_index)
31
55
  end
32
56
  end
33
57
 
58
+ def changed?(this, other)
59
+ this.map { |x| x[:path] }.uniq.sort != other.map { |x| x[:path] }.uniq.sort
60
+ end
61
+
34
62
  def load_system_index
35
63
  index = File.exist?(Fontist.system_index_path) ? YAML.load_file(Fontist.system_index_path) : []
36
- index.group_by { |x| x[:path] }
64
+
65
+ index.each do |item|
66
+ missing_keys = %i[path full_name family_name type] - item.keys
67
+ unless missing_keys.empty?
68
+ raise(Errors::FontIndexCorrupted, <<~MSG.chomp)
69
+ Font index is corrupted.
70
+ Item #{item.inspect} misses required attributes: #{missing_keys.join(', ')}.
71
+ You can remove the index file (#{Fontist.system_index_path}) and try again.
72
+ MSG
73
+ end
74
+ end
75
+
76
+ index
37
77
  end
38
78
 
39
- def detect_paths(paths, indexed)
79
+ def detect_paths(paths, index)
80
+ by_path = index.group_by { |x| x[:path] }
81
+
40
82
  paths.flat_map do |path|
41
- next indexed[path] if indexed[path]
83
+ next by_path[path] if by_path[path]
42
84
 
43
85
  detect_fonts(path)
44
86
  end
45
87
  end
46
88
 
47
89
  def detect_fonts(path)
48
- case File.extname(path).delete_prefix(".").downcase
90
+ case File.extname(path).gsub(/^\./, "").downcase
49
91
  when "ttf", "otf"
50
92
  detect_file_font(path)
51
93
  when "ttc"
data/lib/fontist/utils.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require "fontist/utils/ui"
2
+ require "fontist/utils/locking"
2
3
  require "fontist/utils/system"
3
4
  require "fontist/utils/dsl"
4
5
  require "fontist/utils/dsl/font"
@@ -8,6 +9,10 @@ require "fontist/utils/zip_extractor"
8
9
  require "fontist/utils/exe_extractor"
9
10
  require "fontist/utils/msi_extractor"
10
11
  require "fontist/utils/seven_zip_extractor"
12
+ require "fontist/utils/rpm_extractor"
13
+ require "fontist/utils/gzip_extractor"
14
+ require "fontist/utils/cpio_extractor"
15
+ require "fontist/utils/tar_extractor"
11
16
 
12
17
  module Fontist
13
18
  module Utils
@@ -1,6 +1,8 @@
1
1
  module Fontist
2
2
  module Utils
3
3
  class Cache
4
+ include Locking
5
+
4
6
  def fetch(key, bar: nil)
5
7
  map = load_cache
6
8
  if cache_exist?(map[key])
@@ -26,7 +28,7 @@ module Fontist
26
28
  end
27
29
 
28
30
  def downloaded_file(path)
29
- File.new(downloaded_path(path))
31
+ File.new(downloaded_path(path), "rb")
30
32
  end
31
33
 
32
34
  def cache_exist?(path)
@@ -48,13 +50,19 @@ module Fontist
48
50
  def save_cache(generated_file, key)
49
51
  path = move_to_downloads(generated_file)
50
52
 
51
- map = load_cache
52
- map[key] = path
53
- File.write(cache_map_path, YAML.dump(map))
53
+ lock(lock_path) do
54
+ map = load_cache
55
+ map[key] = path
56
+ File.write(cache_map_path, YAML.dump(map))
57
+ end
54
58
 
55
59
  path
56
60
  end
57
61
 
62
+ def lock_path
63
+ cache_map_path.to_s + ".lock"
64
+ end
65
+
58
66
  def move_to_downloads(source)
59
67
  create_downloads_directory
60
68
  path = generate_file_path(source)
@@ -0,0 +1,199 @@
1
+ # code is obtained from https://github.com/jordansissel/ruby-arr-pm/blob/8071591173ebb90dea27d5acfdde69a37dcb2744/cpio.rb
2
+ # rubocop:disable all
3
+ class BoundedIO
4
+ attr_reader :length
5
+ attr_reader :remaining
6
+
7
+ def initialize(io, length, &eof_callback)
8
+ @io = io
9
+ @length = length
10
+ @remaining = length
11
+
12
+ @eof_callback = eof_callback
13
+ @eof = false
14
+ end
15
+
16
+ def read(size=nil)
17
+ return nil if eof?
18
+ size = @remaining if size.nil?
19
+ data = @io.read(size)
20
+ @remaining -= data.bytesize
21
+ eof?
22
+ data
23
+ end
24
+
25
+ def sysread(size)
26
+ raise EOFError, "end of file reached" if eof?
27
+ read(size)
28
+ end
29
+
30
+ def eof?
31
+ return false if @remaining > 0
32
+ return @eof if @eof
33
+
34
+ @eof_callback.call
35
+ @eof = true
36
+ end
37
+ end
38
+
39
+ module CPIO
40
+ FIELDS = [
41
+ :magic, :ino, :mode, :uid, :gid, :nlink, :mtime, :filesize, :devmajor,
42
+ :devminor, :rdevmajor, :rdevminor, :namesize, :check
43
+ ]
44
+ end
45
+
46
+ class CPIO::ASCIIReader
47
+ FIELD_SIZES = {
48
+ :magic => 6,
49
+ :ino => 8,
50
+ :mode => 8,
51
+ :uid => 8,
52
+ :gid => 8,
53
+ :nlink => 8,
54
+ :mtime => 8,
55
+ :filesize => 8,
56
+ :devmajor => 8,
57
+ :devminor => 8,
58
+ :rdevmajor => 8,
59
+ :rdevminor => 8,
60
+ :namesize => 8,
61
+ :check => 8
62
+ }
63
+ HEADER_LENGTH = FIELD_SIZES.reduce(0) { |m, (_, v)| m + v }
64
+ HEADER_PACK = FIELD_SIZES.collect { |_, v| "A#{v}" }.join
65
+
66
+ FIELD_ORDER = [
67
+ :magic, :ino, :mode, :uid, :gid, :nlink, :mtime, :filesize, :devmajor,
68
+ :devminor, :rdevmajor, :rdevminor, :namesize, :check
69
+ ]
70
+
71
+ def initialize(io)
72
+ @io = io
73
+ end
74
+
75
+ private
76
+
77
+ def io
78
+ @io
79
+ end
80
+
81
+ def each(&block)
82
+ while true
83
+ entry = read
84
+ break if entry.nil?
85
+ # The CPIO format has the end-of-stream marker as a file called "TRAILER!!!"
86
+ break if entry.name == "TRAILER!!!"
87
+ block.call(entry, entry.file)
88
+ verify_correct_read(entry) unless entry.directory?
89
+ end
90
+ end
91
+
92
+ def verify_correct_read(entry)
93
+ # Read and throw away the whole file if not read at all.
94
+ entry.file.tap do |file|
95
+ if file.nil? || file.remaining == 0
96
+ # All OK! :)
97
+ elsif file.remaining == file.length
98
+ file.read(16384) while !file.eof?
99
+ else
100
+ # The file was only partially read? This should be an error by the
101
+ # user.
102
+ consumed = file.length - file.remaining
103
+ raise BadState, "Only #{consumed} bytes were read of the #{file.length} byte file: #{entry.name}"
104
+ end
105
+ end
106
+ end
107
+
108
+ def read
109
+ entry = CPIOEntry.new
110
+ header = io.read(HEADER_LENGTH)
111
+ return nil if header.nil?
112
+ FIELD_ORDER.zip(header.unpack(HEADER_PACK)).each do |field, value|
113
+ entry.send("#{field}=", value.to_i(16))
114
+ end
115
+
116
+ entry.validate
117
+ entry.mtime = Time.at(entry.mtime)
118
+ read_name(entry, @io)
119
+ read_file(entry, @io)
120
+ entry
121
+ end
122
+
123
+ def read_name(entry, io)
124
+ entry.name = io.read(entry.namesize - 1) # - 1 for null terminator
125
+ nul = io.read(1)
126
+ raise ArgumentError, "Corrupt CPIO or bug? Name null terminator was not null: #{nul.inspect}" if nul != "\0"
127
+ padding_data = io.read(padding_name(entry))
128
+ # Padding should be all null bytes
129
+ if padding_data != ("\0" * padding_data.bytesize)
130
+ raise ArgumentError, "Corrupt CPIO or bug? Name null padding was #{padding_name(entry)} bytes: #{padding_data.inspect}"
131
+ end
132
+ end
133
+
134
+ def read_file(entry, io)
135
+ if entry.directory?
136
+ entry.file = nil
137
+ #read_file_padding(entry, io)
138
+ nil
139
+ else
140
+ entry.file = BoundedIO.new(io, entry.filesize) do
141
+ read_file_padding(entry, io)
142
+ end
143
+ end
144
+ end
145
+
146
+ def read_file_padding(entry, io)
147
+ padding_data = io.read(padding_file(entry))
148
+ if padding_data != ("\0" * padding_data.bytesize)
149
+ raise ArgumentError, "Corrupt CPIO or bug? File null padding was #{padding_file(entry)} bytes: #{padding_data.inspect}"
150
+ end
151
+ end
152
+
153
+ def padding_name(entry)
154
+ # name padding is padding up to a multiple of 4 after header+namesize
155
+ -(HEADER_LENGTH + entry.namesize) % 4
156
+ end
157
+
158
+ def padding_file(entry)
159
+ (-(HEADER_LENGTH + entry.filesize + 2) % 4)
160
+ end
161
+ public(:each)
162
+ end
163
+
164
+ class CPIOEntry
165
+ CPIO::FIELDS.each do |field|
166
+ attr_accessor field
167
+ end
168
+
169
+ attr_accessor :name
170
+ attr_accessor :file
171
+
172
+ DIRECTORY_FLAG = 0040000
173
+
174
+ def validate
175
+ raise "Invalid magic #{magic.inspect}" if magic != 0x070701
176
+ raise "Invalid ino #{ino.inspect}" if ino < 0
177
+ raise "Invalid mode #{mode.inspect}" if mode < 0
178
+ raise "Invalid uid #{uid.inspect}" if uid < 0
179
+ raise "Invalid gid #{gid.inspect}" if gid < 0
180
+ raise "Invalid nlink #{nlink.inspect}" if nlink < 0
181
+ raise "Invalid mtime #{mtime.inspect}" if mtime < 0
182
+ raise "Invalid filesize #{filesize.inspect}" if filesize < 0
183
+ raise "Invalid devmajor #{devmajor.inspect}" if devmajor < 0
184
+ raise "Invalid devminor #{devminor.inspect}" if devminor < 0
185
+ raise "Invalid rdevmajor #{rdevmajor.inspect}" if rdevmajor < 0
186
+ raise "Invalid rdevminor #{rdevminor.inspect}" if rdevminor < 0
187
+ raise "Invalid namesize #{namesize.inspect}" if namesize < 0
188
+ raise "Invalid check #{check.inspect}" if check < 0
189
+ end # def validate
190
+
191
+ def read(*args)
192
+ return nil if directory?
193
+ file.read(*args)
194
+ end
195
+
196
+ def directory?
197
+ mode & DIRECTORY_FLAG > 0
198
+ end
199
+ end
@@ -0,0 +1,47 @@
1
+ module Fontist
2
+ module Utils
3
+ module CpioExtractor
4
+ def cpio_extract(resource)
5
+ file = @downloaded ? resource : download_file(resource)
6
+
7
+ dir = extract_cpio_file(file)
8
+
9
+ largest_file_in_dir(dir)
10
+ end
11
+
12
+ private
13
+
14
+ def extract_cpio_file(archive_path)
15
+ archive_file = File.open(archive_path, "rb")
16
+ dir = Dir.mktmpdir
17
+ extract_cpio_file_to_dir(archive_file, dir)
18
+
19
+ dir
20
+ end
21
+
22
+ def extract_cpio_file_to_dir(archive_file, dir)
23
+ cpio_reader_class.new(archive_file).each do |entry, file|
24
+ path = File.join(dir, entry.name)
25
+ if entry.directory?
26
+ FileUtils.mkdir_p(path)
27
+ else
28
+ File.write(path, file.read, mode: "wb")
29
+ end
30
+ end
31
+ end
32
+
33
+ def cpio_reader_class
34
+ @cpio_reader_class ||= begin
35
+ require "fontist/utils/cpio/cpio"
36
+ CPIO::ASCIIReader
37
+ end
38
+ end
39
+
40
+ def largest_file_in_dir(dir)
41
+ Dir.glob(File.join(dir, "**/*")).max_by do |path|
42
+ File.size(path)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -6,7 +6,7 @@ module Fontist
6
6
 
7
7
  exe_file = download_file(exe_file).path if download
8
8
 
9
- Fontist.ui.say(%(Installing font "#{key}".))
9
+ Fontist.ui.say(%(Installing font "#{formula.key}".))
10
10
  cab_file = decompressor.search(exe_file)
11
11
  cabbed_fonts = grep_fonts(cab_file.files) || []
12
12
  fonts_paths = extract_cabbed_fonts_to_assets(cabbed_fonts)
@@ -0,0 +1,24 @@
1
+ module Fontist
2
+ module Utils
3
+ module GzipExtractor
4
+ def gzip_extract(resource)
5
+ file = @downloaded ? resource : download_file(resource)
6
+
7
+ extract_gzip_file(file)
8
+ end
9
+
10
+ private
11
+
12
+ def extract_gzip_file(file)
13
+ Zlib::GzipReader.open(file) do |gz|
14
+ basename = File.basename(file, ".*")
15
+ dir = Dir.mktmpdir
16
+ path = File.join(dir, basename)
17
+ File.write(path, gz.read, mode: "wb")
18
+
19
+ path
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ module Fontist
2
+ module Utils
3
+ module Locking
4
+ def lock(lock_path)
5
+ File.dirname(lock_path).tap do |dir|
6
+ FileUtils.mkdir_p(dir) unless File.exist?(dir)
7
+ end
8
+
9
+ f = File.open(lock_path, File::CREAT)
10
+ f.flock(File::LOCK_EX)
11
+ yield
12
+ ensure
13
+ f.flock(File::LOCK_UN)
14
+ end
15
+ end
16
+ end
17
+ end