## modules/qi/query_instructions.c

/* [<][>][^][v][top][bottom][index][help] */

## FUNCTIONS

This source file includes following functions.

1. log_inst_print
2. create_name_query
3. add_filter
4. create_query
5. write_results
6. write_objects
7. insert_radix_serials
8. map_qc2rx
9. QI_execute
10. instruction_free
11. QI_free
12. QI_new

    /***************************************
      $Revision: 1.22 $

      Sql module (sq).  This is a mysql implementation of an sql module.

      Status: NOT REVUED, NOT TESTED

      Note: this code has been heavily coupled to MySQL, and may need to be changed
      (to improve performance) if a new RDBMS is used.

      ******************/ /******************
      Filename            : query_instructions.c
      Author              : ottrey@ripe.net
      OSs Tested          : Solaris
      Problems            : Moderately linked to MySQL.  Not sure which inverse
                            attributes each option has.  Would like to modify this
                            after re-designing the objects module.
      Comments            : Not sure about the different keytypes.
      ******************/ /******************
      Copyright (c) 1999                              RIPE NCC

      All Rights Reserved

      Permission to use, copy, modify, and distribute this software and its
      documentation for any purpose and without fee is hereby granted,
      provided that the above copyright notice appear in all copies and that
      both that copyright notice and this permission notice appear in
      supporting documentation, and that the name of the author not be
      used in advertising or publicity pertaining to distribution of the
      software without specific, written prior permission.

      THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
      ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS; IN NO EVENT SHALL
      AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
      DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
      AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
      OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
      ***************************************/
    #include <stdio.h>
    #include <string.h>
    #include "which_keytypes.h"
    #include "query_instructions.h"
    #include "mysql_driver.h"
    #include "rxroutines.h"
    #include "stubs.h"
    #include "constants.h"

    /*+ String sizes +*/
    #define STR_S   63
    #define STR_M   255
    #define STR_L   1023
    #define STR_XL  4095
    #define STR_XXL 16383

    #include "QI_queries.def"

    /* log_inst_print() */
    /*++++++++++++++++++++++++++++++++++++++
      Log the instruction.

      char *str instruction to be logged.

      More:
      +html+ <PRE>
      Authors:
            ottrey
      +html+ </PRE><DL COMPACT>
      +html+ <DT>Online References:
      +html+ <DD><UL>
      +html+ </UL></DL>

      ++++++++++++++++++++++++++++++++++++++*/
    static void log_inst_print(char *str) {
    /* [<][>][^][v][top][bottom][index][help] */
      FILE *logf;

      if (CO_get_instr_logging() == 1) {
        if (strcmp(CO_get_instr_logfile(), "stdout") == 0) {
          printf("%s", str);
        }
        else {
          logf = fopen(CO_get_instr_logfile(), "a");
          fprintf(logf, "%s", str);
          fclose(logf);
        }
      }

    } /* log_inst_print() */

    /* create_name_query() */
    /*++++++++++++++++++++++++++++++++++++++
      Create an sql query for the names table.

      char *query_str

      const char *sql_query

      const char *keys

      More:
      +html+ <PRE>
      Authors:
            ottrey
      +html+ </PRE><DL COMPACT>
      +html+ <DT>Online References:
      +html+ <DD><UL>
      +html+ </UL></DL>

      ++++++++++++++++++++++++++++++++++++++*/
    static void create_name_query(char *query_str, const char *sql_query, const char *keys) {
    /* [<][>][^][v][top][bottom][index][help] */
      int i;
      char *word;
      char from_clause_atom[STR_XL];
      char from_clause[STR_XXL];
      char where_clause_atom[STR_XL];
      char where_clause[STR_XXL];
      char *keys_tmp;

      strcpy(from_clause, "");
      strcpy(where_clause, "");

      keys_tmp = (char *)calloc(1, strlen(keys)+1);
      strcpy(keys_tmp, keys);

      word = (char *)strtok(keys_tmp, " ");

      sprintf(from_clause_atom, "names N%.2d", 1);
      sprintf(where_clause_atom, "N%.2d.name='%s'", 1, word);

      strcat(from_clause, from_clause_atom);
      strcat(where_clause, where_clause_atom);

      for (i=2; (word=(char *)strtok(NULL, " ")) != NULL; i++) {
        sprintf(from_clause_atom, ", names N%.2d", i);
        sprintf(where_clause_atom, " AND N%.2d.name='%s' AND N01.pe_ro_id = N%.2d.pe_ro_id", i, word, i);

        strcat(from_clause, from_clause_atom);
        strcat(where_clause, where_clause_atom);

        strcpy(from_clause_atom, "");
        strcpy(where_clause_atom, "");
      }

      sprintf(query_str, sql_query, from_clause, where_clause);

      /* XXX Free here */
      /*
      free(keys_tmp);
      */

    } /* create_name_query() */

    static void add_filter(char *query_str, const Query_command *qc) {
    /* [<][>][^][v][top][bottom][index][help] */
      int i;
      int qlen;
      char filter_atom[STR_M];

      if (MA_bitcount(qc->object_type_bitmap) > 0) {
        strcat(query_str, " AND (");
        for (i=0; i < C_END; i++) {
          if (MA_isset(qc->object_type_bitmap, i)) {
            strcpy(filter_atom, "");
            sprintf(filter_atom, "i.object_type = %d OR ", DF_get_class_dbase_code(i));
            strcat(query_str, filter_atom);
          }
        }
        qlen = strlen(query_str);
        query_str[qlen-3] = ')';
        query_str[qlen-2] = '\0';
        query_str[qlen-1] = '\0';
      }

    } /* add_filter() */

    /* create_query() */
    /*++++++++++++++++++++++++++++++++++++++
      Create an sql query from the query_command and the matching keytype and the
      selected inverse attributes.
      Note this clears the first inv_attribute it sees, so is called sequentially
      until there are no inv_attributes left.

      WK_Type keytype The matching keytype.

      const Query_command *qc The query command.

      mask_t *inv_attrs_bitmap The selected inverse attributes.

      More:
      +html+ <PRE>
      Authors:
            ottrey
      +html+ </PRE><DL COMPACT>
      +html+ <DT>Online References:
      +html+ <DD><UL>
      +html+ </UL></DL>

      ++++++++++++++++++++++++++++++++++++++*/
    static char *create_query(Query_t q, const Query_command *qc) {
    /* [<][>][^][v][top][bottom][index][help] */
      char query_str_buf[STR_XXL];
      char *query_str=NULL;
      Q_Type_t querytype;
      int conduct_test = 0;

      int ibits = MA_bitcount(qc->inv_attrs_bitmap);
      if (ibits > 0 ) {
        querytype = Q_INVERSE;
      }
      else {
        querytype = Q_LOOKUP;
      }

      strcpy(query_str_buf, "");

      if (   (q.query != NULL)
          && (q.querytype == querytype) ) {

        if (querytype == Q_LOOKUP) {
          if (MA_bitcount(qc->object_type_bitmap) == 0) {
            conduct_test=1;
          }
          else if (MA_isset(qc->object_type_bitmap, q.class)) {
            conduct_test=1;
          }
        }
        else {
          conduct_test=1;
        }
      }
      else {
        conduct_test=0;
      }

      if (conduct_test == 1) {
        if (q.keytype == WK_NAME) {
          /* Name queries require special treatment. */
          create_name_query(query_str_buf, q.query, qc->keys);
        }
        else {
          sprintf(query_str_buf, q.query, qc->keys);
        }

        if (q.querytype == -1) {
          /* It is class type ANY so add the object filtering */
          add_filter(query_str_buf, qc);
        }

        if (strcmp(query_str_buf, "") == 0) {
          query_str = NULL;
        }
        else {
          query_str = (char *)calloc(1, strlen(query_str_buf)+1);
          strcpy(query_str, query_str_buf);
        }
      }

      return query_str;
    } /* create_query() */

    /* write_results() */
    /*++++++++++++++++++++++++++++++++++++++
      Write the results to the client socket.

      SQ_result_set_t *result The result set returned from the sql query.

      int sock The socket that the client is connected to.

      XXX NB. this is very dependendant on what rows are returned in the result!!!

      More:
      +html+ <PRE>
      Authors:
            ottrey
      +html+ </PRE><DL COMPACT>
      +html+ <DT>Online References:
      +html+ <DD><UL>
      +html+ </UL></DL>

      ++++++++++++++++++++++++++++++++++++++*/
    static int write_results(SQ_result_set_t *result, int sock) {
    /* [<][>][^][v][top][bottom][index][help] */
      SQ_row_t *row;
      char *str;
      char log_str[STR_L];
      int retrieved_objects=0;

      /* Get all the results - one at a time */
      if (result != NULL) {
        while ( (row = SQ_row_next(result)) != NULL) {
          str = SQ_get_column_string(result, row, 0);
          if (str != NULL) {
            strcpy(log_str, "");
            sprintf(log_str, "Retrieved serial id = %d\n", atoi(str));
            log_inst_print(log_str);
          }
          free(str);

          str = SQ_get_column_string(result, row, 2);
          if (str != NULL) {
            SK_puts(sock, str);
            SK_puts(sock, "\n");
            retrieved_objects++;
          }
          free(str);
        }
      }

      return retrieved_objects;
    } /* write_results() */

    /* write_objects() */
    /*++++++++++++++++++++++++++++++++++++++
      This is linked into MySQL by the fact that MySQL doesn't have sub selects
      (yet).  The queries are done in two stages.  Make some temporary tables and
      insert into them.  Then use them in the next select.

      SQ_connection_t *sql_connection The connection to the database.

      char *id_table The id of the temporary table (This is a result of the hacky
                      way we've tried to get MySQL to do sub-selects.)

      unsigned int recursive A recursive query.

      unsigned int sock The client socket.

      More:
      +html+ <PRE>
      Authors:
            ottrey
      +html+ </PRE><DL COMPACT>
      ++++++++++++++++++++++++++++++++++++++*/
    static void write_objects(SQ_connection_t *sql_connection, char *id_table, unsigned int recursive, unsigned int sock) {
    /* [<][>][^][v][top][bottom][index][help] */
      /* XXX This should really return a linked list of the objects */

      SQ_result_set_t *result;
      int retrieved_objects=0;

      char sql_command[STR_XL];
      char log_str[STR_L];

      strcpy(sql_command, "");
      /* XXX These may and should change a lot. */
      sprintf(sql_command, Q_OBJECTS, id_table, id_table);
      result = SQ_execute_query(sql_connection, sql_command);

      retrieved_objects += write_results(result, sock);

      SQ_free_result(result);

      /* Now for recursive queries */
      if (recursive == 1) {
        strcpy(sql_command, "");
        sprintf(sql_command, Q_REC, id_table, "admin_c", id_table, id_table);
        SQ_execute_query(sql_connection, sql_command);

        strcpy(sql_command, "");
        sprintf(sql_command, Q_REC, id_table, "tech_c", id_table, id_table);
        SQ_execute_query(sql_connection, sql_command);

        strcpy(sql_command, "");
        sprintf(sql_command, Q_REC, id_table, "zone_c", id_table, id_table);
        SQ_execute_query(sql_connection, sql_command);

        /* XXX These may and should change a lot. */
        strcpy(sql_command, "");
        sprintf(sql_command, Q_REC_OBJECTS, id_table, id_table, id_table);
        result = SQ_execute_query(sql_connection, sql_command);

        retrieved_objects += write_results(result, sock);

        SQ_free_result(result);

        /* Now drop the IDS recursive table */
        strcpy(sql_command, "");
        sprintf(sql_command, "DROP TABLE %s_R", id_table);
        SQ_execute_query(sql_connection, sql_command);
      }

      /* If nothing is retreived default to return the 0th object - I.e "not found" */
      if (retrieved_objects == 0) {
        result = SQ_execute_query(sql_connection, Q_NO_OBJECTS);
        write_results(result, sock);

        SQ_free_result(result);
      }

      /* Now drop the IDS table */
      strcpy(sql_command, "");
      sprintf(sql_command, "DROP TABLE %s", id_table);
      SQ_execute_query(sql_connection, sql_command);

      strcpy(log_str, "");
      sprintf(log_str, "%d object(s) retrieved\n\n", retrieved_objects);
      log_inst_print(log_str);

      if (CO_get_accounting() == 1) {
        /* XXX - later man.
        account(retrieved_objects);
        */
        printf("account(retrieved_objects);\n");
      }

    } /* write_objects() */

    /* insert_radix_serials() */
    /*++++++++++++++++++++++++++++++++++++++
      Insert the radix serial numbers into a temporary table in the database.

      mask_t bitmap The bitmap of attribute to be converted.

      SQ_connection_t *sql_connection The connection to the database.

      char *id_table The id of the temporary table (This is a result of the hacky
                      way we've tried to get MySQL to do sub-selects.)

      GList *datlist The list of data from the radix tree.

      XXX Hmmmmm this isn't really a good place to free things... infact it's quite nasty.  :-(

      More:
      +html+ <PRE>
      Authors:
            ottrey
      +html+ </PRE><DL COMPACT>
      +html+ <DT>Online References:
      +html+ <DD><UL>
                 <LI><A HREF="http://www.gtk.org/rdp/glib/glib-doubly-linked-lists.html">Glist</A>
      +html+ </UL></DL>

      ++++++++++++++++++++++++++++++++++++++*/
    static void insert_radix_serials(SQ_connection_t *sql_connection, char *id_table, GList *datlist) {
    /* [<][>][^][v][top][bottom][index][help] */
      GList    *qitem;
      char sql_command[STR_XL];
      int serial;
      int i;

    /*
      TODO -- marek --- TODO
      for( qitem = g_list_last(datlist); qitem != NULL; qitem = g_list_previous(qitem)) {
     */
      for( qitem = g_list_first(datlist); qitem != NULL; qitem = g_list_next(qitem)) {
        rx_datcpy_t *datcpy = qitem->data;

        serial = datcpy->leafcpy.data_key;

        strcpy(sql_command, "");
        sprintf(sql_command, "INSERT INTO %s values (%d)", id_table, serial);
        SQ_execute_query(sql_connection, sql_command);

        /* XXX AAAARGH why here?!?! */
        wr_free(datcpy->leafcpy.data_ptr);
      }

      /* XXX Possible memory leak here. -fix above?? */
      g_list_foreach(datlist, rx_free_list_element, NULL);
      g_list_free(datlist);

    } /* insert_radix_serials() */

    /* map_qc2rx() */
    /*++++++++++++++++++++++++++++++++++++++
      The mapping between a query_command and a radix query.

      Query_instruction *qi The Query Instruction to be created from the mapping
                            of the query command.

      const Query_command *qc The query command to be mapped.

      More:
      +html+ <PRE>
      Authors:
            ottrey
      +html+ </PRE><DL COMPACT>
      +html+ <DT>Online References:
      +html+ <DD><UL>
      +html+ </UL></DL>

      ++++++++++++++++++++++++++++++++++++++*/
    static int map_qc2rx(Query_instruction *qi, const Query_command *qc) {
    /* [<][>][^][v][top][bottom][index][help] */
      int result=1;

      qi->rx_keys = qc->keys;

      switch(qi->family) {
        case RX_FAM_IN:
          if (!MA_isset(qc->object_type_bitmap, C_IN)) {
            result=0;
          }
        break;

        case RX_FAM_RT:
          if (!MA_isset(qc->object_type_bitmap, C_RT)) {
            result=0;
          }
        break;

        default:
          fprintf(stderr, "ERROR: Bad family type in radix query\n");
      }

      if ( (qc->L == 0) && (qc->M == 0) && (qc->l == 0) && (qc->m == 0) && (qc->x == 0) ) {
        qi->rx_srch_mode = RX_SRCH_EXLESS;
        qi->rx_par_a = 0;
      }
      else if ( (qc->L == 1) && (qc->M == 0) && (qc->l == 0) && (qc->m == 0) && (qc->x == 0) ) {
        qi->rx_srch_mode = RX_SRCH_LESS;
        qi->rx_par_a = RX_ALL_DEPTHS;
      }
      else if ( (qc->L == 0) && (qc->M == 1) && (qc->l == 0) && (qc->m == 0) && (qc->x == 0) ) {
        qi->rx_srch_mode = RX_SRCH_MORE;
        qi->rx_par_a = RX_ALL_DEPTHS;
      }
      else if ( (qc->L == 0) && (qc->M == 0) && (qc->l == 1) && (qc->m == 0) && (qc->x == 0) ) {
        qi->rx_srch_mode = RX_SRCH_LESS;
        qi->rx_par_a = 1;
      }
      else if ( (qc->L == 0) && (qc->M == 0) && (qc->l == 0) && (qc->m == 1) && (qc->x == 0) ) {
        qi->rx_srch_mode = RX_SRCH_MORE;
        qi->rx_par_a = 1;
      }
      else if ( (qc->L == 0) && (qc->M == 0) && (qc->l == 0) && (qc->m == 0) && (qc->x == 1) ) {
        qi->rx_srch_mode = RX_SRCH_EXACT;
        qi->rx_par_a = 0;
      }
      else {
        strcpy(log_str, "");
        sprintf(log_str, "ERROR in qc2rx mapping: bad combination of flags\n");
        log_inst_print(log_str);
        result = 0;
      }

      return result;

    } /* map_qc2rx() */

    /* QI_execute() */
    /*++++++++++++++++++++++++++++++++++++++
      Execute the query instructions.  This is called by a g_list_foreach
      function, so each of the sources in the "database source" list can be passed
      into this function.

      void *database_voidptr Pointer to the database.

      void *qis_voidptr Pointer to the query_instructions.

      More:
      +html+ <PRE>
      Authors:
            ottrey
      +html+ </PRE><DL COMPACT>
      +html+ <DT>Online References:
      +html+ <DD><UL>
                 <LI><A
                 HREF="http://www.gtk.org/rdp/glib/glib-singly-linked-lists.html#G-SLIST-FOREACH">g_list_foreach</A>
      +html+ </UL></DL>

      ++++++++++++++++++++++++++++++++++++++*/
    void QI_execute(void *database_voidptr, void *qis_voidptr) {
    /* [<][>][^][v][top][bottom][index][help] */
      char *database = (char *)database_voidptr;
      Query_instructions *qis = (Query_instructions *)qis_voidptr;
      Query_instruction **ins=NULL;
      char id_table[STR_S];
      char sql_command[STR_XL];
      GList *datlist=NULL;
      int i;

      char log_str[STR_L];

      SQ_connection_t *sql_connection=NULL;

      sql_connection = SQ_get_connection(CO_get_host(), CO_get_database_port(), database, CO_get_user(), CO_get_password() );

      if (sql_connection == NULL) {
        SK_puts(qis->sock, "% WARNING: Failed to make connection to ");
        SK_puts(qis->sock, database);
        SK_puts(qis->sock, " database mirror.\n\n");
      }
      else {
        strcpy(sql_command, "");

        /* XXX This is a really bad thing to do.  It should'nt _have_ to be called here.
           But unfortunately it does.   -- Sigh. */
        sprintf(id_table, "ID_%d", mysql_thread_id(sql_connection) );

        strcpy(sql_command, "");
        sprintf(sql_command, "CREATE TABLE %s ( id int ) TYPE=HEAP", id_table);
        SQ_execute_query(sql_connection, sql_command);

        if (qis->recursive == 1) {
          strcpy(sql_command, "");
          sprintf(sql_command, "CREATE TABLE %s_R ( id int ) TYPE=HEAP", id_table);
          SQ_execute_query(sql_connection, sql_command);
        }

        ins = qis->instruction;
        for (i=0; ins[i] != NULL; i++) {
          Query_instruction *qi = ins[i];

          switch ( qi->search_type ) {
            case R_SQL:
              if ( qi->query_str != NULL ) {
                strcpy(sql_command, "");
                sprintf(sql_command, "INSERT INTO %s %s", id_table, qi->query_str);
                SQ_execute_query(sql_connection, sql_command);
              }
            break;

    #define RIPE_REG 17
            case R_RADIX:
              datlist = NULL;
              /* Apply the object filter */
              if ( RX_asc_search(qi->rx_srch_mode, qi->rx_par_a, 0, qi->rx_keys, RIPE_REG, qi->space, qi->family, &datlist, RX_ANS_ALL) == RX_OK ) {
                insert_radix_serials(sql_connection, id_table, datlist);
              }
              else {
                /* Skip query */
                strcpy(log_str, "");
                sprintf(log_str, " /* Skip in query */\n");
                log_inst_print(log_str);
              }
            break;

            default:
              strcpy(log_str, "");
              sprintf(log_str, "ERROR: bad search_type\n");
              log_inst_print(log_str);
          } /* switch */
        }
        write_objects(sql_connection, id_table, qis->recursive, qis->sock);
      }

      SQ_close_connection(sql_connection);

    } /* QI_execute() */

    /* instruction_free() */
    /*++++++++++++++++++++++++++++++++++++++
      Free the instruction.

      Query_instruction *qi query_instruction to be freed.

      More:
      +html+ <PRE>
      Authors:
            ottrey
      +html+ </PRE><DL COMPACT>
      +html+ <DT>Online References:
      +html+ <DD><UL>
      +html+ </UL></DL>

      ++++++++++++++++++++++++++++++++++++++*/
    static void instruction_free(Query_instruction *qi) {
    /* [<][>][^][v][top][bottom][index][help] */
      if (qi != NULL) {
        if (qi->query_str != NULL) {
          free(qi->query_str);
        }
        free(qi);
      }
    } /* instruction_free() */

    /* QI_free() */
    /*++++++++++++++++++++++++++++++++++++++
      Free the query_instructions.

      Query_instructions *qis Query_instructions to be freed.

      XXX This isn't working too well at the moment.

      More:
      +html+ <PRE>
      Authors:
            ottrey
      +html+ </PRE><DL COMPACT>
      +html+ <DT>Online References:
      +html+ <DD><UL>
      +html+ </UL></DL>

      ++++++++++++++++++++++++++++++++++++++*/
    void QI_free(Query_instructions *qis) {
    /* [<][>][^][v][top][bottom][index][help] */
      /* XXX huh!?H?
      int i;

      for (i=0; qis[i] != NULL; i++) {
        instruction_free(*qis[i]);
      }
      */
      if (qis != NULL) {
        free(qis);
      }

    } /* QI_free() */

    /* QI_new() */
    /*++++++++++++++++++++++++++++++++++++++
      Create a new set of query_instructions.

      mask_t bitmap The bitmap of attribute to be converted.

      const Query_command *qc The query_command that the instructions are created
                              from.

      unsigned int sock The client socket.

      More:
      +html+ <PRE>
      Authors:
            ottrey
      +html+ </PRE><DL COMPACT>
      +html+ <DT>Online References:
      +html+ <DD><UL>
      +html+ </UL></DL>

      ++++++++++++++++++++++++++++++++++++++*/
    Query_instructions *QI_new(const Query_command *qc, unsigned int sock) {
    /* [<][>][^][v][top][bottom][index][help] */
      Query_instructions *qis=NULL;
      Query_instruction *qi=NULL;
      int i_no=0,j;
      int i;
      char *query_str;

      char log_str[STR_L];

      qis = (Query_instructions *)calloc(1, sizeof(Query_instructions));
      qis->sock = sock;
      qis->recursive = qc->qe.recursive;

      for (i=0; Query[i].query != NULL; i++) {

        /* If matched the keytype */
        if ( MA_isset(qc->keytypes_bitmap, Query[i].keytype) == 1) {
          qi = (Query_instruction *)calloc(1, sizeof(Query_instruction));

          /* SQL Query */
          if ( Query[i].refer == R_SQL) {
            qi->search_type = R_SQL;
            query_str = create_query(Query[i], qc);
            if (query_str != NULL) {
              qi->query_str = query_str;
              qis->instruction[i_no++] = qi;
            }
          }
          /* Radix Query */
          else if (Query[i].refer == R_RADIX) {
            qi->search_type = R_RADIX;
            qi->space = Query[i].space;
            qi->family = Query[i].family;

            if (map_qc2rx(qi, qc) == 1) {
              /* Add the query_instruction to the array */
              qis->instruction[i_no++] = qi;
            }
          }
          else {
            strcpy(log_str, "");
            sprintf(log_str, "ERROR: bad search_type\n");
            log_inst_print(log_str);
          }
        }
      }
      qis->instruction[i_no++] = NULL;

      return qis;

    } /* QI_new() */

/* [<][>][^][v][top][bottom][index][help] */