// SPDX-License-Identifier: GPL-2.0 // // xfer-options.c - a parser of commandline options for xfer. // // Copyright (c) 2018 Takashi Sakamoto // // Licensed under the terms of the GNU General Public License, version 2. #include "xfer.h" #include "misc.h" #include #include #include enum no_short_opts { // 128 or later belong to non us-ascii character set. OPT_XFER_TYPE = 128, OPT_DUMP_HW_PARAMS, }; static int allocate_paths(struct xfer_context *xfer, char *const *paths, unsigned int count) { bool stdio = false; int i; if (count == 0) { stdio = true; count = 1; } xfer->paths = calloc(count, sizeof(xfer->paths[0])); if (xfer->paths == NULL) return -ENOMEM; xfer->path_count = count; if (stdio) { xfer->paths[0] = strndup("-", PATH_MAX); if (xfer->paths[0] == NULL) return -ENOMEM; } else { for (i = 0; i < count; ++i) { xfer->paths[i] = strndup(paths[i], PATH_MAX); if (xfer->paths[i] == NULL) return -ENOMEM; } } return 0; } static int verify_cntr_format(struct xfer_context *xfer) { static const struct { const char *const literal; enum container_format cntr_format; } *entry, entries[] = { {"raw", CONTAINER_FORMAT_RAW}, {"voc", CONTAINER_FORMAT_VOC}, {"wav", CONTAINER_FORMAT_RIFF_WAVE}, {"au", CONTAINER_FORMAT_AU}, {"sparc", CONTAINER_FORMAT_AU}, }; int i; for (i = 0; i < ARRAY_SIZE(entries); ++i) { entry = &entries[i]; if (strcasecmp(xfer->cntr_format_literal, entry->literal)) continue; xfer->cntr_format = entry->cntr_format; return 0; } fprintf(stderr, "unrecognized file format '%s'\n", xfer->cntr_format_literal); return -EINVAL; } // This should be called after 'verify_cntr_format()'. static int verify_sample_format(struct xfer_context *xfer) { static const struct { const char *const literal; unsigned int frames_per_second; unsigned int samples_per_frame; snd_pcm_format_t le_format; snd_pcm_format_t be_format; } *entry, entries[] = { {"cd", 44100, 2, SND_PCM_FORMAT_S16_LE, SND_PCM_FORMAT_S16_BE}, {"cdr", 44100, 2, SND_PCM_FORMAT_S16_LE, SND_PCM_FORMAT_S16_BE}, {"dat", 48000, 2, SND_PCM_FORMAT_S16_LE, SND_PCM_FORMAT_S16_BE}, }; int i; xfer->sample_format = snd_pcm_format_value(xfer->sample_format_literal); if (xfer->sample_format != SND_PCM_FORMAT_UNKNOWN) return 0; for (i = 0; i < ARRAY_SIZE(entries); ++i) { entry = &entries[i]; if (strcmp(entry->literal, xfer->sample_format_literal)) continue; if (xfer->frames_per_second > 0 && xfer->frames_per_second != entry->frames_per_second) { fprintf(stderr, "'%s' format can't be used with rate except " "for %u.\n", entry->literal, entry->frames_per_second); return -EINVAL; } if (xfer->samples_per_frame > 0 && xfer->samples_per_frame != entry->samples_per_frame) { fprintf(stderr, "'%s' format can't be used with channel except " "for %u.\n", entry->literal, entry->samples_per_frame); return -EINVAL; } xfer->frames_per_second = entry->frames_per_second; xfer->samples_per_frame = entry->samples_per_frame; if (xfer->cntr_format == CONTAINER_FORMAT_AU) xfer->sample_format = entry->be_format; else xfer->sample_format = entry->le_format; return 0; } fprintf(stderr, "wrong extended format '%s'\n", xfer->sample_format_literal); return -EINVAL; } static int validate_options(struct xfer_context *xfer) { unsigned int val; int err = 0; if (xfer->cntr_format_literal == NULL) { if (xfer->direction == SND_PCM_STREAM_CAPTURE) { // To stdout. if (xfer->path_count == 1 && !strcmp(xfer->paths[0], "-")) { xfer->cntr_format = CONTAINER_FORMAT_RAW; } else { // Use first path as a representative. xfer->cntr_format = container_format_from_path( xfer->paths[0]); } } // For playback, perform auto-detection. } else { err = verify_cntr_format(xfer); } if (err < 0) return err; if (xfer->multiple_cntrs) { if (!strcmp(xfer->paths[0], "-")) { fprintf(stderr, "An option for separated channels is not " "available with stdin/stdout.\n"); return -EINVAL; } // For captured PCM frames, even if one path is given for // container files, it can be used to generate several paths. // For this purpose, please see // 'xfer_options_fixup_paths()'. if (xfer->direction == SND_PCM_STREAM_PLAYBACK) { // Require several paths for containers. if (xfer->path_count == 1) { fprintf(stderr, "An option for separated channels " "requires several files to playback " "PCM frames.\n"); return -EINVAL; } } } else { // A single path is available only. if (xfer->path_count > 1) { fprintf(stderr, "When using several files, an option for " "sepatated channels is used with.\n"); return -EINVAL; } } xfer->sample_format = SND_PCM_FORMAT_UNKNOWN; if (xfer->sample_format_literal) { err = verify_sample_format(xfer); if (err < 0) return err; } val = xfer->frames_per_second; if (xfer->frames_per_second == 0) xfer->frames_per_second = 8000; if (xfer->frames_per_second < 1000) xfer->frames_per_second *= 1000; if (xfer->frames_per_second < 2000 || xfer->frames_per_second > 192000) { fprintf(stderr, "bad speed value '%i'\n", val); return -EINVAL; } if (xfer->samples_per_frame > 0) { if (xfer->samples_per_frame < 1 || xfer->samples_per_frame > 256) { fprintf(stderr, "invalid channels argument '%u'\n", xfer->samples_per_frame); return -EINVAL; } } return err; } int xfer_options_parse_args(struct xfer_context *xfer, const struct xfer_data *data, int argc, char *const *argv) { static const char *short_opts = "CPhvqf:c:r:t:I"; static const struct option long_opts[] = { // For generic purposes. {"capture", 0, 0, 'C'}, {"playback", 0, 0, 'P'}, {"xfer-type", 1, 0, OPT_XFER_TYPE}, {"help", 0, 0, 'h'}, {"verbose", 0, 0, 'v'}, {"quiet", 0, 0, 'q'}, // For transfer backend. {"format", 1, 0, 'f'}, {"channels", 1, 0, 'c'}, {"rate", 1, 0, 'r'}, // For containers. {"file-type", 1, 0, 't'}, // For mapper. {"separate-channels", 0, 0, 'I'}, // For debugging. {"dump-hw-params", 0, 0, OPT_DUMP_HW_PARAMS}, }; char *s_opts; struct option *l_opts; int l_index; int key; int err = 0; // Concatenate short options. s_opts = malloc(strlen(data->s_opts) + strlen(short_opts) + 1); if (s_opts == NULL) return -ENOMEM; strcpy(s_opts, data->s_opts); strcpy(s_opts + strlen(s_opts), short_opts); s_opts[strlen(data->s_opts) + strlen(short_opts)] = '\0'; // Concatenate long options, including a sentinel. l_opts = calloc(ARRAY_SIZE(long_opts) * data->l_opts_count + 1, sizeof(*l_opts)); if (l_opts == NULL) { free(s_opts); return -ENOMEM; } memcpy(l_opts, long_opts, ARRAY_SIZE(long_opts) * sizeof(*l_opts)); memcpy(&l_opts[ARRAY_SIZE(long_opts)], data->l_opts, data->l_opts_count * sizeof(*l_opts)); // Parse options. l_index = 0; optarg = NULL; optind = 1; opterr = 1; // use error output. optopt = 0; while (1) { key = getopt_long(argc, argv, s_opts, l_opts, &l_index); if (key < 0) break; else if (key == 'C') ; // already parsed. else if (key == 'P') ; // already parsed. else if (key == OPT_XFER_TYPE) ; // already parsed. else if (key == 'h') xfer->help = true; else if (key == 'v') ++xfer->verbose; else if (key == 'q') xfer->quiet = true; else if (key == 'f') xfer->sample_format_literal = arg_duplicate_string(optarg, &err); else if (key == 'c') xfer->samples_per_frame = arg_parse_decimal_num(optarg, &err); else if (key == 'r') xfer->frames_per_second = arg_parse_decimal_num(optarg, &err); else if (key == 't') xfer->cntr_format_literal = arg_duplicate_string(optarg, &err); else if (key == 'I') xfer->multiple_cntrs = true; else if (key == OPT_DUMP_HW_PARAMS) xfer->dump_hw_params = true; else if (key == '?') return -EINVAL; else { err = xfer->ops->parse_opt(xfer, key, optarg); if (err < 0 && err != -ENXIO) break; } } free(l_opts); free(s_opts); err = allocate_paths(xfer, argv + optind, argc - optind); if (err < 0) return err; return validate_options(xfer); } static const char *const allowed_duplication[] = { "/dev/null", "/dev/zero", "/dev/full", "/dev/random", "/dev/urandom", }; static int generate_path_with_suffix(struct xfer_context *xfer, const char *template, unsigned int index, const char *suffix) { static const char *const single_format = "%s%s"; static const char *const multiple_format = "%s-%i%s"; unsigned int len; len = strlen(template) + strlen(suffix) + 1; if (xfer->path_count > 1) len += (unsigned int)log10(xfer->path_count) + 2; xfer->paths[index] = malloc(len); if (xfer->paths[index] == NULL) return -ENOMEM; if (xfer->path_count == 1) { snprintf(xfer->paths[index], len, single_format, template, suffix); } else { snprintf(xfer->paths[index], len, multiple_format, template, index, suffix); } return 0; } static int generate_path_without_suffix(struct xfer_context *xfer, const char *template, unsigned int index, const char *suffix) { static const char *const single_format = "%s"; static const char *const multiple_format = "%s-%i"; unsigned int len; len = strlen(template) + 1; if (xfer->path_count > 1) len += (unsigned int)log10(xfer->path_count) + 2; xfer->paths[index] = malloc(len); if (xfer->paths[index] == NULL) return -ENOMEM; if (xfer->path_count == 1) { snprintf(xfer->paths[index], len, single_format, template); } else { snprintf(xfer->paths[index], len, multiple_format, template, index); } return 0; } static int generate_path(struct xfer_context *xfer, char *template, unsigned int index, const char *suffix) { int (*generator)(struct xfer_context *xfer, const char *template, unsigned int index, const char *suffix); char *pos; if (strlen(suffix) > 0) { pos = template + strlen(template) - strlen(suffix); // Separate filename and suffix. if (!strcmp(pos, suffix)) *pos = '\0'; } // Select handlers. if (strlen(suffix) > 0) generator = generate_path_with_suffix; else generator = generate_path_without_suffix; return generator(xfer, template, index, suffix); } static int create_paths(struct xfer_context *xfer, unsigned int path_count) { char *template; const char *suffix; int i, j; int err = 0; // Can cause memory leak. assert(xfer->path_count == 1); assert(xfer->paths); assert(xfer->paths[0]); assert(xfer->paths[0][0] != '\0'); // Release at first. template = xfer->paths[0]; free(xfer->paths); xfer->paths = NULL; // Allocate again. xfer->paths = calloc(path_count, sizeof(*xfer->paths)); if (xfer->paths == NULL) { err = -ENOMEM; goto end; } xfer->path_count = path_count; suffix = container_suffix_from_format(xfer->cntr_format); for (i = 0; i < xfer->path_count; ++i) { // Some file names are allowed to be duplicated. for (j = 0; j < ARRAY_SIZE(allowed_duplication); ++j) { if (!strcmp(template, allowed_duplication[j])) break; } if (j < ARRAY_SIZE(allowed_duplication)) continue; err = generate_path(xfer, template, i, suffix); if (err < 0) break; } end: free(template); return err; } static int fixup_paths(struct xfer_context *xfer) { const char *suffix; char *template; int i, j; int err = 0; suffix = container_suffix_from_format(xfer->cntr_format); for (i = 0; i < xfer->path_count; ++i) { // Some file names are allowed to be duplicated. for (j = 0; j < ARRAY_SIZE(allowed_duplication); ++j) { if (!strcmp(xfer->paths[i], allowed_duplication[j])) break; } if (j < ARRAY_SIZE(allowed_duplication)) continue; template = xfer->paths[i]; xfer->paths[i] = NULL; err = generate_path(xfer, template, i, suffix); free(template); if (err < 0) break; } return err; } int xfer_options_fixup_paths(struct xfer_context *xfer) { int i, j; int err; if (xfer->path_count == 1) { // Nothing to do for sign of stdin/stdout. if (!strcmp(xfer->paths[0], "-")) return 0; if (!xfer->multiple_cntrs) err = fixup_paths(xfer); else err = create_paths(xfer, xfer->samples_per_frame); } else { if (!xfer->multiple_cntrs) return -EINVAL; if (xfer->path_count != xfer->samples_per_frame) return -EINVAL; else err = fixup_paths(xfer); } if (err < 0) return err; // Check duplication of the paths. for (i = 0; i < xfer->path_count - 1; ++i) { // Some file names are allowed to be duplicated. for (j = 0; j < ARRAY_SIZE(allowed_duplication); ++j) { if (!strcmp(xfer->paths[i], allowed_duplication[j])) break; } if (j < ARRAY_SIZE(allowed_duplication)) continue; for (j = i + 1; j < xfer->path_count; ++j) { if (!strcmp(xfer->paths[i], xfer->paths[j])) { fprintf(stderr, "Detect duplicated file names:\n"); err = -EINVAL; break; } } if (j < xfer->path_count) break; } if (xfer->verbose > 1) fprintf(stderr, "Handled file names:\n"); if (err < 0 || xfer->verbose > 1) { for (i = 0; i < xfer->path_count; ++i) fprintf(stderr, " %d: %s\n", i, xfer->paths[i]); } return err; }