--- /dev/null
+// SPDX-License-Identifier: GPL-2.0
+//
+// xfer-options.c - a parser of commandline options for xfer.
+//
+// Copyright (c) 2018 Takashi Sakamoto <o-takashi@sakamocchi.jp>
+//
+// Licensed under the terms of the GNU General Public License, version 2.
+
+#include "xfer.h"
+#include "misc.h"
+
+#include <getopt.h>
+#include <math.h>
+#include <limits.h>
+
+enum no_short_opts {
+ // 128 or later belong to non us-ascii character set.
+ OPT_XFER_TYPE = 128,
+};
+
+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 = "CPhvf: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'},
+ // 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'},
+ };
+ 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 == '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 == '?')
+ 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;
+}