Gary
Gary

Reputation: 2859

Novice C question concerning building strings using pointers in sprintf and snprintf

In C, if j is a pointer to the first position of the numerical representation of a date held in a portion of a JSON string such as "{\"date\":1605924795664}" held in a character array (thus, pointing to "1"), in the code below, is there a way to build the string in sprintf using the pointer j without having to first copy the date to character array d? Or is it best to just copy it and use it in the sprintf expression as is?

int build_schema( char *j )
  {
    char d[14];
    char sql[380] = ...expression of length 322;
    int p;
    for ( p = 0; p < 13; p++ ) { d[p] = *(j+p); }
    d[p] = '\0';
    p = 321;
    p += sprintf( sql + p, "%s,\"e\":%s,\"m\":0,\"g\":[],\"q\":[]}');", d, d );
    /* ... */
    return 0;
  }

I tried this using snprintf and the pointer only without copying. It worked in litte tests when j was of length 13 not counting the \0; but when j points to the address of a character in the midst of a string with more than 13 characters remaining, snprintf returns -1 instead of 13. I'm using the GCC compiler.

p += snprintf( sql + p, 13, "%s", j );  
p += sprintf( sql + p, "%s", ",\"e\":" );
p += snprintf( sql + p, 13, "%s", j );
p += sprintf( sql + p, "%s", ",\"m\":0,\"g\":[],\"q\":[]}');" );

I don't understand that. I thought -1 was returned only if there wasn't enough space in sql to hold the additional 13 characters, not if j was larger than 13. Would you please tell me what I am doing wrong?

Thank you.


Here is the test code I ran. Thanks.

When *j is "1605924795664" the values of p are: 334, 339, 352, 376.

When *j is "1605924795664 abc" the values of p are: 320, 325, 324, 348.

Note that p starts of at 321. You can see that it decreases by 1 after each attempt to write j when *j is longer than 13.

#include <stdio.h>
void build_schema( char * );
char *j = "1605924795664 abc";
int main( void )
 { build_schema( j ); return 0; }

void build_schema( char *j )
  {
    int p = 321;
    char sql[ 380 ] = "BEGIN EXCLUSIVE;"
                      "create table maps( key integer primary key, portfolio integer, module integer, tab integer, page integer, map text );"
                      "create table media( key integer primary key, note text, entry integer, data text );"
                      "create table tags( key integer primary key, desc text );"
                      "insert into maps values(NULL,0,0,0,0,'{\"k\":1,\"c\":'"; // Length 326 (less four '\' escape characters) = 322.

    p += snprintf( sql + p, 13, "%s", j );  
    printf( "p : %d\nsql: %s\n", p, sql );
    p += sprintf( sql + p, "%s", ",\"e\":" );
    printf( "p : %d\nsql: %s\n", p, sql );
    p += snprintf( sql + p, 13, "%s", j );
    printf( "p : %d\nsql: %s\n", p, sql );
    p += sprintf( sql + p, "%s", ",\"m\":0,\"g\":[],\"q\":[]}');" );
    printf( "p : %d\nsql: %s\n", p, sql );
  }

I learned some important things through this question. Following the answer from @dxiv, the full expression I was trying to build can be done as:

q = snprintf( sql + p,          
              sizeof(sql) - p,  
              "%.*s%s%.*s%s",   
              len, j,
              ",\"e\":",
              len, j, 
              ",\"m\":0,\"g\":[],\"q\":[]}" );  

Regarding the difference in the return value, apparently the minGW-W64 bug issue is still present or it uses the glibc 2.0.6 version or earlier, I don't know, but there is a note that Until glibc 2.0.6, they would return -1.

Interesting though, if the expression above for q is used, and the size of sql reduced to 370 to cause a truncation, the result of

printf( "q: %d, p: %d, sizeof(sql): %d, sizeof(sql)-p-1: %d\n", q, p, sizeof(sql), sizeof(sql)-p-1 );

is q: -1, p: 321, sizeof(sql): 370, sizeof(sql)-p-1: 48

yet the expression if ( q > sizeof(sql) - p - 1 ) evaluates to true and the error message of *** error: -1 char string too long is printed.

So, I don't understand what is going on concerning the return value in minGW-W64 when run out of space, for it prints -1 but evaluates to greater than 48.

Regardless, this answers my question and solves my problem. To be safe, the test on q could be modified to include q < 0.

Thanks again for all the help.

Upvotes: 0

Views: 84

Answers (1)

dxiv
dxiv

Reputation: 17648

Simplifying the original code to just the first snprintf + sprintf:

int build_schema( char *j )
  {
    printf("j = '%s', strlen(j) = %zu\n", j, strlen(j));

    char sql[380];
    int p = 321, q;

    memset(sql, '_', sizeof(sql));

    q = snprintf( sql + p, 13, "%s", j );  // <-- writes 12 chars + nul terminator
                                           //     returns 14 == strlen(j)
    printf("snprintf = %d\n", q);
    p += q;
    p += sprintf( sql + p, "%s", ",\"e\":" );

    printf("sql + 321 = '%s\\%d%c%s'\n", sql + 321, (sql + 321)[12], (sql + 321)[13], sql + 321 + 14);

    return 0;
}

int main()
{
    char json[] = "{\"date\":1605924795664}";
    build_schema(json + 8);
}

Output, with explanations inserted below:

j = '1605924795664}', strlen(j) = 14
snprintf = 14
sql + 321 = '160592479566\0_,"e":'

             ^          ^ ^^^
             |          | |||
     sql + 321          | |||
                        | |||
   12 chars from snprintf |||
                          |||
   nul term from snprintf |||
                           ||
    '_' left from sql buffer|
                            |
         5 chars from sprintf

Fixed code:

int build_schema( char *j )
  {
    char sql[380];
    int p = 321, q;

    snprintf( sql + p, 14, "%s", j );  
    p += 13;
    p += sprintf( sql + p, "%s", ",\"e\":" );

    printf("sql + 321 = '%s'\n", sql + 321);

    return 0;
}

int main()
{
    char json[] = "{\"date\":1605924795664}";
    build_schema(json + 8);
}

Output:

sql + 321 = '1605924795664,"e":'

[ EDIT ]   The above explains the issue, and the "fixed code" shows the minimal change to fix it. However, it does not address the root cause of the problem, which is (mis)using the bufsize parameter of snprintf to control the width of a string argument.

Instead, bufsize should be used the way it was meant to, for preventing buffer overruns, and the string width should be controlled with the "%.*s syntax for the precision specifier. Sample such code below, with the same correct 1605924795664,"e": output.

#include <stdio.h>
#include <string.h>

int build_schema( char *j, int len )
  {
    char sql[380];
    int p = 321, q;

    q = snprintf( sql + p,          // write to `sql` buffer starting at offset 321
                  sizeof(sql) - p,  // 59 chars left in buffer, do not overrun
                  "%.*s%s",         // write 'len' chars from 'j', then 2nd string
                  len, j,
                  ",\"e\":" );  

    if( q > sizeof(sql) - p - 1)
    {
        printf("*** error: %d char string too long\n", q);
        return -1;
    }

    printf("sql + 321 = '%s'\n", sql + 321);

    return 0;
}

int main()
{
    char json[] = "{\"date\":1605924795664}";
    build_schema(json + 8,  // string starting at `1`
                 13);       // excluding the trailing `}`
}

Upvotes: 1

Related Questions