• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stefanberger / libtpms / #2033

04 Dec 2025 02:24PM UTC coverage: 77.218% (-0.003%) from 77.221%
#2033

push

travis-ci

web-flow
Merge 914712db4 into 4f71e9b45

50 of 63 new or added lines in 13 files covered. (79.37%)

897 existing lines in 26 files now uncovered.

36122 of 46779 relevant lines covered (77.22%)

125357.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.17
/src/tpm2/CommandCodeAttributes.c
1
// SPDX-License-Identifier: BSD-2-Clause
2

3
//** Introduction
4
// This file contains the functions for testing various command properties.
5

6
//** Includes and Defines
7

8
#include "Tpm.h"
9
#include "CommandCodeAttributes_fp.h"
10

11
// Set the default value for CC_VEND if not already set
12
#ifndef CC_VEND
13
#  define CC_VEND (TPM_CC)(0x20000000)
14
#endif
15

16
typedef UINT16 ATTRIBUTE_TYPE;
17

18
// The following file is produced from the command tables in part 3 of the
19
// specification. It defines the attributes for each of the commands.
20
// NOTE: This file is currently produced by an automated process. Files
21
// produced from Part 2 or Part 3 tables through automated processes are not
22
// included in the specification so that their is no ambiguity about the
23
// table containing the information being the normative definition.
24
#define _COMMAND_CODE_ATTRIBUTES_
25
#include "CommandAttributeData.h"
26

27
//** Command Attribute Functions
28

29
//*** NextImplementedIndex()                                                        // libtpms added begin
30
// This function is used when the lists are not compressed. In a compressed list,
31
// only the implemented commands are present. So, a search might find a value
32
// but that value may not be implemented. This function checks to see if the input
33
// commandIndex points to an implemented command and, if not, it searches upwards
34
// until it finds one. When the list is compressed, this function gets defined
35
// as a no-op.
36
//  Return Type: COMMAND_INDEX
37
//  UNIMPLEMENTED_COMMAND_INDEX     command is not implemented
38
//  other                           index of the command
39
#if !COMPRESSED_LISTS
40
static COMMAND_INDEX NextImplementedIndex(COMMAND_INDEX commandIndex)
162✔
41
{
42
    for(; commandIndex < COMMAND_COUNT; commandIndex++)
163✔
43
    {
44
       if(RuntimeCommandsCheckEnabled(&g_RuntimeProfile.RuntimeCommands,        // libtpms added begin
163✔
45
                                      GET_ATTRIBUTE(s_ccAttr[commandIndex],
163✔
46
                                                    TPMA_CC, commandIndex)))        // libtpms added end
47
            return commandIndex;
162✔
48
    }
49
    return UNIMPLEMENTED_COMMAND_INDEX;
50
}
51
#else
52
#  define NextImplementedIndex(x) (x)
53
#endif                                                                                // libtpms added end
54

55
//*** GetClosestCommandIndex()
56
// This function returns the command index for the command with a value that is
57
// equal to or greater than the input value
58
//  Return Type: COMMAND_INDEX
59
//  UNIMPLEMENTED_COMMAND_INDEX     command is not implemented
60
//  other                           index of a command
61
COMMAND_INDEX
62
GetClosestCommandIndex(TPM_CC commandCode  // IN: the command code to start at
174✔
63
)
64
{
65
    BOOL          vendor      = (commandCode & CC_VEND) != 0;
174✔
66
    COMMAND_INDEX searchIndex = (COMMAND_INDEX)commandCode;
174✔
67

68
    // The commandCode is a UINT32 and the search index is UINT16. We are going to
69
    // search for a match but need to make sure that the commandCode value is not
70
    // out of range. To do this, need to clear the vendor bit of the commandCode
71
    // (if set) and compare the result to the 16-bit searchIndex value. If it is
72
    // out of range, indicate that the command is not implemented
73
    if((commandCode & ~CC_VEND) != searchIndex)
174✔
74
        return UNIMPLEMENTED_COMMAND_INDEX;
75

76
    // if there is at least one vendor command, the last entry in the array will
77
    // have the v bit set. If the input commandCode is larger than the last
78
    // vendor-command, then it is out of range.
79
    if(vendor)
162✔
80
    {
81
#if VENDOR_COMMAND_ARRAY_COUNT > 0
82
        COMMAND_INDEX commandIndex;
83
        COMMAND_INDEX min;
84
        COMMAND_INDEX max;
85
        int           diff;
86
#  if LIBRARY_COMMAND_ARRAY_SIZE == COMMAND_COUNT
87
#    error "Constants are not consistent."
88
#  endif
89
        // Check to see if the value is equal to or below the minimum
90
        // entry.
91
        // Note: Put this check first so that the typical case of only one vendor-
92
        // specific command doesn't waste any more time.
93
        if(GET_ATTRIBUTE(s_ccAttr[LIBRARY_COMMAND_ARRAY_SIZE], TPMA_CC, commandIndex)
94
           >= searchIndex)
95
        {
96
            // the vendor array is always assumed to be packed so there is
97
            // no need to check to see if the command is implemented
98
            return LIBRARY_COMMAND_ARRAY_SIZE;
99
        }
100
        // See if this is out of range on the top
101
        if(GET_ATTRIBUTE(s_ccAttr[COMMAND_COUNT - 1], TPMA_CC, commandIndex)
102
           < searchIndex)
103
        {
104
            return UNIMPLEMENTED_COMMAND_INDEX;
105
        }
106
        commandIndex = UNIMPLEMENTED_COMMAND_INDEX;  // Needs initialization to keep
107
                                                     // compiler happy
108
        min  = LIBRARY_COMMAND_ARRAY_SIZE;           // first vendor command
109
        max  = COMMAND_COUNT - 1;                    // last vendor command
110
        diff = 1;                                    // needs initialization to keep
111
                                                     // compiler happy
112
        while(min <= max)
113
        {
114
            commandIndex = (min + max + 1) / 2;
115
            diff = GET_ATTRIBUTE(s_ccAttr[commandIndex], TPMA_CC, commandIndex)
116
                   - searchIndex;
117
            if(diff == 0)
118
                return commandIndex;
119
            if(diff > 0)
120
                max = commandIndex - 1;
121
            else
122
                min = commandIndex + 1;
123
        }
124
        // didn't find and exact match. commandIndex will be pointing at the last
125
        // item tested. If 'diff' is positive, then the last item tested was
126
        // larger index of the command code so it is the smallest value
127
        // larger than the requested value.
128
        if(diff > 0)
129
            return commandIndex;
130
        // if 'diff' is negative, then the value tested was smaller than
131
        // the commandCode index and the next higher value is the correct one.
132
        // Note: this will necessarily be in range because of the earlier check
133
        // that the index was within range.
134
        return commandIndex + 1;
135
#else
136
        // If there are no vendor commands so anything with the vendor bit set is out
137
        // of range
138
        return UNIMPLEMENTED_COMMAND_INDEX;
139
#endif
140
    }
141
    // Get here if the V-Bit was not set in 'commandCode'
142

143
    if(GET_ATTRIBUTE(s_ccAttr[LIBRARY_COMMAND_ARRAY_SIZE - 1], TPMA_CC, commandIndex)
162✔
144
       < searchIndex)
145
    {
146
        // requested index is out of the range to the top
147
#if VENDOR_COMMAND_ARRAY_COUNT > 0
148
        // If there are vendor commands, then the first vendor command
149
        // is the next value greater than the commandCode.
150
        // NOTE: we got here if the starting index did not have the V bit but we
151
        // reached the end of the array of library commands (non-vendor). Since
152
        // there is at least one vendor command, and vendor commands are always
153
        // in a compressed list that starts after the library list, the next
154
        // index value contains a valid vendor command.
155
        return LIBRARY_COMMAND_ARRAY_SIZE;
156
#else
157
        // if there are no vendor commands, then this is out of range
158
        return UNIMPLEMENTED_COMMAND_INDEX;
159
#endif
160
    }
161
    // If the request is lower than any value in the array, then return
162
    // the lowest value (needs to be an index for an implemented command
163
    if(GET_ATTRIBUTE(s_ccAttr[0], TPMA_CC, commandIndex) >= searchIndex)
162✔
164
    {
165
        return NextImplementedIndex(0);                                // libtpms changed
114✔
166
    }
167
    else
168
    {
169
#if COMPRESSED_LISTS                                                        // libtpms added
170
        COMMAND_INDEX commandIndex = UNIMPLEMENTED_COMMAND_INDEX;
171
        COMMAND_INDEX min          = 0;
172
        COMMAND_INDEX max          = LIBRARY_COMMAND_ARRAY_SIZE - 1;
173
        int           diff         = 1;
174
#  if LIBRARY_COMMAND_ARRAY_SIZE == 0
175
#    error "Something is terribly wrong"
176
#  endif
177
        // The s_ccAttr array contains an extra entry at the end (a zero value).
178
        // Don't count this as an array entry. This means that max should start
179
        // out pointing to the last valid entry in the array which is - 2
180
        VERIFY(max
181
                   == (sizeof(s_ccAttr) / sizeof(TPMA_CC) - VENDOR_COMMAND_ARRAY_COUNT
182
                       - 2),
183
               FATAL_ERROR_ASSERT,
184
               UNIMPLEMENTED_COMMAND_INDEX);
185
        while(min <= max)
186
        {
187
            commandIndex = (min + max + 1) / 2;
188
            diff = GET_ATTRIBUTE(s_ccAttr[commandIndex], TPMA_CC, commandIndex)
189
                   - searchIndex;
190
            if(diff == 0)
191
                return commandIndex;
192
            if(diff > 0)
193
                max = commandIndex - 1;
194
            else
195
                min = commandIndex + 1;
196
        }
197
        // didn't find and exact match. commandIndex will be pointing at the
198
        // last item tested. If diff is positive, then the last item tested was
199
        // larger index of the command code so it is the smallest value
200
        // larger than the requested value.
201
        if(diff > 0)
202
            return commandIndex;
203
        // if diff is negative, then the value tested was smaller than
204
        // the commandCode index and the next higher value is the correct one.
205
        // Note: this will necessarily be in range because of the earlier check
206
        // that the index was within range.
207
        return commandIndex + 1;
208
#else                                                                        // libtpms added begin
209
        // The list is not compressed so offset into the array by the command
210
        // code value of the first entry in the list. Then go find the first
211
        // implemented command.
212
        return NextImplementedIndex(
48✔
213
            searchIndex - (COMMAND_INDEX)GET_ATTRIBUTE(s_ccAttr[0], TPMA_CC, commandIndex));
48✔
214
#endif                                                                        // libtpms added end
215
    }
216
}
217

218
//*** CommandCodeToComandIndex()
219
// This function returns the index in the various attributes arrays of the
220
// command.
221
//  Return Type: COMMAND_INDEX
222
//  UNIMPLEMENTED_COMMAND_INDEX     command is not implemented
223
//  other                           index of the command
224
COMMAND_INDEX
225
CommandCodeToCommandIndex(TPM_CC commandCode  // IN: the command code to look up
22,146✔
226
)
227
{
228
    // Extract the low 16-bits of the command code to get the starting search index
229
    COMMAND_INDEX searchIndex = (COMMAND_INDEX)commandCode;
22,146✔
230
    BOOL          vendor      = (commandCode & CC_VEND) != 0;
22,146✔
231
    COMMAND_INDEX commandIndex;
22,146✔
232
#if !COMPRESSED_LISTS                                                        // libtpms added begin
233
    if(!vendor)
22,146✔
234
    {
235
        commandIndex = searchIndex - (COMMAND_INDEX)GET_ATTRIBUTE(s_ccAttr[0], TPMA_CC, commandIndex);
22,146✔
236
        // Check for out of range or unimplemented.
237
        // Note, since a COMMAND_INDEX is unsigned, if searchIndex is smaller than
238
        // the lowest value of command, it will become a 'negative' number making
239
        // it look like a large unsigned number, this will cause it to fail
240
        // the unsigned check below.
241
        if(commandIndex >= LIBRARY_COMMAND_ARRAY_SIZE
22,146✔
242
           || !RuntimeCommandsCheckEnabled(&g_RuntimeProfile.RuntimeCommands,
22,141✔
243
                                           commandCode))
244
            return UNIMPLEMENTED_COMMAND_INDEX;
7✔
245
        return commandIndex;
246
    }
247
#endif                                                                        // libtpms added end
248
    // Need this code for any vendor code lookup or for compressed lists
UNCOV
249
    commandIndex = GetClosestCommandIndex(commandCode);
×
250

251
    // Look at the returned value from get closest. If it isn't the one that was
252
    // requested, then the command is not implemented.
253
    // libtpms: Or it may be runtime-disabled
UNCOV
254
    if(commandIndex != UNIMPLEMENTED_COMMAND_INDEX)
×
255
    {
UNCOV
256
        if((GET_ATTRIBUTE(s_ccAttr[commandIndex], TPMA_CC, commandIndex)
×
257
            != searchIndex)
UNCOV
258
           || (IS_ATTRIBUTE(s_ccAttr[commandIndex], TPMA_CC, V)) != vendor
×
UNCOV
259
           || !RuntimeCommandsCheckEnabled(&g_RuntimeProfile.RuntimeCommands,        // libtpms added
×
260
                                           commandCode))                        // libtpms added
261
            commandIndex = UNIMPLEMENTED_COMMAND_INDEX;
262
    }
263
    return commandIndex;
264
}
265

266
//*** GetNextCommandIndex()
267
// This function returns the index of the next implemented command.
268
//  Return Type: COMMAND_INDEX
269
//  UNIMPLEMENTED_COMMAND_INDEX     no more implemented commands
270
//  other                           the index of the next implemented command
271
COMMAND_INDEX
272
GetNextCommandIndex(COMMAND_INDEX commandIndex  // IN: the starting index
10,782✔
273
)
274
{
275
    while(++commandIndex < COMMAND_COUNT)
13,179✔
276
    {
277
        if(!RuntimeCommandsCheckEnabled(&g_RuntimeProfile.RuntimeCommands,        // libtpms added begin
13,058✔
278
                                        GET_ATTRIBUTE(s_ccAttr[commandIndex],
13,058✔
279
                                                      TPMA_CC, commandIndex)))
280
            continue;                                                                // libtpms added end
2,397✔
281
        return commandIndex;
282
    }
283
    return UNIMPLEMENTED_COMMAND_INDEX;
284
}
285

286
//*** GetCommandCode()
287
// This function returns the commandCode associated with the command index
288
TPM_CC
289
GetCommandCode(COMMAND_INDEX commandIndex  // IN: the command index
57✔
290
)
291
{
292
    TPM_CC commandCode = GET_ATTRIBUTE(s_ccAttr[commandIndex], TPMA_CC, commandIndex);
57✔
293
    if(IS_ATTRIBUTE(s_ccAttr[commandIndex], TPMA_CC, V))
57✔
UNCOV
294
        commandCode += CC_VEND;
×
295
    return commandCode;
57✔
296
}
297

298
//*** CommandAuthRole()
299
//
300
//  This function returns the authorization role required of a handle.
301
//
302
//  Return Type: AUTH_ROLE
303
//  AUTH_NONE       no authorization is required
304
//  AUTH_USER       user role authorization is required
305
//  AUTH_ADMIN      admin role authorization is required
306
//  AUTH_DUP        duplication role authorization is required
307
AUTH_ROLE
308
CommandAuthRole(COMMAND_INDEX commandIndex,  // IN: command index
17,608✔
309
                UINT32        handleIndex    // IN: handle index (zero based)
310
)
311
{
312
    if(0 == handleIndex)
17,608✔
313
    {
314
        // Any authorization role set?
315
        COMMAND_ATTRIBUTES properties = s_commandAttributes[commandIndex];
14,412✔
316

317
        if(properties & HANDLE_1_USER)
14,412✔
318
            return AUTH_USER;
319
        if(properties & HANDLE_1_ADMIN)
2,627✔
320
            return AUTH_ADMIN;
321
        if(properties & HANDLE_1_DUP)
2,058✔
322
            return AUTH_DUP;
33✔
323
    }
324
    else if(1 == handleIndex)
3,196✔
325
    {
326
        if(s_commandAttributes[commandIndex] & HANDLE_2_USER)
3,090✔
327
            return AUTH_USER;
825✔
328
    }
329
    return AUTH_NONE;
330
}
331

332
//*** EncryptSize()
333
// This function returns the size of the decrypt size field. This function returns
334
// 0 if encryption is not allowed
335
//  Return Type: int
336
//  0       encryption not allowed
337
//  2       size field is two bytes
338
//  4       size field is four bytes
339
int EncryptSize(COMMAND_INDEX commandIndex  // IN: command index
306✔
340
)
341
{
342
    return ((s_commandAttributes[commandIndex] & ENCRYPT_2)   ? 2
306✔
343
            : (s_commandAttributes[commandIndex] & ENCRYPT_4) ? 4
306✔
UNCOV
344
                                                              : 0);
×
345
}
346

347
//*** DecryptSize()
348
// This function returns the size of the decrypt size field. This function returns
349
// 0 if decryption is not allowed
350
//  Return Type: int
351
//  0       encryption not allowed
352
//  2       size field is two bytes
353
//  4       size field is four bytes
354
int DecryptSize(COMMAND_INDEX commandIndex  // IN: command index
322✔
355
)
356
{
357
    return ((s_commandAttributes[commandIndex] & DECRYPT_2)   ? 2
322✔
358
            : (s_commandAttributes[commandIndex] & DECRYPT_4) ? 4
322✔
UNCOV
359
                                                              : 0);
×
360
}
361

362
//*** IsSessionAllowed()
363
//
364
// This function indicates if the command is allowed to have sessions.
365
//
366
// This function must not be called if the command is not known to be implemented.
367
//
368
//  Return Type: BOOL
369
//      TRUE(1)         session is allowed with this command
370
//      FALSE(0)        session is not allowed with this command
371
BOOL IsSessionAllowed(COMMAND_INDEX commandIndex  // IN: the command to be checked
20,862✔
372
)
373
{
374
    return ((s_commandAttributes[commandIndex] & NO_SESSIONS) == 0);
20,862✔
375
}
376

377
//*** IsHandleInResponse()
378
// This function determines if a command has a handle in the response
379
BOOL IsHandleInResponse(COMMAND_INDEX commandIndex)
16,519✔
380
{
381
    return ((s_commandAttributes[commandIndex] & R_HANDLE) != 0);
16,519✔
382
}
383

384
//*** IsDisallowedInReadOnlyMode()
385
// This function determines if a command is disallowed when operating in Read-Only mode
NEW
386
BOOL IsDisallowedInReadOnlyMode(COMMAND_INDEX commandIndex)
×
387
{
NEW
388
    return ((s_commandAttributes[commandIndex] & RO_DISALLOW) != 0);
×
389
}
390

391
//*** IsWriteOperation()
392
// Checks to see if an operation will write to an NV Index and is subject to being
393
// blocked by read-lock
394
BOOL IsWriteOperation(COMMAND_INDEX commandIndex  // IN: Command to check
533✔
395
)
396
{
397
#ifdef WRITE_LOCK
398
    return ((s_commandAttributes[commandIndex] & WRITE_LOCK) != 0);
399
#else
400
    if(!IS_ATTRIBUTE(s_ccAttr[commandIndex], TPMA_CC, V))
533✔
401
    {
402
        switch(GET_ATTRIBUTE(s_ccAttr[commandIndex], TPMA_CC, commandIndex))
533✔
403
        {
404
            case TPM_CC_NV_Write:
243✔
405
#  if CC_NV_Increment
406
            case TPM_CC_NV_Increment:
407
#  endif
408
#  if CC_NV_SetBits
409
            case TPM_CC_NV_SetBits:
410
#  endif
411
#  if CC_NV_Extend
412
            case TPM_CC_NV_Extend:
413
#  endif
414
#  if CC_AC_Send
415
            case TPM_CC_AC_Send:
416
#  endif
417
            // NV write lock counts as a write operation for authorization purposes.
418
            // We check to see if the NV is write locked before we do the
419
            // authorization. If it is locked, we fail the command early.
420
            case TPM_CC_NV_WriteLock:
421
                return TRUE;
243✔
422
            default:
423
                break;
424
        }
425
    }
426
    return FALSE;
427
#endif
428
}
429

430
//*** IsReadOperation()
431
// Checks to see if an operation will write to an NV Index and is
432
// subject to being blocked by write-lock.
UNCOV
433
BOOL IsReadOperation(COMMAND_INDEX commandIndex  // IN: Command to check
×
434
)
435
{
436
#ifdef READ_LOCK
437
    return ((s_commandAttributes[commandIndex] & READ_LOCK) != 0);
438
#else
439

UNCOV
440
    if(!IS_ATTRIBUTE(s_ccAttr[commandIndex], TPMA_CC, V))
×
441
    {
UNCOV
442
        switch(GET_ATTRIBUTE(s_ccAttr[commandIndex], TPMA_CC, commandIndex))
×
443
        {
UNCOV
444
            case TPM_CC_NV_Read:
×
445
            case TPM_CC_PolicyNV:
446
            case TPM_CC_NV_Certify:
447
            // NV read lock counts as a read operation for authorization purposes.
448
            // We check to see if the NV is read locked before we do the
449
            // authorization. If it is locked, we fail the command early.
450
            case TPM_CC_NV_ReadLock:
UNCOV
451
                return TRUE;
×
452
            default:
453
                break;
454
        }
455
    }
456
    return FALSE;
457
#endif
458
}
459

460
//*** CommandCapGetCCList()
461
// This function returns a list of implemented commands and command attributes
462
// starting from the command in 'commandCode'.
463
//  Return Type: TPMI_YES_NO
464
//      YES         more command attributes are available
465
//      NO          no more command attributes are available
466
TPMI_YES_NO
467
CommandCapGetCCList(TPM_CC commandCode,  // IN: start command code
46✔
468
                    UINT32 count,        // IN: maximum count for number of entries in
469
                                         //     'commandList'
470
                    TPML_CCA* commandList  // OUT: list of TPMA_CC
471
)
472
{
473
    TPMI_YES_NO   more = NO;
46✔
474
    COMMAND_INDEX commandIndex;
46✔
475

476
    // initialize output handle list count
477
    commandList->count = 0;
46✔
478

479
    for(commandIndex = GetClosestCommandIndex(commandCode);
46✔
480
        commandIndex != UNIMPLEMENTED_COMMAND_INDEX;
1,296✔
481
        commandIndex = GetNextCommandIndex(commandIndex))
1,250✔
482
    {
483
        if (!RuntimeCommandsCheckEnabled(&g_RuntimeProfile.RuntimeCommands,                // libtpms added begin
1,283✔
484
                                         GET_ATTRIBUTE(s_ccAttr[commandIndex],
1,283✔
485
                                                       TPMA_CC, commandIndex)))
NEW
486
             continue;                                                                        // libtpms added end
×
487
        if(commandList->count < count)
1,283✔
488
        {
489
            // If the list is not full, add the attributes for this command.
490
            commandList->commandAttributes[commandList->count] =
1,250✔
491
                s_ccAttr[commandIndex];
492
            commandList->count++;
1,250✔
493
        }
494
        else
495
        {
496
            // If the list is full but there are more commands to report,
497
            // indicate this and return.
498
            more = YES;
499
            break;
500
        }
501
    }
502
    return more;
46✔
503
}
504

505
//*** CommandCapGetOneCC()
506
// This function checks whether a command is implemented, and returns its
507
// attributes if so.
UNCOV
508
BOOL CommandCapGetOneCC(TPM_CC   commandCode,       // IN: command code
×
509
                        TPMA_CC* commandAttributes  // OUT: command attributes
510
)
511
{
UNCOV
512
    COMMAND_INDEX commandIndex = CommandCodeToCommandIndex(commandCode);
×
UNCOV
513
    if(commandIndex != UNIMPLEMENTED_COMMAND_INDEX)
×
514
    {
UNCOV
515
        *commandAttributes = s_ccAttr[commandIndex];
×
UNCOV
516
        return TRUE;
×
517
    }
518
    return FALSE;
519
}
520
#if 0 /* libtpms added */
521

522
//*** IsVendorCommand()
523
// Function indicates if a command index references a vendor command.
524
//  Return Type: BOOL
525
//      TRUE(1)         command is a vendor command
526
//      FALSE(0)        command is not a vendor command
527
BOOL IsVendorCommand(COMMAND_INDEX commandIndex  // IN: command index to check
528
)
529
{
530
    return (IS_ATTRIBUTE(s_ccAttr[commandIndex], TPMA_CC, V));
531
}
532
#endif /* libtpms added */
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc