/* * This file and its contents are supplied under the terms of the * Common Development and Distribution License ("CDDL"), version 1.0. * You may only use this file in accordance with the terms of version * 1.0 of the CDDL. * * A full copy of the text of the CDDL should have accompanied this * source. A copy of the CDDL is also available via the Internet at * http://www.illumos.org/license/CDDL. */ /* * Copyright 2019, Joyent, Inc. * Copyright 2024 Oxide Computer Company */ /* * Print out information about a CCID device. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define EXIT_USAGE 2 static const char *ccidadm_pname; #define CCID_ROOT "/dev/ccid/" typedef enum { CCIDADM_LIST_DEVICE, CCIDADM_LIST_PRODUCT, CCIDADM_LIST_STATE, CCIDADM_LIST_TRANSPORT, CCIDADM_LIST_SUPPORTED, } ccidadm_list_index_t; typedef struct ccidadm_pair { uint32_t ccp_val; const char *ccp_name; } ccidadm_pair_t; typedef struct ccid_list_ofmt_arg { const char *cloa_name; uccid_cmd_status_t *cloa_status; } ccid_list_ofmt_arg_t; /* * Attempt to open a CCID slot specified by a user. In general, we expect that * users will use a path like "ccid0/slot0". However, they may also specify a * full path. If the card boolean is set to true, that means that they may have * just specified "ccid0", so we need to try to open up the default slot. */ static int ccidadm_open(const char *base, boolean_t card) { int fd; char buf[PATH_MAX]; /* * If it's an absolute path, just try to open it. */ if (base[0] == '/') { return (open(base, O_RDWR)); } /* * For a card, try to append slot0 first. */ if (card) { if (snprintf(buf, sizeof (buf), "%s/%s/slot0", CCID_ROOT, base) >= sizeof (buf)) { errno = ENAMETOOLONG; return (-1); } if ((fd = open(buf, O_RDWR)) >= 0) { return (fd); } if (errno != ENOENT && errno != ENOTDIR) { return (fd); } } if (snprintf(buf, sizeof (buf), "%s/%s", CCID_ROOT, base) >= sizeof (buf)) { errno = ENAMETOOLONG; return (-1); } return (open(buf, O_RDWR)); } static void ccidadm_iter(boolean_t readeronly, boolean_t newline, void(*cb)(int, const char *, void *), void *arg) { FTS *fts; FTSENT *ent; char *const paths[] = { CCID_ROOT, NULL }; int fd; boolean_t first = B_TRUE; fts = fts_open(paths, FTS_LOGICAL | FTS_NOCHDIR, NULL); if (fts == NULL) { err(EXIT_FAILURE, "failed to create directory stream"); } while ((ent = fts_read(fts)) != NULL) { const char *name; /* Skip the root and post-order dirs */ if (ent->fts_level == 0 || ent->fts_info == FTS_DP) { continue; } if (readeronly && ent->fts_level != 1) { continue; } else if (!readeronly && ent->fts_level != 2) { continue; } if (ent->fts_info == FTS_ERR || ent->fts_info == FTS_NS) { warn("skipping %s, failed to get information: %s", ent->fts_name, strerror(ent->fts_errno)); continue; } name = ent->fts_path + strlen(CCID_ROOT); if ((fd = ccidadm_open(name, readeronly)) < 0) { err(EXIT_FAILURE, "failed to open %s", name); } if (!first && newline) { (void) printf("\n"); } first = B_FALSE; cb(fd, name, arg); (void) close(fd); } (void) fts_close(fts); } static void ccidadm_list_slot_status_str(uccid_cmd_status_t *ucs, ilstr_t *s) { if (!(ucs->ucs_status & UCCID_STATUS_F_CARD_PRESENT)) { ilstr_append_str(s, "missing"); return; } if (!(ucs->ucs_status & UCCID_STATUS_F_CARD_ACTIVE)) { ilstr_append_str(s, "un"); } ilstr_append_str(s, "activated"); } static void ccidadm_list_slot_transport_str(uccid_cmd_status_t *ucs, ilstr_t *s) { uint_t bits = CCID_CLASS_F_TPDU_XCHG | CCID_CLASS_F_SHORT_APDU_XCHG | CCID_CLASS_F_EXT_APDU_XCHG; switch (ucs->ucs_class.ccd_dwFeatures & bits) { case 0: ilstr_append_str(s, "character"); break; case CCID_CLASS_F_TPDU_XCHG: ilstr_append_str(s, "TPDU"); break; case CCID_CLASS_F_SHORT_APDU_XCHG: case CCID_CLASS_F_EXT_APDU_XCHG: ilstr_append_str(s, "APDU"); break; default: ilstr_append_str(s, "unknown"); break; } if ((ucs->ucs_status & UCCID_STATUS_F_PARAMS_VALID) != 0) { switch (ucs->ucs_prot) { case UCCID_PROT_T0: ilstr_append_str(s, " (T=0)"); break; case UCCID_PROT_T1: ilstr_append_str(s, " (T=1)"); break; default: break; } } } static void ccidadm_list_slot_usable_str(uccid_cmd_status_t *ucs, ilstr_t *s) { ccid_class_features_t feat; uint_t prot = CCID_CLASS_F_SHORT_APDU_XCHG | CCID_CLASS_F_EXT_APDU_XCHG; uint_t param = CCID_CLASS_F_AUTO_PARAM_NEG | CCID_CLASS_F_AUTO_PPS; uint_t clock = CCID_CLASS_F_AUTO_BAUD | CCID_CLASS_F_AUTO_ICC_CLOCK; feat = ucs->ucs_class.ccd_dwFeatures; if ((feat & prot) == 0 || (feat & param) != param || (feat & clock) != clock) { ilstr_append_str(s, "un"); } ilstr_append_str(s, "supported"); } static boolean_t ccidadm_list_ofmt_cb(ofmt_arg_t *ofmt, char *buf, uint_t buflen) { ccid_list_ofmt_arg_t *cloa = ofmt->ofmt_cbarg; ilstr_t s; ilstr_init_prealloc(&s, buf, buflen); switch (ofmt->ofmt_id) { case CCIDADM_LIST_DEVICE: ilstr_append_str(&s, cloa->cloa_name); break; case CCIDADM_LIST_PRODUCT: ilstr_append_str(&s, cloa->cloa_status->ucs_product); break; case CCIDADM_LIST_STATE: ccidadm_list_slot_status_str(cloa->cloa_status, &s); break; case CCIDADM_LIST_TRANSPORT: ccidadm_list_slot_transport_str(cloa->cloa_status, &s); break; case CCIDADM_LIST_SUPPORTED: ccidadm_list_slot_usable_str(cloa->cloa_status, &s); break; default: return (B_FALSE); } return (ilstr_errno(&s) == ILSTR_ERROR_OK); } static void ccidadm_list_slot(int slotfd, const char *name, void *arg) { uccid_cmd_status_t ucs; ofmt_handle_t ofmt = arg; ccid_list_ofmt_arg_t cloa; bzero(&ucs, sizeof (ucs)); ucs.ucs_version = UCCID_CURRENT_VERSION; if (ioctl(slotfd, UCCID_CMD_STATUS, &ucs) != 0) { err(EXIT_FAILURE, "failed to issue status ioctl to %s", name); } if ((ucs.ucs_status & UCCID_STATUS_F_PRODUCT_VALID) == 0) { (void) strlcpy(ucs.ucs_product, "", sizeof (ucs.ucs_product)); } cloa.cloa_name = name; cloa.cloa_status = &ucs; ofmt_print(ofmt, &cloa); } static ofmt_field_t ccidadm_list_fields[] = { { "PRODUCT", 24, CCIDADM_LIST_PRODUCT, ccidadm_list_ofmt_cb }, { "DEVICE", 16, CCIDADM_LIST_DEVICE, ccidadm_list_ofmt_cb }, { "CARD STATE", 12, CCIDADM_LIST_STATE, ccidadm_list_ofmt_cb }, { "TRANSPORT", 12, CCIDADM_LIST_TRANSPORT, ccidadm_list_ofmt_cb }, { "SUPPORTED", 12, CCIDADM_LIST_SUPPORTED, ccidadm_list_ofmt_cb }, { NULL, 0, 0, NULL } }; static void ccidadm_do_list(int argc, char *argv[]) { ofmt_handle_t ofmt; if (argc != 0) { errx(EXIT_USAGE, "list command does not take arguments\n"); } if (ofmt_open(NULL, ccidadm_list_fields, 0, 0, &ofmt) != OFMT_SUCCESS) { errx(EXIT_FAILURE, "failed to initialize ofmt state"); } ccidadm_iter(B_FALSE, B_FALSE, ccidadm_list_slot, ofmt); ofmt_close(ofmt); } static void ccidadm_list_usage(FILE *out) { (void) fprintf(out, "\tlist\n"); } /* * Print out logical information about the ICC's ATR. This includes information * about what protocols it supports, required negotiation, etc. */ static void ccidadm_atr_props(uccid_cmd_status_t *ucs) { int ret; atr_data_t *data; atr_protocol_t prots, defprot; boolean_t negotiate; atr_data_rate_choice_t rate; uint32_t bps; if ((data = atr_data_alloc()) == NULL) { err(EXIT_FAILURE, "failed to allocate memory for " "ATR data"); } ret = atr_parse(ucs->ucs_atr, ucs->ucs_atrlen, data); if (ret != ATR_CODE_OK) { errx(EXIT_FAILURE, "failed to parse ATR data: %s", atr_strerror(ret)); } prots = atr_supported_protocols(data); (void) printf("ICC supports protocol(s): "); if (prots == ATR_P_NONE) { (void) printf("none\n"); atr_data_free(data); return; } (void) printf("%s\n", atr_protocol_to_string(prots)); negotiate = atr_params_negotiable(data); defprot = atr_default_protocol(data); if (negotiate) { (void) printf("Card protocol is negotiable; starts with " "default %s parameters\n", atr_protocol_to_string(defprot)); } else { (void) printf("Card protocol is not negotiable; starts with " "specific %s parameters\n", atr_protocol_to_string(defprot)); } /* * For each supported protocol, figure out parameters we would * negotiate. We only need to warn about auto-negotiation if this * is TPDU or character and specific bits are missing. */ if (((ucs->ucs_class.ccd_dwFeatures & (CCID_CLASS_F_SHORT_APDU_XCHG | CCID_CLASS_F_EXT_APDU_XCHG)) == 0) && ((ucs->ucs_class.ccd_dwFeatures & (CCID_CLASS_F_AUTO_PARAM_NEG | CCID_CLASS_F_AUTO_PPS)) == 0)) { (void) printf("CCID/ICC require explicit TPDU parameter/PPS " "negotiation\n"); } /* * Determine which set of Di/Fi values we should use and how we should * get there (note a reader may not have to set them). */ rate = atr_data_rate(data, &ucs->ucs_class, NULL, 0, &bps); switch (rate) { case ATR_RATE_USEDEFAULT: (void) printf("Reader will run ICC at the default (Di=1/Fi=1) " "speed\n"); break; case ATR_RATE_USEATR: (void) printf("Reader will run ICC at ICC's Di/Fi values\n"); break; case ATR_RATE_USEATR_SETRATE: (void) printf("Reader will run ICC at ICC's Di/Fi values, but " "must set data rate to %u bps\n", bps); break; case ATR_RATE_UNSUPPORTED: (void) printf("Reader cannot run ICC due to Di/Fi mismatch\n"); break; default: (void) printf("Cannot determine Di/Fi rate, unexpected " "value: %u\n", rate); break; } if (prots & ATR_P_T0) { uint8_t fi, di; atr_convention_t conv; atr_clock_stop_t clock; fi = atr_fi_index(data); di = atr_di_index(data); conv = atr_convention(data); clock = atr_clock_stop(data); (void) printf("T=0 properties that would be negotiated:\n"); (void) printf(" + Fi/Fmax Index: %u (Fi %s/Fmax %s MHz)\n", fi, atr_fi_index_to_string(fi), atr_fmax_index_to_string(fi)); (void) printf(" + Di Index: %u (Di %s)\n", di, atr_di_index_to_string(di)); (void) printf(" + Clock Convention: %u (%s)\n", conv, atr_convention_to_string(conv)); (void) printf(" + Extra Guardtime: %u\n", atr_extra_guardtime(data)); (void) printf(" + WI: %u\n", atr_t0_wi(data)); (void) printf(" + Clock Stop: %u (%s)\n", clock, atr_clock_stop_to_string(clock)); } if (prots & ATR_P_T1) { uint8_t fi, di; atr_clock_stop_t clock; atr_t1_checksum_t cksum; fi = atr_fi_index(data); di = atr_di_index(data); clock = atr_clock_stop(data); cksum = atr_t1_checksum(data); (void) printf("T=1 properties that would be negotiated:\n"); (void) printf(" + Fi/Fmax Index: %u (Fi %s/Fmax %s MHz)\n", fi, atr_fi_index_to_string(fi), atr_fmax_index_to_string(fi)); (void) printf(" + Di Index: %u (Di %s)\n", di, atr_di_index_to_string(di)); (void) printf(" + Checksum: %s\n", cksum == ATR_T1_CHECKSUM_CRC ? "CRC" : "LRC"); (void) printf(" + Extra Guardtime: %u\n", atr_extra_guardtime(data)); (void) printf(" + BWI: %u\n", atr_t1_bwi(data)); (void) printf(" + CWI: %u\n", atr_t1_cwi(data)); (void) printf(" + Clock Stop: %u (%s)\n", clock, atr_clock_stop_to_string(clock)); (void) printf(" + IFSC: %u\n", atr_t1_ifsc(data)); (void) printf(" + CCID Supports NAD: %s\n", ucs->ucs_class.ccd_dwFeatures & CCID_CLASS_F_ALTNAD_SUP ? "yes" : "no"); } atr_data_free(data); } static void ccidadm_atr_verbose(uccid_cmd_status_t *ucs) { int ret; atr_data_t *data; if ((data = atr_data_alloc()) == NULL) { err(EXIT_FAILURE, "failed to allocate memory for " "ATR data"); } ret = atr_parse(ucs->ucs_atr, ucs->ucs_atrlen, data); if (ret != ATR_CODE_OK) { errx(EXIT_FAILURE, "failed to parse ATR data: %s", atr_strerror(ret)); } atr_data_dump(data, stdout); atr_data_free(data); } typedef struct cciadm_atr_args { boolean_t caa_hex; boolean_t caa_props; boolean_t caa_verbose; } ccidadm_atr_args_t; static void ccidadm_atr_fetch(int fd, const char *name, void *arg) { uccid_cmd_status_t ucs; ccidadm_atr_args_t *caa = arg; bzero(&ucs, sizeof (ucs)); ucs.ucs_version = UCCID_CURRENT_VERSION; if (ioctl(fd, UCCID_CMD_STATUS, &ucs) != 0) { err(EXIT_FAILURE, "failed to issue status ioctl to %s", name); } if (ucs.ucs_atrlen == 0) { warnx("slot %s has no card inserted or activated", name); return; } (void) printf("ATR for %s (%u bytes):\n", name, ucs.ucs_atrlen); if (caa->caa_props) { ccidadm_atr_props(&ucs); } if (caa->caa_hex) { atr_data_hexdump(ucs.ucs_atr, ucs.ucs_atrlen, stdout); } if (caa->caa_verbose) { ccidadm_atr_verbose(&ucs); } } static void ccidadm_do_atr(int argc, char *argv[]) { uint_t i; int c; ccidadm_atr_args_t caa; bzero(&caa, sizeof (caa)); optind = 0; while ((c = getopt(argc, argv, "vx")) != -1) { switch (c) { case 'v': caa.caa_verbose = B_TRUE; break; case 'x': caa.caa_hex = B_TRUE; break; case ':': errx(EXIT_USAGE, "Option -%c requires an argument\n", optopt); break; case '?': errx(EXIT_USAGE, "Unknown option: -%c\n", optopt); break; } } if (!caa.caa_verbose && !caa.caa_props && !caa.caa_hex) { caa.caa_props = B_TRUE; } argc -= optind; argv += optind; if (argc == 0) { ccidadm_iter(B_FALSE, B_TRUE, ccidadm_atr_fetch, &caa); return; } for (i = 0; i < argc; i++) { int fd; if ((fd = ccidadm_open(argv[i], B_FALSE)) < 0) { warn("failed to open %s", argv[i]); errx(EXIT_FAILURE, "valid CCID slot?"); } ccidadm_atr_fetch(fd, argv[i], &caa); (void) close(fd); if (i + 1 < argc) { (void) printf("\n"); } } } static void ccidadm_atr_usage(FILE *out) { (void) fprintf(out, "\tatr [-vx]\t[device] ...\n"); } static void ccidadm_print_pairs(uint32_t val, ccidadm_pair_t *ccp) { while (ccp->ccp_name != NULL) { if ((val & ccp->ccp_val) == ccp->ccp_val) { (void) printf(" + %s\n", ccp->ccp_name); } ccp++; } } static ccidadm_pair_t ccidadm_p_protocols[] = { { 0x01, "T=0" }, { 0x02, "T=1" }, { 0x0, NULL } }; static ccidadm_pair_t ccidadm_p_voltages[] = { { CCID_CLASS_VOLT_5_0, "5.0 V" }, { CCID_CLASS_VOLT_3_0, "3.0 V" }, { CCID_CLASS_VOLT_1_8, "1.8 V" }, { 0x0, NULL } }; static ccidadm_pair_t ccidadm_p_syncprots[] = { { 0x01, "2-Wire Support" }, { 0x02, "3-Wire Support" }, { 0x04, "I2C Support" }, { 0x0, NULL } }; static ccidadm_pair_t ccidadm_p_mechanical[] = { { CCID_CLASS_MECH_CARD_ACCEPT, "Card Accept Mechanism" }, { CCID_CLASS_MECH_CARD_EJECT, "Card Eject Mechanism" }, { CCID_CLASS_MECH_CARD_CAPTURE, "Card Capture Mechanism" }, { CCID_CLASS_MECH_CARD_LOCK, "Card Lock/Unlock Mechanism" }, { 0x0, NULL } }; static ccidadm_pair_t ccidadm_p_features[] = { { CCID_CLASS_F_AUTO_PARAM_ATR, "Automatic parameter configuration based on ATR data" }, { CCID_CLASS_F_AUTO_ICC_ACTIVATE, "Automatic activation on ICC insertion" }, { CCID_CLASS_F_AUTO_ICC_VOLTAGE, "Automatic ICC voltage selection" }, { CCID_CLASS_F_AUTO_ICC_CLOCK, "Automatic ICC clock frequency change" }, { CCID_CLASS_F_AUTO_BAUD, "Automatic baud rate change" }, { CCID_CLASS_F_AUTO_PARAM_NEG, "Automatic parameter negotiation by CCID" }, { CCID_CLASS_F_AUTO_PPS, "Automatic PPS made by CCID" }, { CCID_CLASS_F_ICC_CLOCK_STOP, "CCID can set ICC in clock stop mode" }, { CCID_CLASS_F_ALTNAD_SUP, "NAD value other than zero accepted" }, { CCID_CLASS_F_AUTO_IFSD, "Automatic IFSD exchange" }, { CCID_CLASS_F_TPDU_XCHG, "TPDU support" }, { CCID_CLASS_F_SHORT_APDU_XCHG, "Short APDU support" }, { CCID_CLASS_F_EXT_APDU_XCHG, "Short and Extended APDU support" }, { CCID_CLASS_F_WAKE_UP, "USB Wake Up signaling support" }, { 0x0, NULL } }; static ccidadm_pair_t ccidadm_p_pin[] = { { CCID_CLASS_PIN_VERIFICATION, "PIN verification" }, { CCID_CLASS_PIN_MODIFICATION, "PIN modification" }, { 0x0, NULL } }; static void ccidadm_reader_print(int fd, const char *name, void *unused __unused) { uccid_cmd_status_t ucs; ccid_class_descr_t *cd; char nnbuf[NN_NUMBUF_SZ + 1]; bzero(&ucs, sizeof (uccid_cmd_status_t)); ucs.ucs_version = UCCID_CURRENT_VERSION; if (ioctl(fd, UCCID_CMD_STATUS, &ucs) != 0) { err(EXIT_FAILURE, "failed to issue status ioctl to %s", name); } cd = &ucs.ucs_class; (void) printf("Reader %s, CCID class v%u.%u device:\n", name, CCID_VERSION_MAJOR(cd->ccd_bcdCCID), CCID_VERSION_MINOR(cd->ccd_bcdCCID)); if ((ucs.ucs_status & UCCID_STATUS_F_PRODUCT_VALID) == 0) { (void) strlcpy(ucs.ucs_product, "", sizeof (ucs.ucs_product)); } if ((ucs.ucs_status & UCCID_STATUS_F_SERIAL_VALID) == 0) { (void) strlcpy(ucs.ucs_serial, "", sizeof (ucs.ucs_serial)); } (void) printf(" Product: %s\n", ucs.ucs_product); (void) printf(" Serial: %s\n", ucs.ucs_serial); (void) printf(" Slots Present: %u\n", cd->ccd_bMaxSlotIndex + 1); (void) printf(" Maximum Busy Slots: %u\n", cd->ccd_bMaxCCIDBusySlots); (void) printf(" Supported Voltages:\n"); ccidadm_print_pairs(cd->ccd_bVoltageSupport, ccidadm_p_voltages); (void) printf(" Supported Protocols:\n"); ccidadm_print_pairs(cd->ccd_dwProtocols, ccidadm_p_protocols); nicenum_scale(cd->ccd_dwDefaultClock, 1000, nnbuf, sizeof (nnbuf), NN_DIVISOR_1000 | NN_UNIT_SPACE); (void) printf(" Default Clock: %sHz\n", nnbuf); nicenum_scale(cd->ccd_dwMaximumClock, 1000, nnbuf, sizeof (nnbuf), NN_DIVISOR_1000 | NN_UNIT_SPACE); (void) printf(" Maximum Clock: %sHz\n", nnbuf); (void) printf(" Supported Clock Rates: %u\n", cd->ccd_bNumClockSupported); nicenum_scale(cd->ccd_dwDataRate, 1, nnbuf, sizeof (nnbuf), NN_DIVISOR_1000 | NN_UNIT_SPACE); (void) printf(" Default Data Rate: %sbps\n", nnbuf); nicenum_scale(cd->ccd_dwMaxDataRate, 1, nnbuf, sizeof (nnbuf), NN_DIVISOR_1000 | NN_UNIT_SPACE); (void) printf(" Maximum Data Rate: %sbps\n", nnbuf); (void) printf(" Supported Data Rates: %u\n", cd->ccd_bNumDataRatesSupported); (void) printf(" Maximum IFSD (T=1 only): %u\n", cd->ccd_dwMaxIFSD); if (cd->ccd_dwSyncProtocols != 0) { (void) printf(" Synchronous Protocols Supported:\n"); ccidadm_print_pairs(cd->ccd_dwSyncProtocols, ccidadm_p_syncprots); } if (cd->ccd_dwMechanical != 0) { (void) printf(" Mechanical Features:\n"); ccidadm_print_pairs(cd->ccd_dwMechanical, ccidadm_p_mechanical); } if (cd->ccd_dwFeatures != 0) { (void) printf(" Device Features:\n"); ccidadm_print_pairs(cd->ccd_dwFeatures, ccidadm_p_features); } (void) printf(" Maximum Message Length: %u bytes\n", cd->ccd_dwMaxCCIDMessageLength); if (cd->ccd_dwFeatures & CCID_CLASS_F_EXT_APDU_XCHG) { if (cd->ccd_bClassGetResponse == 0xff) { (void) printf(" Default Get Response Class: echo\n"); } else { (void) printf(" Default Get Response Class: %u\n", cd->ccd_bClassGetResponse); } if (cd->ccd_bClassEnvelope == 0xff) { (void) printf(" Default Envelope Class: echo\n"); } else { (void) printf(" Default Envelope Class: %u\n", cd->ccd_bClassEnvelope); } } if (cd->ccd_wLcdLayout != 0) { (void) printf(" %2ux%2u LCD present\n", cd->ccd_wLcdLayout >> 8, cd->ccd_wLcdLayout & 0xff); } if (cd->ccd_bPinSupport) { (void) printf(" Pin Support:\n"); ccidadm_print_pairs(cd->ccd_bPinSupport, ccidadm_p_pin); } } static void ccidadm_do_reader(int argc, char *argv[]) { int i; if (argc == 0) { ccidadm_iter(B_TRUE, B_TRUE, ccidadm_reader_print, NULL); return; } for (i = 0; i < argc; i++) { int fd; if ((fd = ccidadm_open(argv[i], B_TRUE)) < 0) { warn("failed to open %s", argv[i]); errx(EXIT_FAILURE, "valid ccid reader"); } ccidadm_reader_print(fd, argv[i], NULL); (void) close(fd); if (i + 1 < argc) { (void) printf("\n"); } } } static void ccidadm_reader_usage(FILE *out) { (void) fprintf(out, "\treader\t\t[reader] ...\n"); } typedef struct ccidadm_cmdtab { const char *cc_name; void (*cc_op)(int, char *[]); void (*cc_usage)(FILE *); } ccidadm_cmdtab_t; static ccidadm_cmdtab_t ccidadm_cmds[] = { { "list", ccidadm_do_list, ccidadm_list_usage }, { "atr", ccidadm_do_atr, ccidadm_atr_usage }, { "reader", ccidadm_do_reader, ccidadm_reader_usage }, { NULL } }; static int ccidadm_usage(const char *format, ...) { ccidadm_cmdtab_t *tab; if (format != NULL) { va_list ap; va_start(ap, format); (void) fprintf(stderr, "%s: ", ccidadm_pname); (void) vfprintf(stderr, format, ap); (void) fprintf(stderr, "\n"); va_end(ap); } (void) fprintf(stderr, "usage: %s ...\n\n", ccidadm_pname); (void) fprintf(stderr, "Subcommands:\n"); for (tab = ccidadm_cmds; tab->cc_name != NULL; tab++) { tab->cc_usage(stderr); } return (EXIT_USAGE); } int main(int argc, char *argv[]) { ccidadm_cmdtab_t *tab; ccidadm_pname = basename(argv[0]); if (argc < 2) { return (ccidadm_usage("missing required subcommand")); } for (tab = ccidadm_cmds; tab->cc_name != NULL; tab++) { if (strcmp(argv[1], tab->cc_name) == 0) { argc -= 2; argv += 2; tab->cc_op(argc, argv); return (EXIT_SUCCESS); } } return (ccidadm_usage("unknown command: %s", argv[1])); }