abachrome 0.1.1 → 0.1.3

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.
@@ -0,0 +1,437 @@
1
+ #
2
+ # Abachrome::Parsers::CSS - CSS color format parser
3
+ #
4
+ # This parser handles various CSS color formats including:
5
+ # - Named colors (red, blue, etc.)
6
+ # - Hex colors (#rgb, #rrggbb, #rgba, #rrggbbaa)
7
+ # - rgb() and rgba() functions
8
+ # - hsl() and hsla() functions
9
+ # - hwb() function
10
+ # - lab() and lch() functions
11
+ # - oklab() and oklch() functions
12
+ # - color() function
13
+ #
14
+
15
+ require_relative "hex"
16
+ require_relative "../named/css"
17
+ require_relative "../color"
18
+
19
+ module Abachrome
20
+ module Parsers
21
+ class CSS
22
+ def self.parse(input)
23
+ return nil unless input.is_a?(String)
24
+
25
+ input = input.strip.downcase
26
+
27
+ # Try named colors first
28
+ named_color = parse_named_color(input)
29
+ return named_color if named_color
30
+
31
+ # Try hex colors
32
+ hex_color = Hex.parse(input)
33
+ return hex_color if hex_color
34
+
35
+ # Try functional notation
36
+ parse_functional_color(input)
37
+ end
38
+
39
+ private
40
+
41
+ def self.parse_named_color(input)
42
+ # Check if input matches a named color
43
+ rgb_values = Named::CSS.method(input.to_sym)&.call
44
+ return nil unless rgb_values
45
+
46
+ # Convert 0-255 RGB values to 0-1 range
47
+ r, g, b = rgb_values.map { |v| v / 255.0 }
48
+ Color.from_rgb(r, g, b)
49
+ rescue NameError
50
+ nil
51
+ end
52
+
53
+ def self.parse_functional_color(input)
54
+ case input
55
+ when /^rgb\((.+)\)$/
56
+ parse_rgb($1)
57
+ when /^rgba\((.+)\)$/
58
+ parse_rgba($1)
59
+ when /^hsl\((.+)\)$/
60
+ parse_hsl($1)
61
+ when /^hsla\((.+)\)$/
62
+ parse_hsla($1)
63
+ when /^hwb\((.+)\)$/
64
+ parse_hwb($1)
65
+ when /^lab\((.+)\)$/
66
+ parse_lab($1)
67
+ when /^lch\((.+)\)$/
68
+ parse_lch($1)
69
+ when /^oklab\((.+)\)$/
70
+ parse_oklab($1)
71
+ when /^oklch\((.+)\)$/
72
+ parse_oklch($1)
73
+ when /^color\((.+)\)$/
74
+ parse_color_function($1)
75
+ else
76
+ nil
77
+ end
78
+ end
79
+
80
+ def self.parse_rgb(params)
81
+ values = parse_color_values(params, 3)
82
+ return nil unless values
83
+
84
+ r, g, b = values
85
+ Color.from_rgb(r, g, b)
86
+ end
87
+
88
+ def self.parse_rgba(params)
89
+ values = parse_color_values(params, 4)
90
+ return nil unless values
91
+
92
+ r, g, b, a = values
93
+ Color.from_rgb(r, g, b, a)
94
+ end
95
+
96
+ def self.parse_hsl(params)
97
+ values = parse_hsl_values(params, 3)
98
+ return nil unless values
99
+
100
+ h, s, l = values
101
+ rgb = hsl_to_rgb(h, s, l)
102
+ Color.from_rgb(*rgb)
103
+ end
104
+
105
+ def self.parse_hsla(params)
106
+ values = parse_hsl_values(params, 4)
107
+ return nil unless values
108
+
109
+ h, s, l, a = values
110
+ rgb = hsl_to_rgb(h, s, l)
111
+ Color.from_rgb(*rgb, a)
112
+ end
113
+
114
+ def self.parse_hwb(params)
115
+ values = parse_hwb_values(params)
116
+ return nil unless values
117
+
118
+ h, w, b, a = values
119
+ rgb = hwb_to_rgb(h, w, b)
120
+ Color.from_rgb(*rgb, a)
121
+ end
122
+
123
+ def self.parse_lab(params)
124
+ values = parse_lab_values(params, 3)
125
+ return nil unless values
126
+
127
+ l, a, b = values
128
+ # Convert CIELAB to XYZ, then to sRGB
129
+ xyz = lab_to_xyz(l, a, b)
130
+ rgb = xyz_to_rgb(*xyz)
131
+ Color.from_rgb(*rgb)
132
+ end
133
+
134
+ def self.parse_lch(params)
135
+ values = parse_lch_values(params, 3)
136
+ return nil unless values
137
+
138
+ l, c, h = values
139
+ # Convert CIELCH to CIELAB, then to XYZ, then to sRGB
140
+ lab = lch_to_lab(l, c, h)
141
+ xyz = lab_to_xyz(*lab)
142
+ rgb = xyz_to_rgb(*xyz)
143
+ Color.from_rgb(*rgb)
144
+ end
145
+
146
+ def self.parse_oklab(params)
147
+ values = parse_oklab_values(params, 3)
148
+ return nil unless values
149
+
150
+ l, a, b = values
151
+ Color.from_oklab(l, a, b)
152
+ end
153
+
154
+ def self.parse_oklch(params)
155
+ values = parse_oklch_values(params, 3)
156
+ return nil unless values
157
+
158
+ l, c, h = values
159
+ Color.from_oklch(l, c, h)
160
+ end
161
+
162
+ def self.parse_color_function(params)
163
+ # Parse color(space values...)
164
+ parts = params.split(/\s+/, 2)
165
+ return nil unless parts.length == 2
166
+
167
+ space = parts[0]
168
+ values_str = parts[1]
169
+
170
+ case space
171
+ when "srgb"
172
+ values = parse_color_values(values_str, 3)
173
+ return nil unless values
174
+ r, g, b = values
175
+ Color.from_rgb(r, g, b)
176
+ when "srgb-linear"
177
+ values = parse_color_values(values_str, 3)
178
+ return nil unless values
179
+ r, g, b = values
180
+ Color.from_lrgb(r, g, b)
181
+ when "display-p3"
182
+ # For now, approximate as sRGB
183
+ values = parse_color_values(values_str, 3)
184
+ return nil unless values
185
+ r, g, b = values
186
+ Color.from_rgb(r, g, b)
187
+ when "a98-rgb"
188
+ # For now, approximate as sRGB
189
+ values = parse_color_values(values_str, 3)
190
+ return nil unless values
191
+ r, g, b = values
192
+ Color.from_rgb(r, g, b)
193
+ when "prophoto-rgb"
194
+ # For now, approximate as sRGB
195
+ values = parse_color_values(values_str, 3)
196
+ return nil unless values
197
+ r, g, b = values
198
+ Color.from_rgb(r, g, b)
199
+ when "rec2020"
200
+ # For now, approximate as sRGB
201
+ values = parse_color_values(values_str, 3)
202
+ return nil unless values
203
+ r, g, b = values
204
+ Color.from_rgb(r, g, b)
205
+ else
206
+ nil
207
+ end
208
+ end
209
+
210
+ # Helper methods for parsing values
211
+
212
+ def self.parse_color_values(str, expected_count)
213
+ values = str.split(/\s*,\s*/).map(&:strip)
214
+ return nil unless values.length == expected_count
215
+
216
+ values.map do |v|
217
+ parse_numeric_value(v)
218
+ end.compact
219
+ end
220
+
221
+ def self.parse_hsl_values(str, expected_count)
222
+ values = str.split(/\s*,\s*/).map(&:strip)
223
+ return nil unless values.length == expected_count
224
+
225
+ parsed = []
226
+ values.each_with_index do |v, i|
227
+ if i == 0 # Hue
228
+ val = parse_angle_value(v)
229
+ return nil unless val
230
+ parsed << val
231
+ else # Saturation, Lightness, Alpha
232
+ val = parse_percentage_or_number(v)
233
+ return nil unless val
234
+ parsed << val
235
+ end
236
+ end
237
+ parsed
238
+ end
239
+
240
+ def self.parse_hwb_values(str)
241
+ values = str.split(/\s*,\s*/).map(&:strip)
242
+ return nil unless values.length >= 3
243
+
244
+ h = parse_angle_value(values[0])
245
+ w = parse_percentage_or_number(values[1])
246
+ b = parse_percentage_or_number(values[2])
247
+ a = values[3] ? parse_numeric_value(values[3]) : 1.0
248
+
249
+ return nil unless h && w && b && a
250
+
251
+ [h, w, b, a]
252
+ end
253
+
254
+ def self.parse_lab_values(str, expected_count)
255
+ values = str.split(/\s+/, expected_count).map(&:strip)
256
+ return nil unless values.length == expected_count
257
+
258
+ l = parse_percentage_or_number(values[0])
259
+ a = parse_numeric_value(values[1])
260
+ b = parse_numeric_value(values[2])
261
+
262
+ return nil unless l && a && b
263
+
264
+ [l, a, b]
265
+ end
266
+
267
+ def self.parse_lch_values(str, expected_count)
268
+ values = str.split(/\s+/, expected_count).map(&:strip)
269
+ return nil unless values.length == expected_count
270
+
271
+ l = parse_percentage_or_number(values[0])
272
+ c = parse_numeric_value(values[1])
273
+ h = parse_angle_value(values[2])
274
+
275
+ return nil unless l && c && h
276
+
277
+ [l, c, h]
278
+ end
279
+
280
+ def self.parse_oklab_values(str, expected_count)
281
+ values = str.split(/\s+/, expected_count).map(&:strip)
282
+ return nil unless values.length == expected_count
283
+
284
+ l = parse_percentage_or_number(values[0])
285
+ a = parse_numeric_value(values[1])
286
+ b = parse_numeric_value(values[2])
287
+
288
+ return nil unless l && a && b
289
+
290
+ [l, a, b]
291
+ end
292
+
293
+ def self.parse_oklch_values(str, expected_count)
294
+ values = str.split(/\s+/, expected_count).map(&:strip)
295
+ return nil unless values.length == expected_count
296
+
297
+ l = parse_percentage_or_number(values[0])
298
+ c = parse_numeric_value(values[1])
299
+ h = parse_angle_value(values[2])
300
+
301
+ return nil unless l && c && h
302
+
303
+ [l, c, h]
304
+ end
305
+
306
+ def self.parse_numeric_value(str)
307
+ return nil unless str
308
+
309
+ if str.end_with?('%')
310
+ (str.chomp('%').to_f / 100.0)
311
+ else
312
+ str.to_f
313
+ end
314
+ rescue
315
+ nil
316
+ end
317
+
318
+ def self.parse_percentage_or_number(str)
319
+ return nil unless str
320
+
321
+ if str.end_with?('%')
322
+ str.chomp('%').to_f / 100.0
323
+ else
324
+ str.to_f
325
+ end
326
+ rescue
327
+ nil
328
+ end
329
+
330
+ def self.parse_angle_value(str)
331
+ return nil unless str
332
+
333
+ if str.end_with?('deg')
334
+ str.chomp('deg').to_f
335
+ elsif str.end_with?('rad')
336
+ str.chomp('rad').to_f * 180.0 / Math::PI
337
+ elsif str.end_with?('grad')
338
+ str.chomp('grad').to_f * 0.9
339
+ elsif str.end_with?('turn')
340
+ str.chomp('turn').to_f * 360.0
341
+ else
342
+ str.to_f # Assume degrees
343
+ end
344
+ rescue
345
+ nil
346
+ end
347
+
348
+ # Color space conversion functions
349
+
350
+ def self.hsl_to_rgb(h, s, l)
351
+ h = h / 360.0 # Normalize hue to 0-1
352
+
353
+ c = (1 - (2 * l - 1).abs) * s
354
+ x = c * (1 - ((h * 6) % 2 - 1).abs)
355
+ m = l - c / 2
356
+
357
+ if h < 1.0/6
358
+ r, g, b = c, x, 0
359
+ elsif h < 2.0/6
360
+ r, g, b = x, c, 0
361
+ elsif h < 3.0/6
362
+ r, g, b = 0, c, x
363
+ elsif h < 4.0/6
364
+ r, g, b = 0, x, c
365
+ elsif h < 5.0/6
366
+ r, g, b = x, 0, c
367
+ else
368
+ r, g, b = c, 0, x
369
+ end
370
+
371
+ [r + m, g + m, b + m]
372
+ end
373
+
374
+ def self.hwb_to_rgb(h, w, b)
375
+ # Normalize values
376
+ h = h / 360.0
377
+
378
+ # Calculate RGB from HSL equivalent
379
+ if w + b >= 1
380
+ gray = w / (w + b)
381
+ [gray, gray, gray]
382
+ else
383
+ rgb = hsl_to_rgb(h * 360, 1, 0.5)
384
+ r, g, b_rgb = rgb
385
+
386
+ # Apply whiteness and blackness
387
+ r = r * (1 - w - b) + w
388
+ g = g * (1 - w - b) + w
389
+ b_rgb = b_rgb * (1 - w - b) + w
390
+
391
+ [r, g, b_rgb]
392
+ end
393
+ end
394
+
395
+ def self.lab_to_xyz(l, a, b)
396
+ # CIELAB to XYZ conversion (D65 white point)
397
+ y = (l + 16) / 116
398
+ x = a / 500 + y
399
+ z = y - b / 200
400
+
401
+ x = x**3 > 0.008856 ? x**3 : (x - 16/116) / 7.787
402
+ y = y**3 > 0.008856 ? y**3 : (y - 16/116) / 7.787
403
+ z = z**3 > 0.008856 ? z**3 : (z - 16/116) / 7.787
404
+
405
+ # D65 white point
406
+ x *= 0.95047
407
+ y *= 1.0
408
+ z *= 1.08883
409
+
410
+ [x, y, z]
411
+ end
412
+
413
+ def self.lch_to_lab(l, c, h)
414
+ h_rad = h * Math::PI / 180.0
415
+ a = c * Math.cos(h_rad)
416
+ b = c * Math.sin(h_rad)
417
+ [l, a, b]
418
+ end
419
+
420
+ def self.xyz_to_rgb(x, y, z)
421
+ # XYZ to linear RGB
422
+ r = x * 3.2406 + y * -1.5372 + z * -0.4986
423
+ g = x * -0.9689 + y * 1.8758 + z * 0.0415
424
+ b = x * 0.0557 + y * -0.2040 + z * 1.0570
425
+
426
+ # Linear RGB to sRGB
427
+ [r, g, b].map do |v|
428
+ if v > 0.0031308
429
+ 1.055 * (v ** (1/2.4)) - 0.055
430
+ else
431
+ 12.92 * v
432
+ end
433
+ end
434
+ end
435
+ end
436
+ end
437
+ end
@@ -1,5 +1,5 @@
1
1
  #
2
2
 
3
3
  module Abachrome
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
@@ -0,0 +1,53 @@
1
+ # Initial Security Assessment for Abachrome
2
+
3
+ ## Assessment Date
4
+ 2025-10-12
5
+
6
+ ## Scope
7
+ This assessment covers the Abachrome Ruby gem, focusing on color manipulation and conversion functionality.
8
+
9
+ ## Findings
10
+
11
+ ### Positive Security Aspects
12
+ - **No External Dependencies**: Pure Ruby implementation with minimal dependencies
13
+ - **Immutable Objects**: Color objects are immutable, preventing accidental modification
14
+ - **Input Validation**: All parsing operations include validation
15
+ - **No Network Operations**: All computations are local
16
+ - **No File System Access**: No reading/writing of files
17
+ - **No System Calls**: Pure mathematical computations
18
+
19
+ ### Potential Risks
20
+ - **Parsing Complex Inputs**: CSS color parsing could be vulnerable to malformed input
21
+ - **BigDecimal Precision**: High precision could lead to DoS via very large numbers
22
+ - **Memory Usage**: Large color palettes could consume significant memory
23
+
24
+ ### Recommendations
25
+ 1. Implement input length limits for parsing operations
26
+ 2. Add timeout protections for complex computations
27
+ 3. Validate coordinate ranges strictly
28
+ 4. Consider rate limiting for palette operations
29
+ 5. Regular dependency updates and security scans
30
+
31
+ ## Threat Model
32
+
33
+ ### Actors
34
+ - **Users**: Developers using the gem
35
+ - **Attackers**: Malicious users attempting to exploit parsing or computation
36
+
37
+ ### Assets
38
+ - System resources (CPU, memory)
39
+ - User data integrity
40
+
41
+ ### Threats
42
+ - DoS via computationally expensive inputs
43
+ - Memory exhaustion via large data structures
44
+ - Parsing exploits in CSS color functions
45
+
46
+ ### Mitigations
47
+ - Input sanitization and validation
48
+ - Reasonable limits on data sizes
49
+ - Immutable data structures
50
+ - Pure functional operations where possible
51
+
52
+ ## Conclusion
53
+ The gem has a strong security posture due to its pure computational nature and lack of external interfaces. Focus should be on input validation and resource limits for production use.
data/security/vex.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "document": {
3
+ "id": "abachrome-vex",
4
+ "author": "Durable Programming",
5
+ "timestamp": "2025-10-12T00:00:00Z",
6
+ "version": "1.0"
7
+ },
8
+ "product_tree": {
9
+ "branches": [
10
+ {
11
+ "type": "package-url",
12
+ "name": "pkg:gem/abachrome",
13
+ "product": {
14
+ "name": "Abachrome",
15
+ "version": "*"
16
+ }
17
+ }
18
+ ]
19
+ },
20
+ "vulnerabilities": []
21
+ }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: abachrome
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Durable Programming
@@ -37,8 +37,13 @@ files:
37
37
  - ".envrc"
38
38
  - ".rubocop.yml"
39
39
  - CHANGELOG.md
40
+ - CLA.md
41
+ - CODE-OF-CONDUCT.md
42
+ - LICENSE
40
43
  - README.md
41
44
  - Rakefile
45
+ - SECURITY.md
46
+ - abachrome.gemspec
42
47
  - demos/ncurses/plasma.rb
43
48
  - devenv.lock
44
49
  - devenv.nix
@@ -54,23 +59,34 @@ files:
54
59
  - lib/abachrome/color_mixins/to_oklch.rb
55
60
  - lib/abachrome/color_mixins/to_srgb.rb
56
61
  - lib/abachrome/color_models/hsv.rb
62
+ - lib/abachrome/color_models/lms.rb
57
63
  - lib/abachrome/color_models/oklab.rb
58
64
  - lib/abachrome/color_models/oklch.rb
59
65
  - lib/abachrome/color_models/rgb.rb
66
+ - lib/abachrome/color_models/xyz.rb
60
67
  - lib/abachrome/color_space.rb
61
68
  - lib/abachrome/converter.rb
62
69
  - lib/abachrome/converters/base.rb
70
+ - lib/abachrome/converters/lms_to_lrgb.rb
71
+ - lib/abachrome/converters/lms_to_srgb.rb
72
+ - lib/abachrome/converters/lms_to_xyz.rb
73
+ - lib/abachrome/converters/lrgb_to_lms.rb
63
74
  - lib/abachrome/converters/lrgb_to_oklab.rb
64
75
  - lib/abachrome/converters/lrgb_to_srgb.rb
76
+ - lib/abachrome/converters/lrgb_to_xyz.rb
77
+ - lib/abachrome/converters/oklab_to_lms.rb
65
78
  - lib/abachrome/converters/oklab_to_lrgb.rb
66
79
  - lib/abachrome/converters/oklab_to_oklch.rb
67
80
  - lib/abachrome/converters/oklab_to_srgb.rb
68
81
  - lib/abachrome/converters/oklch_to_lrgb.rb
69
82
  - lib/abachrome/converters/oklch_to_oklab.rb
70
83
  - lib/abachrome/converters/oklch_to_srgb.rb
84
+ - lib/abachrome/converters/oklch_to_xyz.rb
71
85
  - lib/abachrome/converters/srgb_to_lrgb.rb
72
86
  - lib/abachrome/converters/srgb_to_oklab.rb
73
87
  - lib/abachrome/converters/srgb_to_oklch.rb
88
+ - lib/abachrome/converters/xyz_to_lms.rb
89
+ - lib/abachrome/converters/xyz_to_oklab.rb
74
90
  - lib/abachrome/gamut/base.rb
75
91
  - lib/abachrome/gamut/p3.rb
76
92
  - lib/abachrome/gamut/rec2020.rb
@@ -86,11 +102,14 @@ files:
86
102
  - lib/abachrome/palette_mixins/interpolate.rb
87
103
  - lib/abachrome/palette_mixins/resample.rb
88
104
  - lib/abachrome/palette_mixins/stretch_luminance.rb
105
+ - lib/abachrome/parsers/css.rb
89
106
  - lib/abachrome/parsers/hex.rb
90
107
  - lib/abachrome/to_abcd.rb
91
108
  - lib/abachrome/version.rb
92
109
  - logo.png
93
110
  - logo.webp
111
+ - security/assesments/2025-10-12-SECURITY_ASSESSMENT.md
112
+ - security/vex.json
94
113
  homepage: https://github.com/durableprogramming/abachrome
95
114
  licenses:
96
115
  - MIT