| /*------------------------------------------------------------------------- |
| * |
| * pg_backup_db.c |
| * |
| * Implements the basic DB functions used by the archiver. |
| * |
| * IDENTIFICATION |
| * $PostgreSQL: pgsql/src/bin/pg_dump/pg_backup_db.c,v 1.75 2006/10/04 00:30:05 momjian Exp $ |
| * |
| *------------------------------------------------------------------------- |
| */ |
| |
| #include "pg_backup_db.h" |
| #include "dumputils.h" |
| |
| #include <unistd.h> |
| |
| #include <ctype.h> |
| |
| #ifdef HAVE_TERMIOS_H |
| #include <termios.h> |
| #endif |
| |
| |
| static const char *modulename = gettext_noop("archiver (db)"); |
| |
| static void _check_database_version(ArchiveHandle *AH); |
| static PGconn *_connectDB(ArchiveHandle *AH, const char *newdbname, const char *newUser); |
| static void notice_processor(void *arg __attribute__((unused)), const char *message); |
| static char *_sendSQLLine(ArchiveHandle *AH, char *qry, char *eos); |
| static char *_sendCopyLine(ArchiveHandle *AH, char *qry, char *eos); |
| |
| static bool _isIdentChar(unsigned char c); |
| static bool _isDQChar(unsigned char c, bool atStart); |
| |
| #define DB_MAX_ERR_STMT 128 |
| |
| static int |
| _parse_version(ArchiveHandle *AH, const char *versionString) |
| { |
| int v; |
| |
| v = parse_version(versionString); |
| if (v < 0) |
| die_horribly(AH, modulename, "could not parse version string \"%s\"\n", versionString); |
| |
| return v; |
| } |
| |
| static void |
| _check_database_version(ArchiveHandle *AH) |
| { |
| int myversion; |
| const char *remoteversion_str; |
| int remoteversion; |
| |
| myversion = _parse_version(AH, PG_VERSION); |
| |
| remoteversion_str = PQparameterStatus(AH->connection, "server_version"); |
| if (!remoteversion_str) |
| die_horribly(AH, modulename, "could not get server_version from libpq\n"); |
| |
| remoteversion = _parse_version(AH, remoteversion_str); |
| |
| AH->public.remoteVersionStr = strdup(remoteversion_str); |
| AH->public.remoteVersion = remoteversion; |
| if (!AH->archiveRemoteVersion) |
| AH->archiveRemoteVersion = AH->public.remoteVersionStr; |
| |
| if (myversion != remoteversion |
| && (remoteversion < AH->public.minRemoteVersion || |
| remoteversion > AH->public.maxRemoteVersion)) |
| { |
| write_msg(NULL, "server version: %s; %s version: %s\n", |
| remoteversion_str, progname, PG_VERSION); |
| die_horribly(AH, NULL, "aborting because of server version mismatch\n"); |
| } |
| } |
| |
| /* |
| * Reconnect to the server. If dbname is not NULL, use that database, |
| * else the one associated with the archive handle. If username is |
| * not NULL, use that user name, else the one from the handle. If |
| * both the database and the user match the existing connection already, |
| * nothing will be done. |
| * |
| * Returns 1 in any case. |
| */ |
| int |
| ReconnectToServer(ArchiveHandle *AH, const char *dbname, const char *username) |
| { |
| PGconn *newConn; |
| const char *newdbname; |
| const char *newusername; |
| |
| if (!dbname) |
| newdbname = PQdb(AH->connection); |
| else |
| newdbname = dbname; |
| |
| if (!username) |
| newusername = PQuser(AH->connection); |
| else |
| newusername = username; |
| |
| /* Let's see if the request is already satisfied */ |
| if (strcmp(newdbname, PQdb(AH->connection)) == 0 && |
| strcmp(newusername, PQuser(AH->connection)) == 0) |
| return 1; |
| |
| newConn = _connectDB(AH, newdbname, newusername); |
| |
| PQfinish(AH->connection); |
| AH->connection = newConn; |
| |
| return 1; |
| } |
| |
| /* |
| * Connect to the db again. |
| * |
| * Note: it's not really all that sensible to use a single-entry password |
| * cache if the username keeps changing. In current usage, however, the |
| * username never does change, so one savedPassword is sufficient. We do |
| * update the cache on the off chance that the password has changed since the |
| * start of the run. |
| */ |
| static PGconn * |
| _connectDB(ArchiveHandle *AH, const char *reqdb, const char *requser) |
| { |
| PGconn *newConn; |
| const char *newdb; |
| const char *newuser; |
| char *password = AH->savedPassword; |
| bool new_pass; |
| |
| if (!reqdb) |
| newdb = PQdb(AH->connection); |
| else |
| newdb = reqdb; |
| |
| if (!requser || strlen(requser) == 0) |
| newuser = PQuser(AH->connection); |
| else |
| newuser = requser; |
| |
| ahlog(AH, 1, "connecting to database \"%s\" as user \"%s\"\n", |
| newdb, newuser); |
| |
| if (AH->promptPassword == TRI_YES && password == NULL) |
| { |
| password = simple_prompt("Password: ", 100, false); |
| if (password == NULL) |
| die_horribly(AH, modulename, "out of memory\n"); |
| } |
| |
| do |
| { |
| #define PARAMS_ARRAY_SIZE 7 |
| const char **keywords = malloc(PARAMS_ARRAY_SIZE * sizeof(*keywords)); |
| const char **values = malloc(PARAMS_ARRAY_SIZE * sizeof(*values)); |
| |
| if (!keywords || !values) |
| die_horribly(AH, modulename, "out of memory\n"); |
| |
| keywords[0] = "host"; |
| values[0] = PQhost(AH->connection); |
| keywords[1] = "port"; |
| values[1] = PQport(AH->connection); |
| keywords[2] = "user"; |
| values[2] = newuser; |
| keywords[3] = "password"; |
| values[3] = password; |
| keywords[4] = "dbname"; |
| values[4] = newdb; |
| keywords[5] = "fallback_application_name"; |
| values[5] = progname; |
| keywords[6] = NULL; |
| values[6] = NULL; |
| |
| new_pass = false; |
| newConn = PQconnectdbParams(keywords, values, true); |
| |
| free(keywords); |
| free(values); |
| |
| if (!newConn) |
| die_horribly(AH, modulename, "failed to reconnect to database\n"); |
| |
| if (PQstatus(newConn) == CONNECTION_BAD) |
| { |
| if (!PQconnectionNeedsPassword(newConn)) |
| die_horribly(AH, modulename, "could not reconnect to database: %s", |
| PQerrorMessage(newConn)); |
| PQfinish(newConn); |
| |
| if (password) |
| fprintf(stderr, "Password incorrect\n"); |
| |
| fprintf(stderr, "Connecting to %s as %s\n", |
| newdb, newuser); |
| |
| if (password) |
| free(password); |
| |
| if (AH->promptPassword != TRI_NO) |
| password = simple_prompt("Password: ", 100, false); |
| else |
| die_horribly(AH, modulename, "connection needs password\n"); |
| |
| if (password == NULL) |
| die_horribly(AH, modulename, "out of memory\n"); |
| new_pass = true; |
| } |
| } while (new_pass); |
| |
| AH->savedPassword = password; |
| |
| /* check for version mismatch */ |
| _check_database_version(AH); |
| |
| PQsetNoticeProcessor(newConn, notice_processor, NULL); |
| |
| return newConn; |
| } |
| |
| |
| /* |
| * Make a database connection with the given parameters. The |
| * connection handle is returned, the parameters are stored in AHX. |
| * An interactive password prompt is automatically issued if required. |
| * |
| * Note: it's not really all that sensible to use a single-entry password |
| * cache if the username keeps changing. In current usage, however, the |
| * username never does change, so one savedPassword is sufficient. |
| */ |
| PGconn * |
| ConnectDatabase(Archive *AHX, |
| const char *dbname, |
| const char *pghost, |
| const char *pgport, |
| const char *username, |
| enum trivalue prompt_password) |
| { |
| ArchiveHandle *AH = (ArchiveHandle *) AHX; |
| char *password = AH->savedPassword; |
| bool new_pass; |
| |
| if (AH->connection) |
| die_horribly(AH, modulename, "already connected to a database\n"); |
| |
| if (prompt_password == TRI_YES && password == NULL) |
| { |
| password = simple_prompt("Password: ", 100, false); |
| if (password == NULL) |
| die_horribly(AH, modulename, "out of memory\n"); |
| } |
| AH->promptPassword = prompt_password; |
| |
| /* |
| * Start the connection. Loop until we have a password if requested by |
| * backend. |
| */ |
| do |
| { |
| #define PARAMS_ARRAY_SIZE 7 |
| const char **keywords = malloc(PARAMS_ARRAY_SIZE * sizeof(*keywords)); |
| const char **values = malloc(PARAMS_ARRAY_SIZE * sizeof(*values)); |
| |
| if (!keywords || !values) |
| die_horribly(AH, modulename, "out of memory\n"); |
| |
| keywords[0] = "host"; |
| values[0] = pghost; |
| keywords[1] = "port"; |
| values[1] = pgport; |
| keywords[2] = "user"; |
| values[2] = username; |
| keywords[3] = "password"; |
| values[3] = password; |
| keywords[4] = "dbname"; |
| values[4] = dbname; |
| keywords[5] = "fallback_application_name"; |
| values[5] = progname; |
| keywords[6] = NULL; |
| values[6] = NULL; |
| |
| new_pass = false; |
| AH->connection = PQconnectdbParams(keywords, values, true); |
| |
| free(keywords); |
| free(values); |
| |
| if (!AH->connection) |
| die_horribly(AH, modulename, "failed to connect to database\n"); |
| |
| if (PQstatus(AH->connection) == CONNECTION_BAD && |
| PQconnectionNeedsPassword(AH->connection) && |
| password == NULL && |
| prompt_password != TRI_NO) |
| { |
| PQfinish(AH->connection); |
| password = simple_prompt("Password: ", 100, false); |
| if (password == NULL) |
| die_horribly(AH, modulename, "out of memory\n"); |
| new_pass = true; |
| } |
| } while (new_pass); |
| |
| AH->savedPassword = password; |
| |
| /* check to see that the backend connection was successfully made */ |
| if (PQstatus(AH->connection) == CONNECTION_BAD) |
| die_horribly(AH, modulename, "connection to database \"%s\" failed: %s", |
| PQdb(AH->connection), PQerrorMessage(AH->connection)); |
| |
| /* check for version mismatch */ |
| _check_database_version(AH); |
| |
| PQsetNoticeProcessor(AH->connection, notice_processor, NULL); |
| |
| return AH->connection; |
| } |
| |
| |
| static void |
| notice_processor(void *arg __attribute__((unused)), const char *message) |
| { |
| write_msg(NULL, "%s", message); |
| } |
| |
| |
| /* Public interface */ |
| /* Convenience function to send a query. Monitors result to handle COPY statements */ |
| static void |
| ExecuteSqlCommand(ArchiveHandle *AH, const char *qry, const char *desc) |
| { |
| PGconn *conn = AH->connection; |
| PGresult *res; |
| char errStmt[DB_MAX_ERR_STMT]; |
| |
| #ifdef NOT_USED |
| fprintf(stderr, "Executing: '%s'\n\n", qry); |
| #endif |
| res = PQexec(conn, qry); |
| |
| switch (PQresultStatus(res)) |
| { |
| case PGRES_COMMAND_OK: |
| case PGRES_TUPLES_OK: |
| /* A-OK */ |
| break; |
| case PGRES_COPY_IN: |
| /* Assume this is an expected result */ |
| AH->pgCopyIn = true; |
| break; |
| default: |
| /* trouble */ |
| strncpy(errStmt, qry, DB_MAX_ERR_STMT); |
| if (errStmt[DB_MAX_ERR_STMT - 1] != '\0') |
| { |
| errStmt[DB_MAX_ERR_STMT - 4] = '.'; |
| errStmt[DB_MAX_ERR_STMT - 3] = '.'; |
| errStmt[DB_MAX_ERR_STMT - 2] = '.'; |
| errStmt[DB_MAX_ERR_STMT - 1] = '\0'; |
| } |
| warn_or_die_horribly(AH, modulename, "%s: %s Command was: %s\n", |
| desc, PQerrorMessage(conn), errStmt); |
| break; |
| } |
| |
| PQclear(res); |
| } |
| |
| /* |
| * Used by ExecuteSqlCommandBuf to send one buffered line when running a COPY command. |
| */ |
| static char * |
| _sendCopyLine(ArchiveHandle *AH, char *qry, char *eos) |
| { |
| size_t loc; /* Location of next newline */ |
| int pos = 0; /* Current position */ |
| int sPos = 0; /* Last pos of a slash char */ |
| int isEnd = 0; |
| |
| /* loop to find unquoted newline ending the line of COPY data */ |
| for (;;) |
| { |
| loc = strcspn(&qry[pos], "\n") + pos; |
| |
| /* If no match, then wait */ |
| if (loc >= (eos - qry)) /* None found */ |
| { |
| appendBinaryPQExpBuffer(AH->pgCopyBuf, qry, (eos - qry)); |
| return eos; |
| } |
| |
| /* |
| * fprintf(stderr, "Found cr at %d, prev char was %c, next was %c\n", |
| * loc, qry[loc-1], qry[loc+1]); |
| */ |
| |
| /* Count the number of preceding slashes */ |
| sPos = loc; |
| while (sPos > 0 && qry[sPos - 1] == '\\') |
| sPos--; |
| |
| sPos = loc - sPos; |
| |
| /* |
| * If an odd number of preceding slashes, then \n was escaped so set |
| * the next search pos, and loop (if any left). |
| */ |
| if ((sPos & 1) == 1) |
| { |
| /* fprintf(stderr, "cr was escaped\n"); */ |
| pos = loc + 1; |
| if (pos >= (eos - qry)) |
| { |
| appendBinaryPQExpBuffer(AH->pgCopyBuf, qry, (eos - qry)); |
| return eos; |
| } |
| } |
| else |
| break; |
| } |
| |
| /* We found an unquoted newline */ |
| qry[loc] = '\0'; |
| appendPQExpBuffer(AH->pgCopyBuf, "%s\n", qry); |
| isEnd = (strcmp(AH->pgCopyBuf->data, "\\.\n") == 0); |
| |
| /* |
| * Note that we drop the data on the floor if libpq has failed to enter |
| * COPY mode; this allows us to behave reasonably when trying to continue |
| * after an error in a COPY command. |
| */ |
| if (AH->pgCopyIn && |
| PQputCopyData(AH->connection, AH->pgCopyBuf->data, |
| AH->pgCopyBuf->len) <= 0) |
| die_horribly(AH, modulename, "error returned by PQputCopyData: %s", |
| PQerrorMessage(AH->connection)); |
| |
| resetPQExpBuffer(AH->pgCopyBuf); |
| |
| if (isEnd && AH->pgCopyIn) |
| { |
| PGresult *res; |
| |
| if (PQputCopyEnd(AH->connection, NULL) <= 0) |
| die_horribly(AH, modulename, "error returned by PQputCopyEnd: %s", |
| PQerrorMessage(AH->connection)); |
| |
| /* Check command status and return to normal libpq state */ |
| res = PQgetResult(AH->connection); |
| if (PQresultStatus(res) != PGRES_COMMAND_OK) |
| warn_or_die_horribly(AH, modulename, "COPY failed: %s", |
| PQerrorMessage(AH->connection)); |
| PQclear(res); |
| |
| AH->pgCopyIn = false; |
| } |
| |
| return qry + loc + 1; |
| } |
| |
| /* |
| * Used by ExecuteSqlCommandBuf to send one buffered line of SQL |
| * (not data for the copy command). |
| */ |
| static char * |
| _sendSQLLine(ArchiveHandle *AH, char *qry, char *eos) |
| { |
| /* |
| * The following is a mini state machine to assess the end of an SQL |
| * statement. It really only needs to parse good SQL, or at least that's |
| * the theory... End-of-statement is assumed to be an unquoted, |
| * un-commented semi-colon that's not within any parentheses. |
| * |
| * Note: the input can be split into bufferloads at arbitrary boundaries. |
| * Therefore all state must be kept in AH->sqlparse, not in local |
| * variables of this routine. We assume that AH->sqlparse was filled with |
| * zeroes when created. |
| */ |
| for (; qry < eos; qry++) |
| { |
| switch (AH->sqlparse.state) |
| { |
| case SQL_SCAN: /* Default state == 0, set in _allocAH */ |
| if (*qry == ';' && AH->sqlparse.braceDepth == 0) |
| { |
| /* |
| * We've found the end of a statement. Send it and reset |
| * the buffer. |
| */ |
| appendPQExpBufferChar(AH->sqlBuf, ';'); /* inessential */ |
| ExecuteSqlCommand(AH, AH->sqlBuf->data, |
| "could not execute query"); |
| resetPQExpBuffer(AH->sqlBuf); |
| AH->sqlparse.lastChar = '\0'; |
| |
| /* |
| * Remove any following newlines - so that embedded COPY |
| * commands don't get a starting newline. |
| */ |
| qry++; |
| while (qry < eos && *qry == '\n') |
| qry++; |
| |
| /* We've finished one line, so exit */ |
| return qry; |
| } |
| else if (*qry == '\'') |
| { |
| if (AH->sqlparse.lastChar == 'E') |
| AH->sqlparse.state = SQL_IN_E_QUOTE; |
| else |
| AH->sqlparse.state = SQL_IN_SINGLE_QUOTE; |
| AH->sqlparse.backSlash = false; |
| } |
| else if (*qry == '"') |
| { |
| AH->sqlparse.state = SQL_IN_DOUBLE_QUOTE; |
| } |
| |
| /* |
| * Look for dollar-quotes. We make the assumption that |
| * $-quotes will not have an ident character just before them |
| * in pg_dump output. XXX is this good enough? |
| */ |
| else if (*qry == '$' && !_isIdentChar(AH->sqlparse.lastChar)) |
| { |
| AH->sqlparse.state = SQL_IN_DOLLAR_TAG; |
| /* initialize separate buffer with possible tag */ |
| if (AH->sqlparse.tagBuf == NULL) |
| AH->sqlparse.tagBuf = createPQExpBuffer(); |
| else |
| resetPQExpBuffer(AH->sqlparse.tagBuf); |
| appendPQExpBufferChar(AH->sqlparse.tagBuf, *qry); |
| } |
| else if (*qry == '-' && AH->sqlparse.lastChar == '-') |
| AH->sqlparse.state = SQL_IN_SQL_COMMENT; |
| else if (*qry == '*' && AH->sqlparse.lastChar == '/') |
| AH->sqlparse.state = SQL_IN_EXT_COMMENT; |
| else if (*qry == '(') |
| AH->sqlparse.braceDepth++; |
| else if (*qry == ')') |
| AH->sqlparse.braceDepth--; |
| break; |
| |
| case SQL_IN_SQL_COMMENT: |
| if (*qry == '\n') |
| AH->sqlparse.state = SQL_SCAN; |
| break; |
| |
| case SQL_IN_EXT_COMMENT: |
| |
| /* |
| * This isn't fully correct, because we don't account for |
| * nested slash-stars, but pg_dump never emits such. |
| */ |
| if (AH->sqlparse.lastChar == '*' && *qry == '/') |
| AH->sqlparse.state = SQL_SCAN; |
| break; |
| |
| case SQL_IN_SINGLE_QUOTE: |
| /* We needn't handle '' specially */ |
| if (*qry == '\'' && !AH->sqlparse.backSlash) |
| AH->sqlparse.state = SQL_SCAN; |
| else if (*qry == '\\') |
| AH->sqlparse.backSlash = !AH->sqlparse.backSlash; |
| else |
| AH->sqlparse.backSlash = false; |
| break; |
| |
| case SQL_IN_E_QUOTE: |
| |
| /* |
| * Eventually we will need to handle '' specially, because |
| * after E'...''... we should still be in E_QUOTE state. |
| * |
| * XXX problem: how do we tell whether the dump was made by a |
| * version that thinks backslashes aren't special in non-E |
| * literals?? |
| */ |
| if (*qry == '\'' && !AH->sqlparse.backSlash) |
| AH->sqlparse.state = SQL_SCAN; |
| else if (*qry == '\\') |
| AH->sqlparse.backSlash = !AH->sqlparse.backSlash; |
| else |
| AH->sqlparse.backSlash = false; |
| break; |
| |
| case SQL_IN_DOUBLE_QUOTE: |
| /* We needn't handle "" specially */ |
| if (*qry == '"') |
| AH->sqlparse.state = SQL_SCAN; |
| break; |
| |
| case SQL_IN_DOLLAR_TAG: |
| if (*qry == '$') |
| { |
| /* Do not add the closing $ to tagBuf */ |
| AH->sqlparse.state = SQL_IN_DOLLAR_QUOTE; |
| AH->sqlparse.minTagEndPos = AH->sqlBuf->len + AH->sqlparse.tagBuf->len + 1; |
| } |
| else if (_isDQChar(*qry, (AH->sqlparse.tagBuf->len == 1))) |
| { |
| /* Valid, so add to tag */ |
| appendPQExpBufferChar(AH->sqlparse.tagBuf, *qry); |
| } |
| else |
| { |
| /* |
| * Ooops, we're not really in a dollar-tag. Valid tag |
| * chars do not include the various chars we look for in |
| * this state machine, so it's safe to just jump from this |
| * state back to SCAN. We have to back up the qry pointer |
| * so that the current character gets rescanned in SCAN |
| * state; and then "continue" so that the bottom-of-loop |
| * actions aren't done yet. |
| */ |
| AH->sqlparse.state = SQL_SCAN; |
| qry--; |
| continue; |
| } |
| break; |
| |
| case SQL_IN_DOLLAR_QUOTE: |
| |
| /* |
| * If we are at a $, see whether what precedes it matches |
| * tagBuf. (Remember that the trailing $ of the tag was not |
| * added to tagBuf.) However, don't compare until we have |
| * enough data to be a possible match --- this is needed to |
| * avoid false match on '$a$a$...' |
| */ |
| if (*qry == '$' && |
| AH->sqlBuf->len >= AH->sqlparse.minTagEndPos && |
| strcmp(AH->sqlparse.tagBuf->data, |
| AH->sqlBuf->data + AH->sqlBuf->len - AH->sqlparse.tagBuf->len) == 0) |
| AH->sqlparse.state = SQL_SCAN; |
| break; |
| } |
| |
| appendPQExpBufferChar(AH->sqlBuf, *qry); |
| AH->sqlparse.lastChar = *qry; |
| } |
| |
| /* |
| * If we get here, we've processed entire bufferload with no complete SQL |
| * stmt |
| */ |
| return eos; |
| } |
| |
| |
| /* Convenience function to send one or more queries. Monitors result to handle COPY statements */ |
| int |
| ExecuteSqlCommandBuf(ArchiveHandle *AH, void *qryv, size_t bufLen) |
| { |
| char *qry = (char *) qryv; |
| char *eos = qry + bufLen; |
| |
| /* |
| * fprintf(stderr, "\n\n*****\n Buffer:\n\n%s\n*******************\n\n", |
| * qry); |
| */ |
| |
| /* Could switch between command and COPY IN mode at each line */ |
| while (qry < eos) |
| { |
| /* |
| * If libpq is in CopyIn mode *or* if the archive structure shows we |
| * are sending COPY data, treat the data as COPY data. The pgCopyIn |
| * check is only needed for backwards compatibility with ancient |
| * archive files that might just issue a COPY command without marking |
| * it properly. Note that in an archive entry that has a copyStmt, |
| * all data up to the end of the entry will go to _sendCopyLine, and |
| * therefore will be dropped if libpq has failed to enter COPY mode. |
| * Also, if a "\." data terminator is found, anything remaining in the |
| * archive entry will be dropped. |
| */ |
| if (AH->pgCopyIn || AH->writingCopyData) |
| qry = _sendCopyLine(AH, qry, eos); |
| else |
| qry = _sendSQLLine(AH, qry, eos); |
| } |
| |
| return 1; |
| } |
| |
| void |
| StartTransaction(ArchiveHandle *AH) |
| { |
| ExecuteSqlCommand(AH, "BEGIN", "could not start database transaction"); |
| } |
| |
| void |
| CommitTransaction(ArchiveHandle *AH) |
| { |
| ExecuteSqlCommand(AH, "COMMIT", "could not commit database transaction"); |
| } |
| |
| static bool |
| _isIdentChar(unsigned char c) |
| { |
| if ((c >= 'a' && c <= 'z') |
| || (c >= 'A' && c <= 'Z') |
| || (c >= '0' && c <= '9') |
| || (c == '_') |
| || (c == '$') |
| || (c >= (unsigned char) '\200') /* no need to check <= \377 */ |
| ) |
| return true; |
| else |
| return false; |
| } |
| |
| static bool |
| _isDQChar(unsigned char c, bool atStart) |
| { |
| if ((c >= 'a' && c <= 'z') |
| || (c >= 'A' && c <= 'Z') |
| || (c == '_') |
| || (!atStart && c >= '0' && c <= '9') |
| || (c >= (unsigned char) '\200') /* no need to check <= \377 */ |
| ) |
| return true; |
| else |
| return false; |
| } |