GetPrivateProfileString - 缓冲区长度

Dai*_*Dai 8 winapi ini

Windows的GetPrivateProfileXXX函数(用于处理INI文件)有一些关于处理缓冲区长度的奇怪规则.

GetPrivateProfileString的文档说明:

如果[...]提供的目标缓冲区太小而无法保存请求的字符串,则字符串将被截断,后跟空字符,返回值等于nSize减1.

我读了这篇文章,我意识到这种行为使得无法区分代码中的两个场景:

  • 当值字符串的长度恰好等于nSize - 1时.
  • 当nSize值(即缓冲区)太小时.

我以为我会做实验:

我在INI文件中有这个:

[Bar]
foo=123456
Run Code Online (Sandbox Code Playgroud)

我用这些参数调用GetPrivateProfileString作为测试:

// Test 1. The buffer is big enough for the string (16 character buffer).
BYTE* buffer1 = (BYTE*)calloc(16, 2); // using 2-byte characters ("Unicode")
DWORD result1 = GetPrivateProfileString(L"Bar", L"foo", NULL, buffer, 16, fileName);

// result1 is 6
// buffer1 is { 49, 0, 50, 0, 51, 0, 52, 0, 53, 0, 54, 0, 0, 0, 0, 0, ... , 0, 0 }

// Test 2. The buffer is exactly sufficient to hold the value and the trailing null (7 characters).
BYTE* buffer2 = (BYTE*)calloc(7, 2);
DWORD result2 = GetPrivateProfileString(L"Bar", L"foo", NULL, buffer, 7, fileName);

// result2 is 6. This is equal to 7-1.
// buffer2 is { 49, 0, 50, 0, 51, 0, 52, 0, 53, 0, 54, 0, 0, 0 }

// Test 3. The buffer is insufficient to hold the value and the trailing null (6 characters).
BYTE* buffer3 = (BYTE*)calloc(6, 2);
DWORD result3 = GetPrivateProfileString(L"Bar", L"foo", NULL, buffer, 6, fileName);

// result3 is 5. This is equal to 6-1.
// buffer3 is { 49, 0, 50, 0, 51, 0, 52, 0, 53, 0, 0, 0 }
Run Code Online (Sandbox Code Playgroud)

调用此代码的程序无法确定实际键值是否确实长度为5个字符,甚至是6个字符,因为在最后两个案例中结果等于nSize - 1.

唯一的解决方案是检查result == nSize - 1并使用更大的缓冲区调用该函数,但在缓冲区大小合适的情况下这是不必要的.

有没有更好的方法?

Bro*_*gan 4

当我致力于将一些古董代码带入未来时,我发现了这个关于缓冲和 Private Profile API 的问题。经过我自己的实验和研究,我可以确认提问者的原始陈述,即无法确定字符串恰好为 nSize - 1 或缓冲区太小时之间的差异。

\n\n

有没有更好的办法?迈克接受的答案说,根据文档,没有,您应该尝试确保缓冲区足够大。马克说要增加缓冲区。罗马说检查错误代码。一些随机用户说您需要提供足够大的缓冲区,并且与 Marc 不同,他继续显示一些扩展缓冲区的代码。

\n\n

有没有更好的办法?让我们了解事实吧!

\n\n

由于 ProfileString API 的历史,因为这个问题的标签都不考虑任何特定的语言,并且为了便于阅读,我决定使用 VB6 来展示我的示例。请随意翻译它们以用于您自己的目的。

\n\n
\n\n

GetPrivateProfileString 文档

\n\n

根据GetPrivateProfileString 文档,提供这些私有配置文件函数只是为了与基于 16 位 Windows 的应用程序兼容。这是非常有用的信息,因为它使我们能够了解这些 API 函数的局限性。

\n\n

16 位有符号整数的范围为 \xe2\x88\x9232,768 到 32,767,无符号 16 位整数的范围为 0 到 65,535。如果这些函数确实是为了在 16 位环境中使用而设计的,那么我们遇到的任何数字很可能都会被限制为这两个限制之一。

\n\n

文档指出返回的每个字符串都将以空字符结尾,并且还表示不适合提供的缓冲区的字符串将被截断并以空字符终止。因此,如果字符串确实适合缓冲区,则倒数第二个字符以及最后一个字符将为空。如果只有最后一个字符为空,则提取的字符串与提供的缓冲区的长度完全相同 - 1,或者缓冲区不够大,无法容纳该字符串。

\n\n

在倒数第二个字符不为空、提取的字符串长度准确或对于缓冲区来说太大的情况下,GetLastError 将返回错误号234 ERROR_MORE_DATA (0xEA) ,使我们无法区分它们。

\n\n
\n\n

GetPrivateProfileString 接受的最大缓冲区大小是多少?

\n\n

虽然文档没有说明最大缓冲区大小,但我们已经知道此 API 是为 16 位环境设计的。经过一些实验,我得出结论,最大缓冲区大小是65,536。如果文件中的字符串长度超过 65,535 个字符,我们在尝试读取该字符串时就会开始看到一些奇怪的行为。如果文件中的字符串长度为 65,536 个字符,则检索到的字符串长度将为 0 个字符。如果文件中的字符串长度为 65,546 个字符,则检索到的字符串长度将为 10 个字符,以空字符结尾,并从文件中包含的字符串的开头被截断。API 将写入大于 65,535 个字符的字符串,但无法读取大于 65,535 个字符的任何内容。如果缓冲区长度为 65,536 并且文件中的字符串长度为 65,535 个字符,则缓冲区将包含文件中的字符串,并以单个空字符结尾。

\n\n

这为我们提供了第一个虽然不是完美的解决方案。如果您想始终确保第一个缓冲区足够大,请将该缓冲区设置为 65,536 个字符长。

\n\n
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\n\nPublic Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String) As String\n    On Error GoTo iniReadError\n    Dim Buffer As String\n    Dim Result As Long\n    Buffer = String$(65536, vbNullChar)\n    Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, 65536, Pathname)\n    If Result <> 0 Then\n        iniRead = Left$(Buffer, Result)\n    Else\n        iniRead = Default\n    End If\niniReadError:\nEnd Function\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

现在我们知道了最大缓冲区大小,我们可以使用文件的大小来修改它。如果文件大小小于 65,535 个字符,则可能没有理由创建如此大的缓冲区。

\n\n

在文档的备注部分中,初始化文件中的部分必须具有以下形式:

\n\n
\n

[部分]
键=字符串

\n
\n\n

我们可以假设每个部分包含两个方括号和一个等号。经过小测试后,我能够验证 API 是否接受部分和键之间的任何类型的换行符(vbLf、vbCr 或 vbCrLf / vbNewLine)。这些详细信息以及节和键名称的长度将使我们能够缩小最大缓冲区长度,并确保在尝试读取文件之前文件大小足以包含字符串。

\n\n
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\n\nPublic Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String) As String\n    On Error Resume Next\n    Dim Buffer_Size As Long\n    Err.Clear\n    Buffer_Size = FileLen(Pathname)\n    On Error GoTo iniReadError\n    If Err.Number = 0 Then\n        If Buffer_Size > 4 + Len(Section) + Len(Key) Then\n            Dim Buffer As String\n            Dim Result As Long\n            Buffer_Size = Buffer_Size - Len(Section) - Len(Key) - 4\n            If Buffer_Size > 65535 Then\n                Buffer_Size = 65536\n            Else\n                Buffer_Size = Buffer_Size + 1\n            End If\n            Buffer = String$(Buffer_Size, vbNullChar)\n            Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)\n            If Result <> 0 Then\n                iniRead = Left$(Buffer, Result)\n                Exit Function\n            End If\n        End If\n    End If\n    iniRead = Default\niniReadError:\nEnd Function\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

增加缓冲区

\n\n

既然我们已经非常努力地确保第一个缓冲区足够大,并且我们已经修改了最大缓冲区大小,那么从较小的缓冲区开始并逐渐将缓冲区的大小增加到创建一个足够大的缓冲区,以便我们可以从文件中提取整个字符串。根据文档,API 创建 234 错误来告诉我们有更多可用数据。他们使用此错误代码告诉我们使用更大的缓冲区重试,这是很有意义的。一遍又一遍地重试的缺点是成本更高。文件中的字符串越长,读取它所需的尝试次数就越多,所需的时间就越长。64 KB 对于当今的计算机来说并不算多,而且当今的计算机速度相当快,因此您可能会发现这些示例中的任何一个都适合您的目的。

\n\n

我已经对 GetPrivateProfileString API 进行了相当多的搜索,并且我发现,通常当没有广泛了解该 API 的人尝试创建足够大的缓冲区来满足他们的需求时,他们会选择 255 的缓冲区长度。将允许您从文件中读取最长 254 个字符的字符串。我不确定为什么有人开始使用这个,但我假设有人在某个地方想象这个 API 使用一个字符串,其中缓冲区长度限制为 8 位无符号数字。也许这是WIN16的限制。

\n\n

我将从低 64 字节开始缓冲区,除非最大缓冲区长度更小,并将该数字增加四倍,直至最大缓冲区长度或 65,536。将数字加倍也是可以接受的,较大的乘法意味着读取较大字符串的文件的尝试较少,而相对而言,某些中等长度的字符串可能有额外的填充。

\n\n
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\n\nPublic Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String) As String\n    On Error Resume Next\n    Dim Buffer_Max As Long\n    Err.Clear\n    Buffer_Max = FileLen(Pathname)\n    On Error GoTo iniReadError\n    If Err.Number = 0 Then\n        If Buffer_Max > 4 + Len(Section) + Len(Key) Then\n            Dim Buffer As String\n            Dim Result As Long\n            Dim Buffer_Size As Long\n            Buffer_Max = Buffer_Max - Len(Section) - Len(Key) - 4\n            If Buffer_Max > 65535 Then\n                Buffer_Max = 65536\n            Else\n                Buffer_Max = Buffer_Max + 1\n            End If\n            If Buffer_Max < 64 Then\n                Buffer_Size = Buffer_Max\n            Else\n                Buffer_Size = 64\n            End If\n            Buffer = String$(Buffer_Size, vbNullChar)\n            Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)\n            If Result <> 0 Then\n                If Buffer_Max > 64 Then\n                    Do While Result = Buffer_Size - 1 And Buffer_Size < Buffer_Max\n                        Buffer_Size = Buffer_Size * 4\n                        If Buffer_Size > Buffer_Max Then\n                            Buffer_Size = Buffer_Max\n                        End If\n                        Buffer = String$(Buffer_Size, vbNullChar)\n                        Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)\n                    Loop\n                End If\n                iniRead = Left$(Buffer, Result)\n                Exit Function\n            End If\n        End If\n    End If\n    iniRead = Default\niniReadError:\nEnd Function\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

改进的验证

\n\n

根据您的实现,改进路径名、节和键名称的验证可能会阻止您需要准备缓冲区。

\n\n

根据维基百科的 INI 文件页面,他们说:

\n\n
\n

在 Windows 实现中,键不能包含字符\n 等号 ( = ) 或分号 ( ; ),因为这些是保留字符。\n 该值可以包含任何字符。

\n
\n\n

\n\n
\n

在 Windows 实现中,该部分不能包含字符\n 右括号 ( ] )。

\n
\n\n

对 GetPrivateProfileString API 的快速测试证明这只是部分正确。只要分号不在开头,我就可以在键名中使用分号。他们没有在文档或维基百科上提到任何其他限制,尽管可能还有更多。

\n\n

另一项快速测试是查找 GetPrivateProfileString 接受的节或键名称的最大长度,结果限制为 65,535 个字符。使用大于 65,535 个字符的字符串的效果与我在测试最大缓冲区长度时所经历的效果相同。另一项测试证明,此 API 将接受节名称或键名称的空白字符串。根据 API 的功能,这是一个可接受的初始化文件:

\n\n
\n

[]
\n =世界你好!

\n
\n\n

根据维基百科,空白的解释各不相同。经过另一次测试后,Profile String API 肯定会从部分和键名称中删除空格,因此如果我们也这样做可能没问题。

\n\n
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\n\nPublic Function iniRead(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, Optional ByVal Default As String) As String\n    On Error Resume Next\n    If Len(Pathname) <> 0 Then\n        Key = Trim$(Key)\n        If InStr(1, Key, ";") <> 1 Then\n            Section = Trim$(Section)\n            If Len(Section) > 65535 Then\n                Section = RTrim$(Left$(Section, 65535))\n            End If\n            If InStr(1, Section, "]") = 0 Then\n                If Len(Key) > 65535 Then\n                    Key = RTrim$(Left$(Key, 65535))\n                End If\n                If InStr(1, Key, "=") = 0 Then\n                    Dim Buffer_Max As Long\n                    Err.Clear\n                    Buffer_Max = FileLen(Pathname)\n                    On Error GoTo iniReadError\n                    If Err.Number = 0 Then\n                        If Buffer_Max > 4 + Len(Section) + Len(Key) Then\n                            Dim Buffer As String\n                            Dim Result As Long\n                            Dim Buffer_Size As Long\n                            Buffer_Max = Buffer_Max - Len(Section) - Len(Key) - 4\n                            If Buffer_Max > 65535 Then\n                                Buffer_Max = 65536\n                            Else\n                                Buffer_Max = Buffer_Max + 1\n                            End If\n                            If Buffer_Max < 64 Then\n                                Buffer_Size = Buffer_Max\n                            Else\n                                Buffer_Size = 64\n                            End If\n                            Buffer = String$(Buffer_Size, vbNullChar)\n                            Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)\n                            If Result <> 0 Then\n                                If Buffer_Max > 64 Then\n                                    Do While Result = Buffer_Size - 1 And Buffer_Size < Buffer_Max\n                                        Buffer_Size = Buffer_Size * 4\n                                        If Buffer_Size > Buffer_Max Then\n                                            Buffer_Size = Buffer_Max\n                                        End If\n                                        Buffer = String$(Buffer_Size, vbNullChar)\n                                        Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)\n                                    Loop\n                                End If\n                                iniRead = Left$(Buffer, Result)\n                                Exit Function\n                            End If\n                        End If\n                    End If\n                    iniRead = Default\n                End If\n            End If\n        End If\n    End If\niniReadError:\nEnd Function\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

静态长度缓冲区

\n\n

有时我们需要存储具有最大长度或静态长度的变量。用户名、电话号码、颜色代码或 IP 地址是您可能想要限制最大缓冲区长度的字符串示例。必要时这样做可以节省您的时间和精力。

\n\n

在下面的代码示例中,Buffer_Max 将被限制为 Buffer_Limit + 1。如果限制大于 64,我们将从 64 开始并像之前一样扩展缓冲区。小于 64,我们将仅使用新的缓冲区限制读取一次。

\n\n
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\n\nPublic 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\n    On Error Resume Next\n    If Len(Pathname) <> 0 Then\n        Key = Trim$(Key)\n        If InStr(1, Key, ";") <> 1 Then\n            Section = Trim$(Section)\n            If Len(Section) > 65535 Then\n                Section = RTrim$(Left$(Section, 65535))\n            End If\n            If InStr(1, Section, "]") = 0 Then\n                If Len(Key) > 65535 Then\n                    Key = RTrim$(Left$(Key, 65535))\n                End If\n                If InStr(1, Key, "=") = 0 Then\n                    Dim Buffer_Max As Long\n                    Err.Clear\n                    Buffer_Max = FileLen(Pathname)\n                    On Error GoTo iniReadError\n                    If Err.Number = 0 Then\n                        If Buffer_Max > 4 + Len(Section) + Len(Key) Then\n                            Dim Buffer As String\n                            Dim Result As Long\n                            Dim Buffer_Size As Long\n                            Buffer_Max = Buffer_Max - Len(Section) - Len(Key) - 4\n                            If Buffer_Limit > 65535 Then\n                                Buffer_Limit = 65535\n                            End If\n                            If Buffer_Max > Buffer_Limit Then\n                                Buffer_Max = Buffer_Limit + 1\n                            Else\n                                Buffer_Max = Buffer_Max + 1\n                            End If\n                            If Buffer_Max < 64 Then\n                                Buffer_Size = Buffer_Max\n                            Else\n                                Buffer_Size = 64\n                            End If\n                            Buffer = String$(Buffer_Size, vbNullChar)\n                            Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)\n                            If Result <> 0 Then\n                                If Buffer_Max > 64 Then\n                                    Do While Result = Buffer_Size - 1 And Buffer_Size < Buffer_Max\n                                        Buffer_Size = Buffer_Size * 4\n                                        If Buffer_Size > Buffer_Max Then\n                                            Buffer_Size = Buffer_Max\n                                        End If\n                                        Buffer = String$(Buffer_Size, vbNullChar)\n                                        Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)\n                                    Loop\n                                End If\n                                iniRead = Left$(Buffer, Result)\n                                Exit Function\n                            End If\n                        End If\n                    End If\n                    iniRead = Default\n                End If\n            End If\n        End If\n    End If\niniReadError:\nEnd Function\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

使用 WritePrivateProfileString

\n\n

为了确保使用 GetPrivateProfileString 读取字符串不会出现问题,请在使用 WritePrivateProfileString 之前将字符串限制为 65,535 个或更少的字符。包含相同的验证也是一个好主意。

\n\n
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\nPrivate 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\n\nPublic 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\n    On Error Resume Next\n    If Len(Pathname) <> 0 Then\n        Key = Trim$(Key)\n        If InStr(1, Key, ";") <> 1 Then\n            Section = Trim$(Section)\n            If Len(Section) > 65535 Then\n                Section = RTrim$(Left$(Section, 65535))\n            End If\n            If InStr(1, Section, "]") = 0 Then\n                If Len(Key) > 65535 Then\n                    Key = RTrim$(Left$(Key, 65535))\n                End If\n                If InStr(1, Key, "=") = 0 Then\n                    Dim Buffer_Max As Long\n                    Err.Clear\n                    Buffer_Max = FileLen(Pathname)\n                    On Error GoTo iniReadError\n                    If Err.Number = 0 Then\n                        If Buffer_Max > 4 + Len(Section) + Len(Key) Then\n                            Dim Buffer As String\n                            Dim Result As Long\n                            Dim Buffer_Size As Long\n                            Buffer_Max = Buffer_Max - Len(Section) - Len(Key) - 4\n                            If Buffer_Limit > 65535 Then\n                                Buffer_Limit = 65535\n                            End If\n                            If Buffer_Max > Buffer_Limit Then\n                                Buffer_Max = Buffer_Limit + 1\n                            Else\n                                Buffer_Max = Buffer_Max + 1\n                            End If\n                            If Buffer_Max < 64 Then\n                                Buffer_Size = Buffer_Max\n                            Else\n                                Buffer_Size = 64\n                            End If\n                            Buffer = String$(Buffer_Size, vbNullChar)\n                            Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)\n                            If Result <> 0 Then\n                                If Buffer_Max > 64 Then\n                                    Do While Result = Buffer_Size - 1 And Buffer_Size < Buffer_Max\n                                        Buffer_Size = Buffer_Size * 4\n                                        If Buffer_Size > Buffer_Max Then\n                                            Buffer_Size = Buffer_Max\n                                        End If\n                                        Buffer = String$(Buffer_Size, vbNullChar)\n                                        Result = GetPrivateProfileString(Section, Key, vbNullString, Buffer, Buffer_Size, Pathname)\n                                    Loop\n                                End If\n                                iniRead = Left$(Buffer, Result)\n                                Exit Function\n                            End If\n                        End If\n                    End If\n                    iniWrite Pathname, Section, Key, Default\n                    iniRead = Default\n                End If\n            End If\n        End If\n    End If\niniReadError:\nEnd Function\n\nPublic Function iniWrite(ByVal Pathname As String, ByVal Section As String, ByVal Key As String, ByVal Value As String) As Boolean\n    On Error GoTo iniWriteError\n    If Len(Pathname) <> 0 Then\n        Key = Trim$(Key)\n        If InStr(1, Key, ";") <> 1 Then\n            Section = Trim$(Section)\n            If Len(Section) > 65535 Then\n                Section = RTrim$(Left$(Section, 65535))\n            End If\n            If InStr(1, Section, "]") = 0 Then\n                If Len(Key) > 65535 Then\n                    Key = RTrim$(Left$(Key, 65535))\n                End If\n                If InStr(1, Key, "=") = 0 Then\n                    If Len(Value) > 65535 Then Value = Left$(Value, 65535)\n                    iniWrite = WritePrivateProfileString(Section, Key, Value, Pathname) <> 0\n                End If\n            End If\n        End If\n    End If\niniWriteError:\nEnd Function\n
Run Code Online (Sandbox Code Playgroud)\n