Пока я работал над тем, чтобы перенести в будущее часть моего античного кода, я обнаружил этот вопрос, касающийся буферизации и API частного профиля. После моих собственных экспериментов и исследований я могу подтвердить исходное утверждение автора вопроса о невозможности определить разницу между тем, когда строка имеет размер ровно nSize - 1, или когда буфер слишком мал.
Есть ли способ лучше? В принятом ответе Майка говорится, что это не соответствует документации, и вам следует просто попытаться убедиться, что буфер достаточно велик. Марк говорит увеличить буфер. Роман говорит коды ошибок проверки. Какой-то случайный пользователь говорит, что вам нужно предоставить достаточно большой буфер, и, в отличие от Марка, продолжает показывать код, расширяющий его буфер.
Есть ли способ лучше? Давайте узнаем факты!
Из-за возраста ProfileString API, поскольку ни один из тегов в этом вопросе не касается какого-либо конкретного языка и для удобства чтения, я решил показать свои примеры с использованием VB6. Не стесняйтесь переводить их для своих целей.
Документация GetPrivateProfileString
Согласно документации GetPrivateProfileString, эти функции частного профиля предоставляются только для совместимости с 16-разрядными приложениями на базе Windows. Это отличная информация, потому что она позволяет нам понять ограничения того, что могут делать эти функции API.
16-битовое целое число со знаком имеет диапазон от -32 768 до 32 767, а 16-битное целое число без знака имеет диапазон от 0 до 65 535. Если эти функции действительно созданы для использования в 16-битной среде, весьма вероятно, что любые числа, с которыми мы столкнемся, будут ограничены одним из этих двух пределов.
В документации указано, что каждая возвращаемая строка будет заканчиваться нулевым символом, а также сказано, что строка, которая не помещается в предоставленный буфер, будет усечена и завершена нулевым символом. Следовательно, если строка действительно помещается в буфер, второй последний символ будет иметь значение NULL, а также последний символ. Если только последний символ имеет значение NULL, то длина извлеченной строки точно такая же, как у предоставленного буфера - 1 или размер буфера недостаточен для хранения строки.
В любой ситуации, когда второй последний символ не равен нулю, извлеченная строка имеет точную длину или слишком велика для буфера, GetLastError вернет номер ошибки 234 ERROR_MORE_DATA (0xEA), что не дает нам возможности различать их.
Какой максимальный размер буфера принимает GetPrivateProfileString?
Хотя в документации не указывается максимальный размер буфера, мы уже знаем, что этот API был разработан для 16-битной среды. После небольших экспериментов я пришел к выводу, что максимальный размер буфера составляет 65 536. Если длина строки в файле превышает 65 535 символов, мы начинаем видеть какое-то странное поведение при попытке прочитать строку. Если длина строки в файле составляет 65 536 символов, полученная строка будет иметь длину 0 символов. Если длина строки в файле составляет 65 546 символов, полученная строка будет иметь длину 10 символов, заканчиваться нулевым символом и будет усечена с самого начала строки, содержащейся в файле. API запишет строку длиной более 65 535 символов, но не сможет прочитать ничего, превышающее 65 535 символов. Если длина буфера составляет 65 536, а длина строки в файле 65 535 символов, буфер будет содержать строку из файла, а также заканчиваться одним нулевым символом.
Это дает нам первое, хотя и не идеальное решение. Если вы хотите, чтобы ваш первый буфер всегда был достаточно большим, сделайте этот буфер длиной 65 536 символов.
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
Теперь, когда мы знаем максимальный размер буфера, мы можем использовать размер файла, чтобы изменить его. Если размер вашего файла меньше 65 535 символов, возможно, нет причин для создания такого большого буфера.
В разделе примечаний документации говорится, что раздел в файле инициализации должен иметь следующую форму:
[раздел]
ключ = строка
Можно считать, что каждый раздел содержит две квадратные скобки и знак равенства. После небольшого теста я смог убедиться, что API принимает любой разрыв строки между разделом и ключом (vbLf, vbCr или vbCrLf / vbNewLine). Эти детали, длина раздела и имена ключей позволят нам сузить максимальную длину буфера, а также обеспечить, чтобы размер файла был достаточно большим, чтобы содержать строку, прежде чем мы попытаемся прочитать файл.
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
Увеличение буфера
Теперь, когда мы очень старались убедиться, что первый буфер достаточно велик и у нас есть пересмотренный максимальный размер буфера, для нас все еще может иметь смысл начать с меньшего буфера и постепенно увеличивать размер буфера для создания буфер, достаточно большой, чтобы мы могли извлечь из файла всю строку. Согласно документации, API создает ошибку 234, чтобы сообщить нам, что доступны дополнительные данные. Имеет смысл использовать этот код ошибки, чтобы сказать нам повторить попытку с большим буфером. Обратной стороной повторных попыток является то, что это дороже. Чем длиннее строка в файле, тем больше попыток требуется для ее чтения, тем больше времени потребуется. 64 килобайта - это не много для современных компьютеров, а современные компьютеры довольно быстры, поэтому вы можете найти любой из этих примеров, который подходит для ваших целей в любом случае.
Я довольно много искал API GetPrivateProfileString и обнаружил, что обычно, когда кто-то, не обладающий обширными знаниями об API, пытается создать достаточно большой буфер для своих нужд, он выбирает длину буфера 255. Это позволило бы вы можете прочитать строку из файла длиной до 254 символов. Я не уверен, почему кто-то начал использовать это, но я предполагаю, что кто-то где-то представил этот API, используя строку, в которой длина буфера ограничена 8-битным числом без знака. Возможно это было ограничением WIN16.
Я собираюсь начать свой буфер с низкого уровня, 64 байта, если максимальная длина буфера не меньше, и учетверить это число либо до максимальной длины буфера, либо до 65 536. Удвоение числа также было бы приемлемым, большее умножение означает меньше попыток чтения файла для более крупных строк, в то время как, условно говоря, некоторые строки средней длины могут иметь дополнительное заполнение.
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
Улучшенная проверка
В зависимости от вашей реализации улучшение проверки вашего имени пути, разделов и имен ключей может избавить вас от необходимости готовить буфер.
Согласно странице INI-файла Википедии, они говорят:
В реализации Windows ключ не может содержать символы знака равенства (=) или точки с запятой (;), поскольку это зарезервированные символы. Значение может содержать любой символ.
и
В реализации Windows раздел не может содержать закрывающую скобку (]).
Быстрый тест API GetPrivateProfileString подтвердил, что это правда лишь частично. У меня не было проблем с использованием точки с запятой в имени ключа, если точка с запятой не была в самом начале. Они не упоминают никаких других ограничений в документации или в Википедии, хотя их может быть больше.
Еще один быстрый тест для определения максимальной длины раздела или имени ключа, принимаемого GetPrivateProfileString, дал мне ограничение в 65 535 символов. Эффект от использования строки, превышающей 65 535 символов, был таким же, как я испытал при тестировании максимальной длины буфера. Другой тест показал, что этот API принимает пустую строку в качестве имени раздела или ключа. По функциональности API это приемлемый файл инициализации:
[]
= Привет, мир!
Согласно Википедии, интерпретация пробелов различается. После еще одного теста Profile String API определенно удаляет пробелы из имен разделов и ключей, так что, вероятно, все в порядке, если мы это сделаем.
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
Буфер статической длины
Иногда нам нужно хранить переменные максимальной или статической длины. Имя пользователя, номер телефона, цветовой код или IP-адрес являются примерами строк, для которых вы можете захотеть ограничить максимальную длину буфера. Если вы сделаете это при необходимости, вы сэкономите время и силы.
В приведенном ниже примере кода Buffer_Max будет ограничен Buffer_Limit + 1. Если предел больше 64, мы начнем с 64 и расширим буфер так же, как и раньше. Менее 64, и мы будем читать только один раз, используя наш новый предел буфера.
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
Использование WritePrivateProfileString
Чтобы гарантировать отсутствие проблем с чтением строки с помощью GetPrivateProfileString, ограничьте количество строк до 65 535 или менее символов задолго до использования WritePrivateProfileString. Также неплохо включить те же проверки.
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
person
Brogan
schedule
03.02.2019