fontist 1.7.3 → 1.8.5

Sign up to get free protection for your applications and to get access to all the features.
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