Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use snprintf instead of sprintf (even in trivial case) #1236

Merged
merged 1 commit into from
Oct 27, 2022

Conversation

eddelbuettel
Copy link
Member

This commit replace the sole use sprintf with snprintf. A full run of reverse depends checks revealed no 'change to worse'.

Checklist

  • Code compiles correctly
  • R CMD check still passes all tests
  • Prefereably, new tests were added which fail without the change
  • Document the changes by file in ChangeLog

@kevinushey
Copy link
Contributor

snprintf() always writes a null terminator as well, so I think you can safely pass the full buffer size when calling it (no -1).

return buffer ;
}
template <>
inline const char* coerce_to_string<RAWSXP >(Rbyte from){
static char buff[3];
::sprintf(buff, "%02x", from);
snprintf(buff, 2, "%02x", from);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a unit test for this one? I'm worried we might be truncating the representation here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would welcome one. Can you think of one?

I did run it over nearly 2600 CRAN packages though as the usual reverse depends check...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try to put one together later this morning.

@kevinushey
Copy link
Contributor

Here's an example showcasing the issue:

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
std::string test() {
    return Rcpp::internal::coerce_to_string<RAWSXP>(0xaa);
}

/*** R
test()
*/

Running with this PR, you'll see:

> test()
[1] "a"

Given that this change doesn't cause any failures I guess this isn't a commonly-exercised code path.

@eddelbuettel
Copy link
Member Author

Whoops. Scrambled egg on my face.

@kevinushey kevinushey mentioned this pull request Oct 27, 2022
4 tasks
@eddelbuettel
Copy link
Member Author

eddelbuettel commented Oct 27, 2022

snprintf() always writes a null terminator as well, so I think you can safely pass the full buffer size when calling it (no -1).

I was a little fuzzy / unaware on this one but do recall seeing many use of -1 in the wild. Including in the same source file a few lines above:

static char tmp[128];
snprintf(tmp, 127, "%f", x);

but then not here (so I changed it in the PR -- the link shown is from the master branch)

static char buffer[NB] ;
snprintf( buffer, NB, "%*d", integer_width(from), from );

What is a good rule about when 'n == n' is safe and when it is not?

@eddelbuettel
Copy link
Member Author

Rebased and updated. Let me know what you think @kevinushey

@kevinushey
Copy link
Contributor

What is a good rule about when 'n == n' is safe and when it is not?

Good question... it's complicated and comes down to whether the format / print routine writes a null terminator for you or not. Since snprintf() always writes a null terminator, it's safe to use n == n where the buffer includes space for the null terminator, but for other formatting functions the documentation needs to be checked to be sure.

Another wrinkle for snprintf() is that, if you're using it to figure out the buffer size when printing, you need to be careful. From https://en.cppreference.com/w/cpp/io/c/fprintf:

  1. Writes the results to a character string buffer. At most buf_size - 1 characters are written. The resulting character string will be terminated with a null character, unless buf_size is zero. If buf_size is zero, nothing is written and buffer may be a null pointer, however the return value (number of bytes that would be written not including the null terminator) is still calculated and returned.

So you might write code like:

// figure out size of formatted string
// NOTE: does _not_ include null terminator!
int n = snprintf(nullptr, 0, "%s\n", "code");

// create a buffer, adding 1 for the null terminator
std::vector<char> buffer(n + 1);

// format string into buffer, again adding 1 for terminator
snprintf(&buffer[0], n + 1, "%s\n", "code");

tl;dr: it's unfortunately complicated and depends on whether the formatter writes a null terminator for you, and in what conditions it writes the null terminator.

@kevinushey
Copy link
Contributor

This is also why I am so happy to have fmt.

Copy link
Contributor

@kevinushey kevinushey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@eddelbuettel
Copy link
Member Author

Thanks for the comment above. Reading it I realize I once knew only n-1 may be copied to leave room. So yes, 'it depends'.

Anyway, we made a nice improvement and you rightly took me to school as there shouldn't be a PR without a test under it. All good.

@eddelbuettel eddelbuettel merged commit c05fd1b into master Oct 27, 2022
@eddelbuettel eddelbuettel deleted the feature/no_sprintf branch October 27, 2022 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants