fontist 1.8.1 → 1.8.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/rspec.yml +2 -2
  3. data/README.md +14 -0
  4. data/fontist.gemspec +7 -5
  5. data/lib/fontist.rb +4 -0
  6. data/lib/fontist/cli.rb +22 -24
  7. data/lib/fontist/errors.rb +51 -2
  8. data/lib/fontist/font.rb +33 -54
  9. data/lib/fontist/font_installer.rb +4 -0
  10. data/lib/fontist/font_path.rb +29 -0
  11. data/lib/fontist/formula.rb +4 -4
  12. data/lib/fontist/import/create_formula.rb +3 -2
  13. data/lib/fontist/import/extractors.rb +4 -0
  14. data/lib/fontist/import/extractors/cpio_extractor.rb +39 -0
  15. data/lib/fontist/import/extractors/gzip_extractor.rb +27 -0
  16. data/lib/fontist/import/extractors/rpm_extractor.rb +45 -0
  17. data/lib/fontist/import/extractors/tar_extractor.rb +47 -0
  18. data/lib/fontist/import/google/skiplist.yml +3 -0
  19. data/lib/fontist/import/recursive_extraction.rb +21 -7
  20. data/lib/fontist/import/sil_import.rb +99 -0
  21. data/lib/fontist/index.rb +4 -65
  22. data/lib/fontist/indexes/base_index.rb +82 -0
  23. data/lib/fontist/indexes/filename_index.rb +19 -0
  24. data/lib/fontist/indexes/font_index.rb +21 -0
  25. data/lib/fontist/indexes/index_formula.rb +36 -0
  26. data/lib/fontist/manifest/install.rb +3 -2
  27. data/lib/fontist/manifest/locations.rb +1 -1
  28. data/lib/fontist/system_font.rb +33 -36
  29. data/lib/fontist/system_index.rb +46 -4
  30. data/lib/fontist/utils.rb +5 -0
  31. data/lib/fontist/utils/cache.rb +12 -4
  32. data/lib/fontist/utils/cpio/cpio.rb +199 -0
  33. data/lib/fontist/utils/cpio_extractor.rb +47 -0
  34. data/lib/fontist/utils/gzip_extractor.rb +24 -0
  35. data/lib/fontist/utils/locking.rb +17 -0
  36. data/lib/fontist/utils/rpm_extractor.rb +37 -0
  37. data/lib/fontist/utils/tar_extractor.rb +61 -0
  38. data/lib/fontist/version.rb +1 -1
  39. metadata +53 -13
  40. data/lib/fontist/index_formula.rb +0 -30
@@ -0,0 +1,82 @@
1
+ require_relative "index_formula"
2
+
3
+ module Fontist
4
+ module Indexes
5
+ class BaseIndex
6
+ def self.from_yaml
7
+ @from_yaml ||= begin
8
+ unless File.exist?(path)
9
+ raise Errors::FormulaIndexNotFoundError.new("Please fetch `#{path}` index with `fontist update`.")
10
+ end
11
+
12
+ data = YAML.load_file(path)
13
+ new(data)
14
+ end
15
+ end
16
+
17
+ def self.path
18
+ raise NotImplementedError, "Please define path of an index"
19
+ end
20
+
21
+ def self.rebuild
22
+ index = new
23
+ index.build
24
+ index.to_yaml
25
+ end
26
+
27
+ def initialize(data = {})
28
+ @index = {}
29
+
30
+ data.each_pair do |key, paths|
31
+ paths.each do |path|
32
+ add_index_formula(key, IndexFormula.new(path))
33
+ end
34
+ end
35
+ end
36
+
37
+ def build
38
+ Formula.all.each do |formula|
39
+ add_formula(formula)
40
+ end
41
+ end
42
+
43
+ def add_formula(_formula)
44
+ raise NotImplementedError, "Please define how to add formula to an index, use #add_index_formula"
45
+ end
46
+
47
+ def add_index_formula(key_raw, index_formula)
48
+ key = normalize_key(key_raw)
49
+ @index[key] ||= []
50
+ @index[key] << index_formula unless @index[key].include?(index_formula)
51
+ end
52
+
53
+ def load_formulas(key)
54
+ index_formulas(key).map(&:to_full)
55
+ end
56
+
57
+ def load_index_formulas(key)
58
+ index_formulas(key)
59
+ end
60
+
61
+ def to_yaml
62
+ File.write(self.class.path, YAML.dump(to_h))
63
+ end
64
+
65
+ def to_h
66
+ @index.map do |key, index_formulas|
67
+ [key, index_formulas.map(&:to_s)]
68
+ end.to_h
69
+ end
70
+
71
+ private
72
+
73
+ def index_formulas(key)
74
+ @index[normalize_key(key)] || []
75
+ end
76
+
77
+ def normalize_key(key)
78
+ key
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,19 @@
1
+ require_relative "base_index"
2
+
3
+ module Fontist
4
+ module Indexes
5
+ class FilenameIndex < BaseIndex
6
+ def self.path
7
+ Fontist.formula_filename_index_path
8
+ end
9
+
10
+ def add_formula(formula)
11
+ formula.fonts.each do |font|
12
+ font.styles.each do |style|
13
+ add_index_formula(style.font, formula.to_index_formula)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ require_relative "base_index"
2
+
3
+ module Fontist
4
+ module Indexes
5
+ class FontIndex < BaseIndex
6
+ def self.path
7
+ Fontist.formula_index_path
8
+ end
9
+
10
+ def add_formula(formula)
11
+ formula.fonts.each do |font|
12
+ add_index_formula(font.name, formula.to_index_formula)
13
+ end
14
+ end
15
+
16
+ def normalize_key(key)
17
+ key.downcase
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,36 @@
1
+ module Fontist
2
+ module Indexes
3
+ class IndexFormula
4
+ def initialize(path)
5
+ @path = path
6
+ end
7
+
8
+ def name
9
+ normalized.sub(/\.yml$/, "")
10
+ end
11
+
12
+ def to_s
13
+ normalized
14
+ end
15
+
16
+ def to_full
17
+ Formula.new_from_file(full_path)
18
+ end
19
+
20
+ def ==(other)
21
+ to_s == other.to_s
22
+ end
23
+
24
+ private
25
+
26
+ def normalized
27
+ escaped = Regexp.escape(Fontist.formulas_path.to_s + "/")
28
+ @path.sub(Regexp.new("^" + escaped), "")
29
+ end
30
+
31
+ def full_path
32
+ Fontist.formulas_path.join(normalized).to_s
33
+ end
34
+ end
35
+ end
36
+ end
@@ -3,9 +3,10 @@ require_relative "locations"
3
3
  module Fontist
4
4
  module Manifest
5
5
  class Install < Locations
6
- def initialize(manifest, confirmation: "no")
6
+ def initialize(manifest, confirmation: "no", hide_licenses: false)
7
7
  @manifest = manifest
8
8
  @confirmation = confirmation
9
+ @hide_licenses = hide_licenses
9
10
  end
10
11
 
11
12
  private
@@ -20,7 +21,7 @@ module Fontist
20
21
  end
21
22
 
22
23
  def install_font(font)
23
- Fontist::Font.install(font, force: true, confirmation: @confirmation)
24
+ Fontist::Font.install(font, force: true, confirmation: @confirmation, hide_licenses: @hide_licenses)
24
25
  end
25
26
  end
26
27
  end
@@ -55,7 +55,7 @@ module Fontist
55
55
  def file_paths(font, style)
56
56
  find_font_with_name(font, style).tap do |x|
57
57
  if x["paths"].empty?
58
- raise Errors::MissingFontError.new("Could not find font #{font} #{style}.")
58
+ raise Errors::MissingFontError.new(font, style)
59
59
  end
60
60
  end
61
61
  end
@@ -9,6 +9,33 @@ module Fontist
9
9
  @user_sources = sources || []
10
10
  end
11
11
 
12
+ def self.font_paths
13
+ system_font_paths + fontist_font_paths
14
+ end
15
+
16
+ def self.system_font_paths
17
+ config_path = Fontist.system_file_path
18
+ os = Fontist::Utils::System.user_os.to_s
19
+ templates = YAML.load_file(config_path)["system"][os]["paths"]
20
+ patterns = expand_paths(templates)
21
+
22
+ Dir.glob(patterns)
23
+ end
24
+
25
+ def self.expand_paths(paths)
26
+ paths.map do |path|
27
+ require "etc"
28
+ passwd = Etc.getpwuid
29
+ username = passwd ? passwd.name : Etc.getlogin
30
+
31
+ username ? path.gsub("{username}", username) : path
32
+ end
33
+ end
34
+
35
+ def self.fontist_font_paths
36
+ Dir.glob(Fontist.fonts_path.join("**"))
37
+ end
38
+
12
39
  def self.find(font, sources: [])
13
40
  new(font: font, sources: sources).find
14
41
  end
@@ -36,50 +63,20 @@ module Fontist
36
63
 
37
64
  attr_reader :font, :style, :user_sources
38
65
 
39
- def normalize_default_paths
40
- @normalize_default_paths ||= default_sources["paths"].map do |path|
41
- require "etc"
42
- passwd = Etc.getpwuid
43
- username = passwd ? passwd.name : Etc.getlogin
44
-
45
- username ? path.gsub("{username}", username) : path
46
- end
47
- end
48
-
49
- def font_paths
50
- @font_paths ||= Dir.glob((
51
- user_sources +
52
- normalize_default_paths +
53
- [fontist_fonts_path.join("**")]
54
- ).flatten.uniq)
55
- end
56
-
57
- def fontist_fonts_path
58
- @fontist_fonts_path ||= Fontist.fonts_path
59
- end
60
-
61
- def user_os
62
- Fontist::Utils::System.user_os
63
- end
64
-
65
- def system_path_file
66
- File.open(Fontist.system_file_path)
67
- end
68
-
69
- def default_sources
70
- @default_sources ||= YAML.safe_load(system_path_file)["system"][user_os.to_s]
71
- end
72
-
73
66
  def find_styles
74
67
  find_by_index || find_by_formulas
75
68
  end
76
69
 
77
70
  def find_by_index
78
- SystemIndex.new(font_paths).find(font, style)
71
+ SystemIndex.new(all_paths).find(font, style)
79
72
  end
80
73
 
81
74
  def find_by_formulas
82
- FormulaPaths.new(font_paths).find(font, style)
75
+ FormulaPaths.new(all_paths).find(font, style)
76
+ end
77
+
78
+ def all_paths
79
+ @all_paths ||= Dir.glob(user_sources) + self.class.font_paths
83
80
  end
84
81
  end
85
82
  end
@@ -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,21 +38,49 @@ 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
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