root/usr/src/cmd/iconv/iconv_list.c
/*
 * 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 2016 Nexenta Systems, Inc.  All rights reserved.
 */

/*
 * implement "iconv -l"
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
#include <unistd.h>
#include <alloca.h>
#include <sys/avl.h>
#include <sys/list.h>
#include <sys/param.h>
#include <stddef.h>
#include <dirent.h>
#include <unistd.h>

#define PATH_LIBICONV   "/usr/lib/iconv"
#define PATH_BTABLES    "/usr/lib/iconv/geniconvtbl/binarytables"
#define PATH_ALIASES    "/usr/lib/iconv/alias"

typedef struct codeset {
        avl_node_t cs_node;
        char *cs_name;
        list_t cs_aliases;
} codeset_t;

typedef struct csalias {
        list_node_t a_node;
        char *a_name;
} csalias_t;

static avl_tree_t       cs_avl;

static void alias_destroy(csalias_t *);

/*
 * codesets
 */

static int
cs_compare(const void *n1, const void *n2)
{
        const codeset_t *c1 = n1;
        const codeset_t *c2 = n2;
        int rv;

        rv = strcmp(c1->cs_name, c2->cs_name);
        return ((rv < 0) ? -1 : (rv > 0) ? 1 : 0);
}

static void
cs_insert(char *key)
{
        codeset_t tmp, *cs;
        avl_index_t where;

        (void) memset(&tmp, 0, sizeof (tmp));
        tmp.cs_name = key;

        cs = avl_find(&cs_avl, &tmp, &where);
        if (cs != NULL)
                return; /* already there */

        cs = calloc(1, sizeof (*cs));
        if (cs == NULL) {
                perror("cs_insert:calloc");
                exit(1);
        }
        cs->cs_name = strdup(key);
        if (cs->cs_name == NULL) {
                perror("cs_insert:strdup");
                exit(1);
        }
        list_create(&cs->cs_aliases, sizeof (csalias_t),
            offsetof(csalias_t, a_node));

        avl_insert(&cs_avl, cs, where);
}

const char topmatter[] =
        "The following are all supported code set names.  All combinations\n"
        "of those names are not necessarily available for the pair of the\n"
        "fromcode-tocode.  Some of those code set names have aliases, which\n"
        "are case-insensitive and described in parentheses following the\n"
        "canonical name:\n";


static void
cs_dump(void)
{
        codeset_t *cs;
        csalias_t *a;

        (void) puts(topmatter);

        for (cs = avl_first(&cs_avl); cs != NULL;
            cs = AVL_NEXT(&cs_avl, cs)) {

                (void) printf("    %s", cs->cs_name);
                if (!list_is_empty(&cs->cs_aliases)) {
                        a = list_head(&cs->cs_aliases);
                        (void) printf(" (%s", a->a_name);
                        while ((a = list_next(&cs->cs_aliases, a)) != NULL) {
                                (void) printf(", %s", a->a_name);
                        }
                        (void) printf(")");
                }
                (void) printf(",\n");
        }
}

static void
cs_destroy(void)
{
        void *cookie = NULL;
        codeset_t *cs;
        csalias_t *a;

        while ((cs = avl_destroy_nodes(&cs_avl, &cookie)) != NULL) {
                while ((a = list_remove_head(&cs->cs_aliases)) != NULL) {
                        alias_destroy(a);
                }
                free(cs->cs_name);
                free(cs);
        }
        avl_destroy(&cs_avl);
}

/*
 * aliases
 */

static void
alias_insert(char *codeset, char *alias)
{
        codeset_t tcs, *cs;
        csalias_t *a;

        /*
         * Find the codeset.  If non-existent,
         * ignore aliases of this codeset.
         */
        (void) memset(&tcs, 0, sizeof (tcs));
        tcs.cs_name = codeset;
        cs = avl_find(&cs_avl, &tcs, NULL);
        if (cs == NULL)
                return;

        /*
         * Add this alias
         */
        a = calloc(1, sizeof (*a));
        if (a == NULL) {
                perror("alias_insert:calloc");
                exit(1);
        }
        a->a_name = strdup(alias);
        if (a->a_name == NULL) {
                perror("alias_insert:strdup");
                exit(1);
        }

        list_insert_tail(&cs->cs_aliases, a);
}

static void
alias_destroy(csalias_t *a)
{
        free(a->a_name);
        free(a);
}


static void
scan_dir(DIR *dh, char sep, char *suffix)
{
        char namebuf[MAXNAMELEN];
        struct dirent *de;

        while ((de = readdir(dh)) != NULL) {
                char *p2, *p1;

                /*
                 * We'll modify, so let's copy.  If the dirent name is
                 * longer than MAXNAMELEN, then it can't possibly be a
                 * valid pair of codeset names, so just skip it.
                 */
                if (strlcpy(namebuf, de->d_name, sizeof (namebuf)) >=
                    sizeof (namebuf))
                        continue;

                /* Find suffix */
                p2 = strrchr(namebuf, *suffix);
                if (p2 == NULL)
                        continue;
                if (strcmp(p2, suffix) != 0)
                        continue;
                *p2 = '\0';

                p1 = strchr(namebuf, sep);
                if (p1 == NULL)
                        continue;
                *p1++ = '\0';

                /* More than one sep? */
                if (strchr(p1, sep) != NULL)
                        continue;

                /* Empty strings? */
                if (*namebuf == '\0' || *p1 == '\0')
                        continue;

                /* OK, add both to the map. */
                cs_insert(namebuf);
                cs_insert(p1);
        }
}

static void
scan_aliases(FILE *fh)
{
        char linebuf[256];
        char *p1, *p2;

        while (fgets(linebuf, sizeof (linebuf), fh) != NULL) {
                if (linebuf[0] == '#')
                        continue;
                p1 = strchr(linebuf, ' ');
                if (p1 == NULL)
                        continue;
                *p1++ = '\0';
                p2 = strchr(p1, '\n');
                if (p2 == NULL)
                        continue;
                *p2 = '\0';
                alias_insert(p1, linebuf);
        }
}

int
list_codesets(void)
{
        DIR *dh;
        FILE *fh;

        avl_create(&cs_avl, cs_compare, sizeof (codeset_t),
            offsetof(codeset_t, cs_node));

        dh = opendir(PATH_LIBICONV);
        if (dh == NULL) {
                perror(PATH_LIBICONV);
                return (1);
        }
        scan_dir(dh, '%', ".so");
        (void) closedir(dh);

        dh = opendir(PATH_BTABLES);
        if (dh == NULL) {
                perror(PATH_BTABLES);
                return (1);
        }
        scan_dir(dh, '%', ".bt");
        (void) closedir(dh);

        fh = fopen(PATH_ALIASES, "r");
        if (fh == NULL) {
                perror(PATH_ALIASES);
                /* let's continue */
        } else {
                scan_aliases(fh);
                (void) fclose(fh);
        }

        cs_dump();

        cs_destroy();

        return (0);
}