While I was working on moving some of my antique code into the future, I found this question regarding buffering and the Private Profile API. After my own experiments and research, I can confirm the original statement of the author of the question of the inability to determine the difference between when the line is exactly equal to nSize - 1 or when the buffer is too small.
Is there a better way? Mike's accepted answer says that this is not consistent with the documentation, and you just need to make sure that the buffer is large enough. Mark says to increase the buffer. Roman says check error codes. Some random users say that you need to provide a sufficiently large buffer and, unlike Mark, continues to show some code that expands its buffer.
Is there a better way? Let's find out the facts!
Due to the age of the ProfileString API, since none of the tags for this question apply to any particular language, and for readability, I decided to show my examples using VB6. Feel free to translate them for your own purposes.
GetPrivateProfileString Documentation
According to GetPrivateProfileString documentation , these private profile features are provided only for compatibility with 16-bit Windows applications. This is great information because it allows us to understand the limitations of what these API functions can do.
A signed 16-bit integer ranges from -32,768 to 32,767, and an unsigned 16-bit integer ranges from 0 to 65,535. If these functions are really intended for use in a 16-bit environment, it is very likely that any numbers we encounter will be limited to one of these two limits.
The documentation says that each returned string will end with a null character, and it also says that a string that does not fit in the provided buffer will be truncated and end with a null character. Therefore, if a string is buffered, the last last character will be zero, as well as the last character. If only the last character is zero, then the extracted string is exactly the same length as the provided buffer - 1, or the buffer was not large enough to hold the string.
In any situation where the second last character is not equal to zero, and the extracted string is the exact length or too large for the buffer, GetLastError will return an error number 234 ERROR_MORE_DATA (0xEA), which will not allow us to distinguish between them.
What is the maximum buffer size accepted by GetPrivateProfileString?
Although the documentation does not specify a maximum buffer size, we already know that this API was designed for a 16-bit environment. After some experimentation, I was able to conclude that the maximum buffer size is 65,536 . If the line length in the file exceeds 65,535 characters, we begin to see strange behavior when we try to read the line. If the line length in the file is 65,536 characters, the resulting string will be 0 characters long. If the line length in the file is 65,546 characters, the resulting line will be 10 characters long, end with a zero character and will be truncated from the very beginning of the line contained in the file. The API will write a string larger than 65,535 characters, but cannot read anything more than 65,535 characters. If the buffer length is 65,536 and the line length in the file is 65,535 characters, the buffer will contain a line from the file and will also end with one null character.
This gives us the first, although not perfect, solution. If you want to always make sure that your first buffer is large enough, make this buffer length of 65,536 characters.
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 file size to review it. If your file size is less than 65,535 characters, there may be no reason to create such a large buffer.
The documentation comments section says that the section in the initialization file should have the following form:
[section]
key = string
It can be assumed that each section contains two square brackets and an equal sign. After a little test, I was able to make sure that the API accepts any type of line break between the section and the key (vbLf, vbCr or vbCrLf / vbNewLine). These details, as well as the lengths of sections and key names, will allow us to narrow the maximum buffer length, as well as provide a sufficiently large file size to accommodate the line before we try 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
Buffering
Now that we have tried hard to make sure that the first buffer is large enough and we have a revised maximum buffer size, it may still make sense for us to start with a smaller buffer and gradually increase the buffer size to create a buffer large enough so that we could extract the entire line from the file. According to the documentation, the API generates a 234 error to tell us more data available. It makes great sense that they use this error code to tell us to try again with a large buffer. The disadvantage of retrying is that it is more expensive. The longer the line in the file, the more attempts are required to read it, the longer it will take. 64 kilobytes is not so much for modern computers, and today computers work quite quickly, so any of these examples may be suitable for your purposes.
I did a lot of searches on the GetPrivateProfileString API and found that usually when someone who does not have extensive knowledge of the API tries to create a sufficiently large buffer for his needs, he chooses a buffer length of 255. This will allow you to read a line from a file up to 254 characters. I'm not sure why anyone started using this, but I would suggest that someone somewhere imagined this API using a line where the buffer length is limited to an 8-bit unsigned number. Perhaps this was a limitation of WIN16.
I will start with a low buffer level, 64 bytes, unless the maximum buffer length is less, and quadruple the number either to the maximum buffer length, or to 65,536. Doubling the number would also be acceptable, more multiplication means fewer attempts to read the file for large lines, while, relatively speaking, some medium-length lines may have additional filling.
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 path, section, and key names may prevent you from preparing a buffer.
According to the INI File Wikipedia page , they say:
In a Windows implementation, a key cannot contain an equal sign (=) or a semicolon (;), since these are reserved characters. The value can contain any character.
and
In a Windows implementation, a section cannot contain the closing bracket of the character (]).
The GetPrivateProfileString API quick test confirmed that this is only partially true. I had no problem using a semicolon in the key name if the semicolon was not at the very beginning. They do not mention any other restrictions in the documentation or on Wikipedia, although there may be more.
Another quick test to determine the maximum length of a section or key name accepted by GetPrivateProfileString gave me a limit of 65,535 characters. The effect of using a string longer than 65,535 characters was the same as I experienced when testing the maximum buffer length. Another test confirmed that this API would accept an empty string for the section or key name. According to the functionality of the API, this is an acceptable initialization file:
[]
= Hello world!
According to Wikipedia, the interpretation of spaces is changing. After another test, the Profile String API definitely removes spaces from section names and keys, so it will probably be nice if we do this 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 Buffer Length
Sometimes we need to store variables that have a maximum length or a static length. Username, phone number, color code or IP address are examples of lines where you can limit the maximum buffer length. By doing this if necessary, you will save 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 start at 64 and expand the buffer as before. Less than 64, and we will read only 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 is no problem reading a line using GetPrivateProfileString, limit the line length to 65,535 or less characters before using WritePrivateProfileString. It is also a good idea to include the same checks.
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