While I was working on bringing some of my antique code into the future, I found this question regarding buffering and the Private Profile API. After my own experimentation and research, I can confirm the asker's original statement regarding the inability to determine the difference between when the string is exactly nSize - 1 or when the buffer is too small.
Is there a better way? The accepted answer from Mike says there isn't according to the documentation and you should just try to make sure the buffer is large enough. Marc says to grow the buffer. Roman says the check error codes. Some random user says you need to provide a buffer large enough and, unlike Marc, proceeds to show some code that expands his buffer.
Is there a better way? Lets get the facts!
Due to the age of the ProfileString API, because none of the tags to this question regard any particular language and for easy readability, I've decided to show my examples using VB6. Feel free to translate them for your own purposes.
GetPrivateProfileString Documentation
According to the GetPrivateProfileString documentation, these Private Profile functions are provided only for compatibility with 16-bit Windows-based applications. This is great information because it allows us to understand the limitations of what these API functions can do.
A 16 bit signed integer has a range of −32,768 to 32,767 and an unsigned 16 bit integer has a range of 0 to 65,535. If these functions are truly made for use in a 16 bit environment, its highly probable that any numbers we encounter will be restricted to one of these two limits.
The documentation states that every string returned will end with a null character and also says a string which doesn't fit into the supplied buffer will be truncated and terminated with a null character. Therefore, if a string does fit into the buffer the second last character will be null as well as the last character. If only the last character is null then the extracted string is exactly the same length of the supplied buffer - 1 or the buffer was not large enough to hold the string.
In either situation where the second last character is not null, the extracted string being the exact length or too large for the buffer, GetLastError will return error number 234 ERROR_MORE_DATA (0xEA) giving us no way to differentiate between them.
What is the maximum buffer size accepted by GetPrivateProfileString?
While the documentation doesn't state the maximum buffer size, we already know this API was designed for a 16-bit environment. After a little experimentation, I was able to conclude that the maximum buffer size is 65,536. If the string in the file is larger than 65,535 characters long we start to see some strange behaviour while trying to read the string. If the string in the file is 65,536 characters long the retrieved string will be 0 characters long. If the string in the file is 65,546 characters long the retrieved string will be 10 characters long, end with a null character and be truncated from the very beginning of the string contained within the file. The API will write a string larger than 65,535 characters but will not be able to read anything larger than 65,535 characters. If the buffer length is 65,536 and the string in the file is 65,535 characters long, the buffer will contain the string from the file and also end in a single null character.
This provides us with our first, albeit not perfect solution. If you want to always make sure your first buffer is large enough, make that buffer 65,536 characters long.
Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long
Public Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String) As String
On Error GoTo iniReadError
Dim Buffer As String
Dim Result As Long
Buffer = String$(65536, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, 65536, Pathname)
If Result <> 0 Then
iniRead = Left$(Buffer, Result)
Else
iniRead = Default
End If
iniReadError:
End Function
Now that we know the maximum buffer size, we can use the size of the file to revise it. If the size of your file is less than 65,535 characters long there may be no reason to create a buffer so large.
In the remarks section of the documentation it says a section in the initialization file must have the following form:
[section]
key=string
We can assume that each section contains two square brackets and an equal sign. After a small test, I was able to verify that the API will accept any kind of line break between the section and key (vbLf , vbCr Or vbCrLf / vbNewLine). These details and the lengths of the section and key names will allow us to narrow the maximum buffer length and also ensure the file size is large enough to contain a string before we attempt to read the file.
Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long
Public Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String) As String
On Error Resume Next
Dim Buffer_Size As Long
Err.Clear
Buffer_Size = FileLen(Pathname)
On Error GoTo iniReadError
If Err.Number = 0 Then
If Buffer_Size > 4 + Len(Section) + Len(Key) Then
Dim Buffer As String
Dim Result As Long
Buffer_Size = Buffer_Size - Len(Section) - Len(Key) - 4
If Buffer_Size > 65535 Then
Buffer_Size = 65536
Else
Buffer_Size = Buffer_Size + 1
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
If Result <> 0 Then
iniRead = Left$(Buffer, Result)
Exit Function
End If
End If
End If
iniRead = Default
iniReadError:
End Function
Growing the Buffer
Now that we've tried really hard to make sure the first buffer is large enough and we have a revised maximum buffer size, it still might make more sense for us to start with a smaller buffer and gradually increase the size of the buffer to create a buffer large enough that we can extract the entire string from the file. According to the documentation, the API creates the 234 error to tell us there's more data available. It makes a lot of sense that they use this error code to tell us to try again with a larger buffer. The downside to retrying over and over again is that its more costly. The longer the string in the file, the more tries required to read it, the longer its going to take. 64 Kilobytes isn't a lot for today's computers and today's computers are pretty fast, so you may find either of these examples fit your purposes regardless.
I've done a fair bit of searching the GetPrivateProfileString API, and I've found that typically when someone without extensive knowledge of the API tries to create a large enough buffer for their needs, they choose a buffer length of 255. This would allow you to read a string from the file up to 254 characters long. I'm not sure why anybody started using this but I would assume someone somewhere imagined this API using a string where the buffer length is limited to an 8-bit unsigned number. Perhaps this was a limitation of WIN16.
I'm going to start my buffer low, 64 bytes, unless the maximum buffer length is less, and quadruple the number either up to the maximum buffer length or 65,536. Doubling the number would also be acceptable, a larger multiplication means less attempts at reading the file for larger strings while, relatively speaking, some medium length strings might have extra padding.
Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long
Public Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String) As String
On Error Resume Next
Dim Buffer_Max As Long
Err.Clear
Buffer_Max = FileLen(Pathname)
On Error GoTo iniReadError
If Err.Number = 0 Then
If Buffer_Max > 4 + Len(Section) + Len(Key) Then
Dim Buffer As String
Dim Result As Long
Dim Buffer_Size As Long
Buffer_Max = Buffer_Max - Len(Section) - Len(Key) - 4
If Buffer_Max > 65535 Then
Buffer_Max = 65536
Else
Buffer_Max = Buffer_Max + 1
End If
If Buffer_Max < 64 Then
Buffer_Size = Buffer_Max
Else
Buffer_Size = 64
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
If Result <> 0 Then
If Buffer_Max > 64 Then
Do While Result = Buffer_Size - 1 And Buffer_Size < Buffer_Max
Buffer_Size = Buffer_Size * 4
If Buffer_Size > Buffer_Max Then
Buffer_Size = Buffer_Max
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
Loop
End If
iniRead = Left$(Buffer, Result)
Exit Function
End If
End If
End If
iniRead = Default
iniReadError:
End Function
Improved Validation
Depending on your implementation, improving the validation of your pathname, section and key names may prevent you from needing to prepare a buffer.
According to Wikipedia's INI File page, they say:
In the Windows implementation the key cannot contain the characters
equal sign ( = ) or semi colon ( ; ) as these are reserved characters.
The value can contain any character.
and
In the Windows implementation the section cannot contain the character
closing bracket ( ] ).
A quick test of the GetPrivateProfileString API proved this to be only partially true. I had no issues with using a semi colon within a key name so long as the semi colon was not at the very beginning. They don't mention any other limitations in the documentation or on Wikipedia although there might be more.
Another quick test to find the maximum length of a section or key name accepted by GetPrivateProfileString gave me a limit of 65,535 characters. The effects of using a string larger than 65,535 characters were the same as I had experienced while testing the maximum buffer length. Another test proved that this API will accept a blank string for either the section or key name. According to the functionality of the API, this is an acceptable initialization file:
[]
=Hello world!
According to Wikipedia, interpretation of whitespace varies. After yet another test, the Profile String API is definitely stripping whitespace from section and key names so its probably okay if we do it too.
Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long
Public Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String) As String
On Error Resume Next
If Len(Pathname) <> 0 Then
Key = Trim$(Key)
If InStr(1, Key, ";") <> 1 Then
Section = Trim$(Section)
If Len(Section) > 65535 Then
Section = RTrim$(Left$(Section, 65535))
End If
If InStr(1, Section, "]") = 0 Then
If Len(Key) > 65535 Then
Key = RTrim$(Left$(Key, 65535))
End If
If InStr(1, Key, "=") = 0 Then
Dim Buffer_Max As Long
Err.Clear
Buffer_Max = FileLen(Pathname)
On Error GoTo iniReadError
If Err.Number = 0 Then
If Buffer_Max > 4 + Len(Section) + Len(Key) Then
Dim Buffer As String
Dim Result As Long
Dim Buffer_Size As Long
Buffer_Max = Buffer_Max - Len(Section) - Len(Key) - 4
If Buffer_Max > 65535 Then
Buffer_Max = 65536
Else
Buffer_Max = Buffer_Max + 1
End If
If Buffer_Max < 64 Then
Buffer_Size = Buffer_Max
Else
Buffer_Size = 64
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
If Result <> 0 Then
If Buffer_Max > 64 Then
Do While Result = Buffer_Size - 1 And Buffer_Size < Buffer_Max
Buffer_Size = Buffer_Size * 4
If Buffer_Size > Buffer_Max Then
Buffer_Size = Buffer_Max
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
Loop
End If
iniRead = Left$(Buffer, Result)
Exit Function
End If
End If
End If
iniRead = Default
End If
End If
End If
End If
iniReadError:
End Function
Static Length Buffer
Sometimes we need to store variables that have a maximum length or a static length. A username, phone number, colour code or IP address are examples of strings where you might want to limit the maximum buffer length. Doing so when necessary will save you time and energy.
In the code example below, Buffer_Max will be limited to Buffer_Limit + 1. If the limit is greater than 64, we will begin with 64 and expand the buffer just as we did before. Less than 64 and we will only read once using our new buffer limit.
Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long
Public Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String, Optional Buffer_Limit As Long = 65535) As String
On Error Resume Next
If Len(Pathname) <> 0 Then
Key = Trim$(Key)
If InStr(1, Key, ";") <> 1 Then
Section = Trim$(Section)
If Len(Section) > 65535 Then
Section = RTrim$(Left$(Section, 65535))
End If
If InStr(1, Section, "]") = 0 Then
If Len(Key) > 65535 Then
Key = RTrim$(Left$(Key, 65535))
End If
If InStr(1, Key, "=") = 0 Then
Dim Buffer_Max As Long
Err.Clear
Buffer_Max = FileLen(Pathname)
On Error GoTo iniReadError
If Err.Number = 0 Then
If Buffer_Max > 4 + Len(Section) + Len(Key) Then
Dim Buffer As String
Dim Result As Long
Dim Buffer_Size As Long
Buffer_Max = Buffer_Max - Len(Section) - Len(Key) - 4
If Buffer_Limit > 65535 Then
Buffer_Limit = 65535
End If
If Buffer_Max > Buffer_Limit Then
Buffer_Max = Buffer_Limit + 1
Else
Buffer_Max = Buffer_Max + 1
End If
If Buffer_Max < 64 Then
Buffer_Size = Buffer_Max
Else
Buffer_Size = 64
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
If Result <> 0 Then
If Buffer_Max > 64 Then
Do While Result = Buffer_Size - 1 And Buffer_Size < Buffer_Max
Buffer_Size = Buffer_Size * 4
If Buffer_Size > Buffer_Max Then
Buffer_Size = Buffer_Max
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
Loop
End If
iniRead = Left$(Buffer, Result)
Exit Function
End If
End If
End If
iniRead = Default
End If
End If
End If
End If
iniReadError:
End Function
Using WritePrivateProfileString
To ensure there are no issues reading a string using GetPrivateProfileString, limit your strings to 65,535 or less characters long before using WritePrivateProfileString. Its also a good idea to include the same validations.
Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long
Private Declare Function WritePrivateProfileString Lib "kernel32" Alias "WritePrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpString As Any, ByVal lpFileName As String) As Long
Public Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String, Optional Buffer_Limit As Long = 65535) As String
On Error Resume Next
If Len(Pathname) <> 0 Then
Key = Trim$(Key)
If InStr(1, Key, ";") <> 1 Then
Section = Trim$(Section)
If Len(Section) > 65535 Then
Section = RTrim$(Left$(Section, 65535))
End If
If InStr(1, Section, "]") = 0 Then
If Len(Key) > 65535 Then
Key = RTrim$(Left$(Key, 65535))
End If
If InStr(1, Key, "=") = 0 Then
Dim Buffer_Max As Long
Err.Clear
Buffer_Max = FileLen(Pathname)
On Error GoTo iniReadError
If Err.Number = 0 Then
If Buffer_Max > 4 + Len(Section) + Len(Key) Then
Dim Buffer As String
Dim Result As Long
Dim Buffer_Size As Long
Buffer_Max = Buffer_Max - Len(Section) - Len(Key) - 4
If Buffer_Limit > 65535 Then
Buffer_Limit = 65535
End If
If Buffer_Max > Buffer_Limit Then
Buffer_Max = Buffer_Limit + 1
Else
Buffer_Max = Buffer_Max + 1
End If
If Buffer_Max < 64 Then
Buffer_Size = Buffer_Max
Else
Buffer_Size = 64
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
If Result <> 0 Then
If Buffer_Max > 64 Then
Do While Result = Buffer_Size - 1 And Buffer_Size < Buffer_Max
Buffer_Size = Buffer_Size * 4
If Buffer_Size > Buffer_Max Then
Buffer_Size = Buffer_Max
End If
Buffer = String$(Buffer_Size, vbNullChar)
Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)
Loop
End If
iniRead = Left$(Buffer, Result)
Exit Function
End If
End If
End If
iniWrite Pathname, Section, Key, Default
iniRead = Default
End If
End If
End If
End If
iniReadError:
End Function
Public Function iniWrite(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, ByVal Value As String) As Boolean
On Error GoTo iniWriteError
If Len(Pathname) <> 0 Then
Key = Trim$(Key)
If InStr(1, Key, ";") <> 1 Then
Section = Trim$(Section)
If Len(Section) > 65535 Then
Section = RTrim$(Left$(Section, 65535))
End If
If InStr(1, Section, "]") = 0 Then
If Len(Key) > 65535 Then
Key = RTrim$(Left$(Key, 65535))
End If
If InStr(1, Key, "=") = 0 Then
If Len(Value) > 65535 Then Value = Left$(Value, 65535)
iniWrite = WritePrivateProfileString(Section, Key, Value, Pathname) <> 0
End If
End If
End If
End If
iniWriteError:
End Function
ERROR_MORE_DATA
when they fill the buffer completely (even without truncating the data). – Muley