6

C standard library has strncpy function, declared as:

char *strncpy(char *dest, const char *src, size_t n);

It's a strange beast, as it fills n bytes of memory pointed to by dest. It does this by first copying from string src as much as it can (either n bytes copied or end of src reached), then if n bytes isn't full yet, it fills the rest with 0 bytes.

So, it wipes the entire dest overwriting any old data or uninitialized garbage there. Also, it does not necessarily produce a string in dest, because if strlen(src) is equal or longer than n, no terminating 0 byte is written. So, for copying NUL-terminated strings, it often does extra work with the 0 padding, unlike other string functions. And then it requires extra code to ensure result is a string (such as strncpy(dst, src, 10); dst[10]=0;). So it looks as if it was not designed for simply copying C strings at all.

What was the original purpose of strncpy function? Why does work the way it does?


I have two speculations, which I'll list here just as an example of what kind of confirmed answer I'm hoping to get:

  • Maybe some earlier language back then had diffrent kind of strings (such as with length prefix), and for them strncpy worked somehow more logically as a string copy. Then Maybe C copied the function without altering behavior to account for terminating NUL.
  • Maybe at that time, before appearance of all the modern database alternatives, it was common to have binary data files with fixed size records, where C structs were accessed with fseek, followed by fwrite/fread. When setting fixed size string in such a struct, you'd want any garbage overwritten with 0, and you wouldn't want to waste precious storage space by always having one extra zero.

I don't know where an answer could be found, but perhaps some old reference manuals or textbooks from 1960's or 1970's might discuss the intended use.

8
  • 1
    Could you help us understand what you find so strange about this? The 0-initialization that strncpy does over strcpy is great. To not do so would be a "short write vulnerability" See developer.apple.com/library/archive/documentation/Security/… for details
    – Alexander
    Apr 13, 2022 at 2:01
  • 1
    The fact that it creates a not-a-c-string when the buffer is not large enough makes it unusable. The fact that copying long utf-8 can result in illegal utf-8 is equally bad.
    – gnasher729
    Apr 13, 2022 at 6:37
  • 3
    @gnasher729: The purpose of strncopy is to convert from a C-style string to a fixed-length record. In other words: the fact that the result isn't a C-style string is the whole point of this function in the first place. The same reason applies to your second criticism: if you convert a C-style string contain UTF-8 to a fixed-length record, there is no guarantee that the record boundary lines up with a character boundary (or a grapheme cluster boundary or a glyph boundary or any other kind of text boundary). Besides, strncopy predates UTF-8 by well over a decade, so making strncopy aware Apr 13, 2022 at 6:58
  • 3
    … of UTF-8 would have required prescience and/or time travel. Apr 13, 2022 at 6:58
  • 1
    Although unfortunately the accepted answer is poor, and you should probably look at this one instead: stackoverflow.com/a/1454071/212858
    – Useless
    Apr 13, 2022 at 12:56

4 Answers 4

7

A good source for answering design questions regarding C is the rationale document published by X3J11, the ANSI technical committee that first standardized the C language: "Rationale for the ANSI C Programming Language", Silicon Press 1990. Here we find:

4.11.2.4 The strncpy function
strncpy was initially introduced into the C library to deal with fixed-length name fields in structures such as directory entries. Such fields are not used in the same way as strings: the trailing null is unnecessary for a maximum-length field, and setting trailing bytes for shorter names to null assures efficient field-wise comparisons. strncpy is not by origin a "bounded strcpy," and the Committee has preferred to recognize existing practice rather than alter the function to better suit it to such use.

Fun fact from my brief time looking at optimizations for high-performance business computing in the early 2000s: In many real-life use cases of strncpy the typical length of the source string is significantly shorter than the width of the destination field, yet many implementations of strncpy did not optimize the zero-fill portion of its functionality, using a simple loop zeroing one byte at a time. As I recall I prepared an assembly-language level implementation for Solaris that included this missing optimization. I see that the OpenSolaris repository includes what looks very much like my code.

1
  • Excellent quote, just what I was after!
    – hyde
    Apr 16, 2022 at 18:06
2

Maybe at that time, before appearance of all the modern database alternatives, it was common to have binary data files with fixed size records

It's still common to have binary data formats with fixed-size records in low-level code. Many protocols designed mainly for fast (de-)serialization do exactly this.

And I still, when implementing those protocols, find myself using strncpy if they're null-padded, or writing an equivalent generalization if they're space-padded instead.

So, I think you've correctly understood the use case, and you're just mistaken in believing that use case no longer applies because you happen to be working at a higher level of abstraction.

1
  • I didn't mean to imply, that the use case has disappeared (though I'd say it's more robust to first zero an entire struct instead of individual fields), but rather wonder if that was the original purpose of the function.
    – hyde
    Apr 13, 2022 at 16:20
2

Let's say one needs to store a large amount of short strings in some preallocated array or buffer, all of the same maximum length N. Using a classic C string will require to reserve N+1 bytes per string. But by using the knowledge about the maximum length, one can obviously save one byte per string in the array (strings of length smaller than N get \0 at the end, strings of length N not).

Copying a standard C string into such a buffer is exactly what strncopy does (with the extra bonus of zero padding all unused bytes).

Are there many use cases for short strings with a maximum length? Absolutely, even today! Not just for filenames in old operating systems (like Raymond Chen mentioned here as a use case for strncpy in Unix System V, or gnasher729 mentioned about MS DOS). Out of my head:

  • operating systems or databases with user names allowing a certain maximum length (for example IBM's DB2 had a user name limit of 8 in the past, earlier Oracle versions had a limit of 30). Certain Unix versions still have a user name limit of 32.

  • other database identifiers, like table / view / index names had typically a size restriction, especially in older databases

  • date strings like "1/1/1980" to "12/12/2022" may vary in length between 8 and 10 characters at maximum. Same holds for time strings.

  • postal codes should not exceed more than 10 characters internationally (restricted to certain countries, a smaller length will apply)

  • international country codes

  • car license plate codes in lots of countries.

  • IPV4 adresses encoded as decimal strings from "0.0.0.0" to "111.222.333.444". Though it would probably more space efficient to save them just in a 32 bit word, there might be cases where the above notation might be preferred.

But is the saved byte actually worth the hassle it causes? Well, that's definitely case dependend, and today the answer would proabably "no" in most practical cases. But in the 1970s and 80s, memory was a lot more expensive than today, network bandwidth also, and security was not such an issue as it is today. That was the time when strncpy was introduced into the C standard lib, by people who had probably several of the above use cases in mind.

Addendum: I almost forgot about the #1 use case where I actually had to work with strncpyseveral hundred times : Embedded SQL (for C). When you declare a host variable of type char[10] within the DECLARE section, then - in all ESQL dialects I have seen in the past - it can be used for strings of length up to length 10 (and not just 9 and a trailing \0) . The ESQL precompiler makes sure that strings not containing \0 are processed correctly. And when you are in the situation to pass a standard C string (usually a function parameter) into a char[10] vector, then strncpy is the right tool for the job.

Of course, ESQL was probably not the reason for having strncpy in the C standard lib, but it shows there are way more use cases for this function than one might expect at a first glance, and the C standard lib inventors had probably foreseen several of them.

-2

My guess is that it was perfect to fill some memory structure with 8 bytes for a file name and 3 bytes extension, like this:

char nameext[11];
strncpy(&nameext[0], name, 8);
strncpy(&nameext[8], ext, 3);

Exactly what DOS needed. I can’t think of anything else where it would be useful.

Against buffer overflows, in my C days I wrote a function mprintf, same arguments as printf, but it would allocate a string for the result using malloc and return that, so you could write

char* str1 = …;
char* str2 = …;
int x = …;
char *result = mprintf(“%s,%s,%d”, str1, str2, x);

without having to calculate buffer sizes or checking for buffer overflows.

3
  • Thanks for the answer, but I'm after facts as an answer, not speculation and guesses.
    – hyde
    Apr 13, 2022 at 6:59
  • Related to your mprintf, there's also non-standard (also not POSIX) GNU asprintf: man7.org/linux/man-pages/man3/asprintf.3.html
    – hyde
    Apr 13, 2022 at 7:05
  • 2
    @hyde: though this answer misses that C came more from the Unix world, according to Raymond Chen's blog post from 2005, it may not be completely off-track, since in certain old Unix implementations there were similar file name restrictions, and strncpy seems to be a suitable string copy function for them.
    – Doc Brown
    Apr 13, 2022 at 11:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.