airbrake-ruby 1.1.0 → 1.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: feeb44b47c5b58cdf1f478065f84640af9102ab9
4
- data.tar.gz: 2319d39125a79fb075a3a629fa3dc8c927e95dc2
3
+ metadata.gz: 28a4924a3ba67bad999146c40bb8b4a18d42c07e
4
+ data.tar.gz: 08b97f8c2ff47d1e78665df246f71b65fbc401fe
5
5
  SHA512:
6
- metadata.gz: 78028e33ffe3b0929fc6f5e26b18933d39df58edade9cbf45bd1ca9025d2121201cbf8fa114f9c9ff5614336d2521012d44d9b05dfadb0af08d9710ee40b3bcd
7
- data.tar.gz: 14e8924670cac95429572ca814d61421459797059a357a39cf23b18241bf8cb9ff084bbb5b6357441e4f85ae5e11cbff2aa18e24668ec03bc493cb45131dbe8a
6
+ metadata.gz: ac71aa148439f4cd54a6b6b6c499d0a5e832f58b0e95897dd837a65219cea8a244f700c38f59340f74f1db8ee82cc4b4fead1dfde267d0adb1207c6819c80d29
7
+ data.tar.gz: 8c6a568e091568b2b3cbb56f1897753bbaa30b5343b9b14055ff815e0fd7244c34036ff6a32f0395d9473573dc2336db414f6703fc49be5bffdbe6f54cade94c
@@ -3,7 +3,6 @@ require 'logger'
3
3
  require 'json'
4
4
  require 'thread'
5
5
  require 'set'
6
- require 'English'
7
6
  require 'socket'
8
7
 
9
8
  require 'airbrake-ruby/version'
@@ -196,6 +195,7 @@ module Airbrake
196
195
  # @return [void]
197
196
  # @since v5.0.0
198
197
  # @see .blacklist_keys
198
+ # @deprecated Please use {Airbrake::Config#whitelist_keys} instead
199
199
  def whitelist_keys(keys, notifier = :default)
200
200
  call_notifier(notifier, __method__, keys)
201
201
  end
@@ -213,6 +213,7 @@ module Airbrake
213
213
  # @return [void]
214
214
  # @since v5.0.0
215
215
  # @see .whitelist_keys
216
+ # @deprecated Please use {Airbrake::Config#blacklist_keys} instead
216
217
  def blacklist_keys(keys, notifier = :default)
217
218
  call_notifier(notifier, __method__, keys)
218
219
  end
@@ -291,8 +292,3 @@ module Airbrake
291
292
  end
292
293
  end
293
294
  end
294
-
295
- # Notify of unhandled exceptions, if there were any, but ignore SystemExit.
296
- at_exit do
297
- Airbrake.notify_sync($ERROR_INFO) if $ERROR_INFO
298
- end
@@ -14,6 +14,7 @@ module Airbrake
14
14
  @sender = SyncSender.new(config)
15
15
  @closed = false
16
16
  @workers = ThreadGroup.new
17
+ @mutex = Mutex.new
17
18
  @pid = nil
18
19
  end
19
20
 
@@ -37,19 +38,22 @@ module Airbrake
37
38
  # @return [void]
38
39
  # @raise [Airbrake::Error] when invoked more than one time
39
40
  def close
40
- if closed?
41
- raise Airbrake::Error, 'attempted to close already closed sender'
42
- end
41
+ threads = @mutex.synchronize do
42
+ if closed?
43
+ raise Airbrake::Error, 'attempted to close already closed sender'
44
+ end
43
45
 
44
- unless @unsent.empty?
45
- msg = "#{LOG_LABEL} waiting to send #{@unsent.size} unsent notice(s)..."
46
- @config.logger.debug(msg + ' (Ctrl-C to abort)')
47
- end
46
+ unless @unsent.empty?
47
+ msg = "#{LOG_LABEL} waiting to send #{@unsent.size} unsent notice(s)..."
48
+ @config.logger.debug(msg + ' (Ctrl-C to abort)')
49
+ end
48
50
 
49
- @config.workers.times { @unsent << :stop }
50
- @workers.list.each(&:join)
51
- @closed = true
51
+ @config.workers.times { @unsent << :stop }
52
+ @closed = true
53
+ @workers.list.dup
54
+ end
52
55
 
56
+ threads.each(&:join)
53
57
  @config.logger.debug("#{LOG_LABEL} closed")
54
58
  end
55
59
 
@@ -57,6 +57,18 @@ module Airbrake
57
57
  # @return [Integer] The HTTP timeout in seconds.
58
58
  attr_accessor :timeout
59
59
 
60
+ ##
61
+ # @return [Array<String, Symbol, Regexp>] the keys, which should be
62
+ # filtered
63
+ # @since 1.2.0
64
+ attr_accessor :blacklist_keys
65
+
66
+ ##
67
+ # @return [Array<String, Symbol, Regexp>] the keys, which shouldn't be
68
+ # filtered
69
+ # @since 1.2.0
70
+ attr_accessor :whitelist_keys
71
+
60
72
  ##
61
73
  # @param [Hash{Symbol=>Object}] user_config the hash to be used to build the
62
74
  # config
@@ -76,6 +88,9 @@ module Airbrake
76
88
 
77
89
  self.timeout = user_config[:timeout]
78
90
 
91
+ self.blacklist_keys = []
92
+ self.whitelist_keys = []
93
+
79
94
  merge(user_config)
80
95
  end
81
96
 
@@ -30,7 +30,13 @@ module Airbrake
30
30
  # @return [Boolean] true if the key matches at least one pattern, false
31
31
  # otherwise
32
32
  def should_filter?(key)
33
- @patterns.any? { |pattern| key.to_s.match(pattern) }
33
+ @patterns.any? do |pattern|
34
+ if pattern.is_a?(Regexp)
35
+ key.match(pattern)
36
+ else
37
+ key.to_s == pattern.to_s
38
+ end
39
+ end
34
40
  end
35
41
  end
36
42
  end
@@ -9,13 +9,17 @@ module Airbrake
9
9
  # @see KeysWhitelist
10
10
  # @see KeysBlacklist
11
11
  module KeysFilter
12
+ ##
13
+ # @return [String] The label to replace real values of filtered payload
14
+ FILTERED = '[Filtered]'.freeze
15
+
12
16
  ##
13
17
  # Creates a new KeysBlacklist or KeysWhitelist filter that uses the given
14
18
  # +patterns+ for filtering a notice's payload.
15
19
  #
16
20
  # @param [Array<String,Regexp,Symbol>] patterns
17
21
  def initialize(*patterns)
18
- @patterns = patterns.map(&:to_s)
22
+ @patterns = patterns
19
23
  end
20
24
 
21
25
  ##
@@ -28,6 +32,10 @@ module Airbrake
28
32
  def call(notice)
29
33
  FILTERABLE_KEYS.each { |key| filter_hash(notice[key]) }
30
34
 
35
+ if notice[:context][:user] && should_filter?(:user)
36
+ notice[:context][:user] = FILTERED
37
+ end
38
+
31
39
  return unless notice[:context][:url]
32
40
  url = URI(notice[:context][:url])
33
41
  return if url.nil? || url.query.nil?
@@ -46,7 +54,7 @@ module Airbrake
46
54
  def filter_hash(hash)
47
55
  hash.each_key do |key|
48
56
  if should_filter?(key)
49
- hash[key] = '[Filtered]'.freeze
57
+ hash[key] = FILTERED
50
58
  elsif hash[key].is_a?(Hash)
51
59
  filter_hash(hash[key])
52
60
  end
@@ -30,7 +30,13 @@ module Airbrake
30
30
  # @return [Boolean] true if the key doesn't match any pattern, false
31
31
  # otherwise.
32
32
  def should_filter?(key)
33
- @patterns.none? { |pattern| key.to_s.match(pattern) }
33
+ @patterns.none? do |pattern|
34
+ if pattern.is_a?(Regexp)
35
+ key.match(pattern)
36
+ else
37
+ key.to_s == pattern.to_s
38
+ end
39
+ end
34
40
  end
35
41
  end
36
42
  end
@@ -7,6 +7,10 @@ module Airbrake
7
7
  # @api private
8
8
  # @since v5.0.0
9
9
  class Notifier
10
+ ##
11
+ # @return [String] the label to be prepended to the log output
12
+ LOG_LABEL = '**Airbrake:'.freeze
13
+
10
14
  ##
11
15
  # Creates a new Airbrake notifier with the given config options.
12
16
  #
@@ -31,6 +35,15 @@ module Airbrake
31
35
  end
32
36
 
33
37
  @filter_chain = FilterChain.new(@config)
38
+
39
+ if @config.blacklist_keys.any?
40
+ add_filter(Filters::KeysBlacklist.new(*@config.blacklist_keys))
41
+ end
42
+
43
+ if @config.whitelist_keys.any?
44
+ add_filter(Filters::KeysWhitelist.new(*@config.whitelist_keys))
45
+ end
46
+
34
47
  @async_sender = AsyncSender.new(@config)
35
48
  @sync_sender = SyncSender.new(@config)
36
49
  end
@@ -60,13 +73,23 @@ module Airbrake
60
73
 
61
74
  ##
62
75
  # @macro see_public_api_method
76
+ # @deprecated Please use {Airbrake::Config#whitelist_keys} instead
63
77
  def whitelist_keys(keys)
78
+ @config.logger.warn(
79
+ "#{LOG_LABEL} Airbrake.whitelist_keys is deprecated. Please use the " \
80
+ "whitelist_keys option instead (https://goo.gl/sQwpYN)"
81
+ )
64
82
  add_filter(Filters::KeysWhitelist.new(*keys))
65
83
  end
66
84
 
67
85
  ##
68
86
  # @macro see_public_api_method
87
+ # @deprecated Please use {Airbrake::Config#blacklist_keys} instead
69
88
  def blacklist_keys(keys)
89
+ @config.logger.warn(
90
+ "#{LOG_LABEL} Airbrake.blacklist_keys is deprecated. Please use the " \
91
+ "blacklist_keys option instead (https://goo.gl/jucrFt)"
92
+ )
70
93
  add_filter(Filters::KeysBlacklist.new(*keys))
71
94
  end
72
95
 
@@ -8,18 +8,6 @@ module Airbrake
8
8
  # @return [String] body for HTTP requests
9
9
  CONTENT_TYPE = 'application/json'.freeze
10
10
 
11
- ##
12
- # @return [Array] the errors to be rescued and logged during an HTTP request
13
- HTTP_ERRORS = [
14
- Timeout::Error,
15
- Net::HTTPBadResponse,
16
- Net::HTTPHeaderSyntaxError,
17
- Errno::ECONNRESET,
18
- Errno::ECONNREFUSED,
19
- EOFError,
20
- OpenSSL::SSL::SSLError
21
- ].freeze
22
-
23
11
  ##
24
12
  # @param [Airbrake::Config] config
25
13
  def initialize(config)
@@ -39,7 +27,7 @@ module Airbrake
39
27
 
40
28
  begin
41
29
  response = https.request(req)
42
- rescue *HTTP_ERRORS => ex
30
+ rescue => ex
43
31
  @config.logger.error("#{LOG_LABEL} HTTP error: #{ex}")
44
32
  return
45
33
  end
@@ -3,5 +3,5 @@
3
3
  module Airbrake
4
4
  ##
5
5
  # @return [String] the library version
6
- AIRBRAKE_RUBY_VERSION = '1.1.0'.freeze
6
+ AIRBRAKE_RUBY_VERSION = '1.2.0'.freeze
7
7
  end
@@ -66,6 +66,14 @@ RSpec.describe Airbrake::Config do
66
66
  it "doesn't set default timeout" do
67
67
  expect(config.timeout).to be_nil
68
68
  end
69
+
70
+ it "doesn't set default blacklist" do
71
+ expect(config.blacklist_keys).to be_empty
72
+ end
73
+
74
+ it "doesn't set default whitelist" do
75
+ expect(config.whitelist_keys).to be_empty
76
+ end
69
77
  end
70
78
  end
71
79
  end
@@ -495,11 +495,11 @@ RSpec.describe Airbrake::Notifier do
495
495
  it "accepts strings" do
496
496
  @airbrake.blacklist_keys('bingo')
497
497
 
498
- @airbrake.notify_sync(ex, bingo: 'bango')
498
+ @airbrake.notify_sync(ex, bingo: 'bango', bbingoo: 'bbangoo')
499
499
 
500
500
  expect(
501
501
  a_request(:post, endpoint).
502
- with(body: /"params":{"bingo":"\[Filtered\]"}/)
502
+ with(body: /"params":{"bingo":"\[Filtered\]","bbingoo":"bbangoo"}/)
503
503
  ).to have_been_made.once
504
504
  end
505
505
  end
@@ -553,6 +553,17 @@ RSpec.describe Airbrake::Notifier do
553
553
  with(body: expected_body)
554
554
  ).to have_been_made.once
555
555
  end
556
+
557
+ it "filters out user" do
558
+ @airbrake.blacklist_keys('user')
559
+
560
+ notice = @airbrake.build_notice(ex)
561
+ notice[:context][:user] = { id: 1337, name: 'Bingo Bango' }
562
+
563
+ @airbrake.notify_sync(notice)
564
+
565
+ expect_a_request_with_body(/"user":"\[Filtered\]"/)
566
+ end
556
567
  end
557
568
 
558
569
  describe "#whitelist_keys" do
@@ -586,13 +597,20 @@ RSpec.describe Airbrake::Notifier do
586
597
  it "accepts strings" do
587
598
  @airbrake.whitelist_keys('bash')
588
599
 
589
- @airbrake.notify_sync(ex, bingo: 'bango', bongo: 'bish', bash: 'bosh')
590
-
591
- body = /"params":{"bingo":"\[Filtered\]","bongo":"\[Filtered\]","bash":"bosh"}/
600
+ @airbrake.notify_sync(
601
+ ex,
602
+ bingo: 'bango',
603
+ bongo: 'bish',
604
+ bash: 'bosh',
605
+ bbashh: 'bboshh'
606
+ )
592
607
 
593
608
  expect(
594
609
  a_request(:post, endpoint).
595
- with(body: body)
610
+ with(
611
+ body: /"params":{"bingo":"\[Filtered\]","bongo":"\[Filtered\]",
612
+ "bash":"bosh","bbashh":"\[Filtered\]"}/x
613
+ )
596
614
  ).to have_been_made.once
597
615
  end
598
616
  end
@@ -1,6 +1,10 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe Airbrake::Notifier do
4
+ def expect_a_request_with_body(body)
5
+ expect(a_request(:post, endpoint).with(body: body)).to have_been_made.once
6
+ end
7
+
4
8
  let(:project_id) { 105138 }
5
9
  let(:project_key) { 'fd04e13d806a90f96614ad8e529b2822' }
6
10
  let(:localhost) { 'http://localhost:8080' }
@@ -213,5 +217,200 @@ RSpec.describe Airbrake::Notifier do
213
217
  include_examples 'sent notice', environment: :development
214
218
  end
215
219
  end
220
+
221
+ describe ":blacklist_keys" do
222
+ describe "the list of values" do
223
+ it "accepts regexps" do
224
+ params = { blacklist_keys: [/\Abin/] }
225
+ airbrake = described_class.new(airbrake_params.merge(params))
226
+
227
+ airbrake.notify_sync(ex, bingo: 'bango')
228
+
229
+ expect_a_request_with_body(/"params":{"bingo":"\[Filtered\]"}/)
230
+ end
231
+
232
+ it "accepts symbols" do
233
+ params = { blacklist_keys: [:bingo] }
234
+ airbrake = described_class.new(airbrake_params.merge(params))
235
+
236
+ airbrake.notify_sync(ex, bingo: 'bango')
237
+
238
+ expect_a_request_with_body(/"params":{"bingo":"\[Filtered\]"}/)
239
+ end
240
+
241
+ it "accepts strings" do
242
+ params = { blacklist_keys: ['bingo'] }
243
+ airbrake = described_class.new(airbrake_params.merge(params))
244
+
245
+ airbrake.notify_sync(ex, bingo: 'bango')
246
+
247
+ expect_a_request_with_body(/"params":{"bingo":"\[Filtered\]"}/)
248
+ end
249
+ end
250
+
251
+ describe "hash values" do
252
+ context "non-recursive" do
253
+ it "filters nested hashes" do
254
+ params = { blacklist_keys: ['bish'] }
255
+ airbrake = described_class.new(airbrake_params.merge(params))
256
+
257
+ airbrake.notify_sync(ex, bongo: { bish: 'bash' })
258
+
259
+ expect_a_request_with_body(/"params":{"bongo":{"bish":"\[Filtered\]"}}/)
260
+ end
261
+ end
262
+
263
+ context "recursive" do
264
+ it "filters recursive hashes" do
265
+ params = { blacklist_keys: ['bango'] }
266
+ airbrake = described_class.new(airbrake_params.merge(params))
267
+
268
+ bongo = { bingo: {} }
269
+ bongo[:bingo][:bango] = bongo
270
+
271
+ airbrake.notify_sync(ex, bongo)
272
+
273
+ expect_a_request_with_body(/"params":{"bingo":{"bango":"\[Filtered\]"}}/)
274
+ end
275
+ end
276
+ end
277
+
278
+ it "filters query parameters correctly" do
279
+ params = { blacklist_keys: ['bish'] }
280
+ airbrake = described_class.new(airbrake_params.merge(params))
281
+
282
+ notice = airbrake.build_notice(ex)
283
+ notice[:context][:url] = 'http://localhost:3000/crash?foo=bar&baz=bongo&bish=bash&color=%23FFAAFF'
284
+
285
+ airbrake.notify_sync(notice)
286
+
287
+ # rubocop:disable Metrics/LineLength
288
+ expected_body =
289
+ %r("context":{.*"url":"http://localhost:3000/crash\?foo=bar&baz=bongo&bish=\[Filtered\]&color=%23FFAAFF".*})
290
+ # rubocop:enable Metrics/LineLength
291
+
292
+ expect_a_request_with_body(expected_body)
293
+ end
294
+
295
+ it "filters out user" do
296
+ params = { blacklist_keys: ['user'] }
297
+ airbrake = described_class.new(airbrake_params.merge(params))
298
+
299
+ notice = airbrake.build_notice(ex)
300
+ notice[:context][:user] = { id: 1337, name: 'Bingo Bango' }
301
+
302
+ airbrake.notify_sync(notice)
303
+
304
+ expect_a_request_with_body(/"user":"\[Filtered\]"/)
305
+ end
306
+ end
307
+
308
+ describe ":whitelist_keys" do
309
+ describe "the list of values" do
310
+ it "accepts regexes" do
311
+ params = { whitelist_keys: [/\Abin/] }
312
+ airbrake = described_class.new(airbrake_params.merge(params))
313
+
314
+ airbrake.notify_sync(ex, bingo: 'bango', bongo: 'bish', bash: 'bosh')
315
+
316
+ expect_a_request_with_body(
317
+ /"params":{"bingo":"bango","bongo":"\[Filtered\]","bash":"\[Filtered\]"}/
318
+ )
319
+ end
320
+
321
+ it "accepts symbols" do
322
+ params = { whitelist_keys: [:bongo] }
323
+ airbrake = described_class.new(airbrake_params.merge(params))
324
+
325
+ airbrake.notify_sync(ex, bingo: 'bango', bongo: 'bish', bash: 'bosh')
326
+
327
+ expect_a_request_with_body(
328
+ /"params":{"bingo":"\[Filtered\]","bongo":"bish","bash":"\[Filtered\]"}/
329
+ )
330
+ end
331
+
332
+ it "accepts strings" do
333
+ params = { whitelist_keys: ['bash'] }
334
+ airbrake = described_class.new(airbrake_params.merge(params))
335
+
336
+ airbrake.notify_sync(
337
+ ex,
338
+ bingo: 'bango',
339
+ bongo: 'bish',
340
+ bash: 'bosh',
341
+ bbashh: 'bboshh'
342
+ )
343
+
344
+ expect_a_request_with_body(
345
+ /"params":{"bingo":"\[Filtered\]","bongo":"\[Filtered\]",
346
+ "bash":"bosh","bbashh":"\[Filtered\]"}/x
347
+ )
348
+ end
349
+ end
350
+
351
+ describe "hash values" do
352
+ context "non-recursive" do
353
+ it "filters out everything but the provided keys" do
354
+ params = { whitelist_keys: %w(bongo bish) }
355
+ airbrake = described_class.new(airbrake_params.merge(params))
356
+
357
+ airbrake.notify_sync(ex, bingo: 'bango', bongo: { bish: 'bash' })
358
+
359
+ expect_a_request_with_body(
360
+ /"params":{"bingo":"\[Filtered\]","bongo":{"bish":"bash"}}/
361
+ )
362
+ end
363
+ end
364
+
365
+ context "recursive" do
366
+ it "errors when nested hashes are not filtered" do
367
+ params = { whitelist_keys: %w(bingo bango) }
368
+ airbrake = described_class.new(airbrake_params.merge(params))
369
+
370
+ bongo = { bingo: {} }
371
+ bongo[:bingo][:bango] = bongo
372
+
373
+ if RUBY_ENGINE == 'jruby'
374
+ # JRuby might raise two different exceptions, which represent the
375
+ # same thing. One is a Java exception, the other is a Ruby
376
+ # exception. It's probably a JRuby bug:
377
+ # https://github.com/jruby/jruby/issues/1903
378
+ begin
379
+ expect do
380
+ airbrake.notify_sync(ex, bongo)
381
+ end.to raise_error(SystemStackError)
382
+ rescue RSpec::Expectations::ExpectationNotMetError
383
+ expect do
384
+ airbrake.notify_sync(ex, bongo)
385
+ end.to raise_error(java.lang.StackOverflowError)
386
+ end
387
+ else
388
+ expect do
389
+ airbrake.notify_sync(ex, bongo)
390
+ end.to raise_error(SystemStackError)
391
+ end
392
+ end
393
+ end
394
+ end
395
+
396
+ describe "context/url" do
397
+ it "filters query parameters correctly" do
398
+ params = { whitelist_keys: %w(bish) }
399
+ airbrake = described_class.new(airbrake_params.merge(params))
400
+
401
+ notice = airbrake.build_notice(ex)
402
+ notice[:context][:url] = 'http://localhost:3000/crash?foo=bar&baz=bongo&bish=bash'
403
+
404
+ airbrake.notify_sync(notice)
405
+
406
+ # rubocop:disable Metrics/LineLength
407
+ expected_body =
408
+ %r("context":{.*"url":"http://localhost:3000/crash\?foo=\[Filtered\]&baz=\[Filtered\]&bish=bash".*})
409
+ # rubocop:enable Metrics/LineLength
410
+
411
+ expect_a_request_with_body(expected_body)
412
+ end
413
+ end
414
+ end
216
415
  end
217
416
  end
@@ -10,4 +10,18 @@ RSpec.describe Airbrake::SyncSender do
10
10
  expect(https.read_timeout).to eq(10)
11
11
  end
12
12
  end
13
+
14
+ describe "#send" do
15
+ it "catches exceptions raised when sending" do
16
+ stdout = StringIO.new
17
+ config = Airbrake::Config.new(logger: Logger.new(stdout))
18
+ sender = described_class.new config
19
+ notice = Airbrake::Notice.new(config, AirbrakeTestError.new)
20
+ https = double("foo")
21
+ allow(sender).to receive(:build_https).and_return(https)
22
+ allow(https).to receive(:request).and_raise(StandardError.new('foo'))
23
+ expect(sender.send(notice)).to be_nil
24
+ expect(stdout.string).to match(/ERROR -- : .+ HTTP error: foo/)
25
+ end
26
+ end
13
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: airbrake-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Airbrake Technologies, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-02-26 00:00:00.000000000 Z
11
+ date: 2016-03-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -160,3 +160,4 @@ test_files:
160
160
  - spec/payload_truncator_spec.rb
161
161
  - spec/spec_helper.rb
162
162
  - spec/sync_sender_spec.rb
163
+ has_rdoc: