GetPrivateProfileString - Buffer length - winapi
Windows' GetPrivateProfileXXX functions (used for working with INI files) have some strange rules about dealing with buffer lengths.
GetPrivateProfileString's documentation states:
If [..] the supplied destination buffer is too small to hold the requested string, the string is truncated and followed by a null character, and the return value is equal to nSize minus one.
I read this and I realised that this behaviour makes it impossible to differentiate between two scenarios in-code:
When the value string's length is exactly equal to nSize - 1.
When the nSize value (i.e. the buffer) is too small.
I thought I'd experiment:
I have this in an INI file:
[Bar]
foo=123456
And I called GetPrivateProfileString with these arguments as a test:
// 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 }
A program calling this code would have no way of knowing for sure if the actual key value is indeed 5 characters in length, or even 6, as in the last two cases result is equal to nSize - 1.
The only solution is to check whenever result == nSize - 1 and recall the function with a larger buffer, but this would be unnecessary in the cases where the buffer is of exactly the right size.
Isn't there a better way?
There is no better way. Just try to make sure the first buffer is large enough. Any method that solves this problem would have to make use of something not described in the documentation and hence would have no guarantee of working.
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
No, unfortunately, there isn't a better way. You have to provide a buffer larger enough. If it is not sufficient, reallocate the buffer. I took a code snippet from here, and adapted to your case:
int nBufferSize = 1000;
int nRetVal;
int nCnt = 0;
BYTE* buffer = (BYTE*)calloc(1, 2);
do
{
nCnt++;
buffer = (BYTE*) realloc (buffer , nBufferSize * 2 * nCnt);
DWORD nRetVal = GetPrivateProfileString(L"Bar", L"foo", NULL,
buffer, nBufferSize*nCnt, filename);
} while( (nRetVal == ((nBufferSize*nCnt) - 1)) ||
(nRetVal == ((nBufferSize*nCnt) - 2)) );
but, in your specific case, a filename cannot have a length greater than MAX_PATH, so (MAX_PATH+1)*2 will always fit.
Maybe, calling GetLastError right after GetPrivateProfileString is a way to go. If the buffer is big enough and there's no other errors, GetLastError returns 0. If the buffer is too small, GetLastError returns 234 (0xEA) ERROR_MORE_DATA.
I know it is a little late, but I came up with an awesome solution. If there is no buffer space left over (return length + 1 = buffer length), then grow the buffer and get the value again. Repeat that process until there is buffer space left over.
The best solution surely is Brogan's, but checking for file size as upper limit for the buffer size is just wrong. Especially when dealing with INI files located in the Windows or system folder, many of the keys where mapped to be read and/or written in the registry.
The mapping structure can be found in:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\IniFileMapping
For a complete explanation on how this works, you can read the Remarks section of the GetPrivateProfileString documentation.
Thus you could have many strings remapped to registry which grow up long enough, but a small INI file on disk. In this case the solution fails reading.
That solution also has another small problem when not using an absolute path to the desired file or when it isn't located in program's current working directory, since the GetPrivateProfileStrings searches for the initialization file in the Windows directory. This operation is not in the FileLen function nor the solution checks for that.
Ended up allocating 64K of memory and accepted this as a limit.
Related
What might make CryptStringToBinaryW crash?
I want to decode a string (which I got via readAsDataUrl()) to bytes. At first, I remove the data:*/*;base64, then I call the following: Option Explicit Private Const CRYPT_STRING_BASE64 As Long = &H1& Private Declare Function CryptStringToBinaryW Lib "Crypt32.dll" ( _ ByVal pszString As Long, _ ByVal cchString As Long, _ ByVal dwFlags As Long, _ ByVal pbBinary As Long, _ ByRef pcbBinary As Long, _ ByVal pdwSkip As Long, _ ByVal pdwFlags As Long) As Long Public Function DecodeBase64(ByVal strData As String) As Byte() Dim Buffer() As Byte Dim dwBinaryBytes As Long dwBinaryBytes = LenB(strData) ReDim Buffer(dwBinaryBytes - 1) As Byte 'within the following call, VB6 crashes: If CryptStringToBinaryW(StrPtr(strData), LenB(strData), CRYPT_STRING_BASE64, _ VarPtr(Buffer(0)), dwBinaryBytes, 0, 0) Then ReDim Preserve Buffer(dwBinaryBytes - 1) As Byte DecodeBase64 = Buffer End If Erase Buffer End Function Now I call this: Dim s$ 'code to get base64 string 'code to strip for example "data:image/jpeg;base64," Dim nBytes() As Byte nBytes = DecodeBase64(s) 'Here VB6 crashes Edit: I am using the following alternative version now, and it works, but I wonder what the error is: Public Function DecodeBase64(ByVal sBase64Buf As String) As Byte() Const CRYPT_STRING_BASE64 As Long = 1 Const CRYPT_STRING_NOCRLF As Long = &H40000000 Dim bTmp() As Byte Dim lLen As Long Dim dwActualUsed As Long 'Get output buffer length If CryptStringToBinary(StrPtr(sBase64Buf), Len(sBase64Buf), CRYPT_STRING_BASE64, StrPtr(vbNullString), lLen, 0&, dwActualUsed) = 0 Then 'RaiseEvent Error(Err.LastDllError, CSB, Routine) GoTo ReleaseHandles End If 'Convert Base64 to binary. ReDim bTmp(lLen - 1) If CryptStringToBinary(StrPtr(sBase64Buf), Len(sBase64Buf), CRYPT_STRING_BASE64, VarPtr(bTmp(0)), lLen, 0&, dwActualUsed) = 0 Then 'RaiseEvent Error(Err.LastDllError, CSB, Routine) GoTo ReleaseHandles Else 'm_bData = bTmp End If ReleaseHandles: DecodeBase64 = bTmp End Function Edit: In version 1, dwBinaryBytes is 156080 in this line: dwBinaryBytes = LenB(strData) and in version 2, lLen is 58528 in this line: ReDim bTmp(lLen - 1) Why the discrepancy, and why didn't the author notice that?
The "CryptStringToBinaryW" requires the number of characters in the string as a parameter. That is returned by the "Len" function. You used the "LenB" function which returns the number of bytes in the string which is larger than the number of characters in the string so the function attempted to access memory past the end of the string which caused the crash.
Convert String to Byte in VB6 with &H81 included in 0th index
How to Convert string in to Byte array that contain &H81 in first index if the byte array mybyte(0) with i need to check in my byte array Private Declare Sub CopyMemory _ Lib "kernel32" _ Alias "RtlMoveMemory" (Destination As Any, _ Source As Any, _ ByVal Length As Long) Private Sub cmdCommand1_Click() Dim str As String Dim BT() As Byte BT() = StrToByte(tbMsg.Text) If BT(0) = &H81 Then 'MyCode End If End Sub the If mybyte(0) = &H81 Then condition is allays getting false and currently i'm using this string to byte converting method Public Function StrToByte(strInput As String) As Byte() Dim lPntr As Long Dim bTmp() As Byte Dim bArray() As Byte If Len(strInput) = 0 Then Exit Function ReDim bTmp(LenB(strInput) - 1) 'Memory length ReDim bArray(Len(strInput) - 1) 'String length CopyMemory bTmp(0), ByVal StrPtr(strInput), LenB(strInput) For lPntr = 0 To UBound(bArray) If bTmp(lPntr * 2 + 1) > 0 Then bArray(lPntr) = Asc(Mid$(strInput, lPntr + 1, 1)) Else bArray(lPntr) = bTmp(lPntr * 2) End If Next lPntr StrToByte = bArray End Function
A typo I think, it should be: If BT(0) = &H81 Then Not If mybyte(0) = &H81 Then Your code seems to be converting the double byte unicode string into a single byte representation of the string, this will result in garbage for any character with a codepoint > 255. If thats ok your code is equivalent to the built in: BT() = StrConv(strInput, vbFromUnicode)
Byte shifting / casting VB6
I'm extremely unfamiliar with VB6 so please excuse the rookie question: I'm attempting to turn a long into it's component bytes. In C it is simple because of the automatic truncation and the bitshift operators. For the life of me I cannot figure out how to do this in VB6. Attempts so far have all generally looked something like this sys1 = CByte(((sys & &HFF000000) / 16777216)) ' >> 24 sys2 = CByte(((sys & &HFF0000) / 65536)) ' >> 16 sys1 and sys2 are declared as Byte and sys is declared as Long I'm getting a type mismatch exception when I try to do this. Anybody know how to convert a Long into 4 Bytes ?? Thanks
You divide correctly, but you forgot to mask out only the least significant bits. Supply the word you want to divide into bytes, and the index (0 is least significant, 1 is next, etc.) Private Function getByte(word As Long, index As Integer) As Byte Dim lTemp As Long ' shift the desired bits to the 8 least significant lTemp = word / (2 ^ (index * 8)) ' perform a bit-mask to keep only the 8 least significant lTemp = lTemp And 255 getByte = lTemp End Function
Found on FreeVBCode.com. Not tested, sorry. Option Explicit Private Declare Sub CopyMemory Lib "kernel32" _ Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal _ Length As Long) Public Function LongToByteArray(ByVal lng as Long) as Byte() 'Example: 'dim bytArr() as Byte 'dim iCtr as Integer 'bytArr = LongToByteArray(90121) 'For iCtr = 0 to Ubound(bytArr) 'Debug.Print bytArr(iCtr) 'Next '****************************************** Dim ByteArray(0 to 3)as Byte CopyMemory ByteArray(0), Byval VarPtr(Lng),Len(Lng) LongToByteArray = ByteArray End Function
You can convert between simple value types and Byte arrays by combining UDTs and the LSet statement. Option Explicit Private Type DataBytes Bytes(3) As Byte End Type Private Type DataLong Long As Long End Type Private DB As DataBytes Private DL As DataLong Private Sub cmdBytesToLong_Click() Dim I As Integer For I = 0 To 3 DB.Bytes(I) = CByte("&H" & txtBytes(I).Text) Next LSet DL = DB txtLong.Text = CStr(DL.Long) txtBytes(0).SetFocus End Sub Private Sub cmdLongToBytes_Click() Dim I As Integer DL.Long = CLng(txtLong.Text) LSet DB = DL For I = 0 To 3 txtBytes(I).Text = Right$("0" & Hex$(DB.Bytes(I)), 2) Next txtLong.SetFocus End Sub
How to enumerate available COM ports on a computer?
Other than looping from 1 to 32 and trying open each of them, is there a reliable way to get COM ports on the system?
I believe under modern windows environments you can find them in the registry under the following key HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM. I'm not sure of the correct way to specify registry keys. However I have only ever tested this on Windows XP.
Check out this article from Randy Birch's site: CreateFile: Determine Available COM Ports There's also the approach of using an MSCOMM control: ConfigurePort: Determine Available COM Ports with the MSCOMM Control The code's a bit too long for me to post here but the links have everything you need.
It's 1 to 255. Fastest you can do it is using QueryDosDevice like this Option Explicit '--- for CreateFile Private Const GENERIC_READ As Long = &H80000000 Private Const GENERIC_WRITE As Long = &H40000000 Private Const OPEN_EXISTING As Long = 3 Private Const INVALID_HANDLE_VALUE As Long = -1 '--- error codes Private Const ERROR_ACCESS_DENIED As Long = 5& Private Const ERROR_GEN_FAILURE As Long = 31& Private Const ERROR_SHARING_VIOLATION As Long = 32& Private Const ERROR_SEM_TIMEOUT As Long = 121& Private Declare Function QueryDosDevice Lib "kernel32" Alias "QueryDosDeviceA" (ByVal lpDeviceName As Long, ByVal lpTargetPath As String, ByVal ucchMax As Long) As Long Private Declare Function CreateFile Lib "kernel32" Alias "CreateFileA" (ByVal lpFileName As String, ByVal dwDesiredAccess As Long, ByVal dwShareMode As Long, ByVal lpSecurityAttributes As Long, ByVal dwCreationDisposition As Long, ByVal dwFlagsAndAttributes As Long, ByVal hTemplateFile As Long) As Long Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long Private Function PrintError(sFunc As String) Debug.Print sFunc; ": "; Error End Function Public Function IsNT() As Boolean IsNT = True End Function Public Function EnumSerialPorts() As Variant Const FUNC_NAME As String = "EnumSerialPorts" Dim sBuffer As String Dim lIdx As Long Dim hFile As Long Dim vRet As Variant Dim lCount As Long On Error GoTo EH ReDim vRet(0 To 255) As Variant If IsNT Then sBuffer = String$(100000, 1) Call QueryDosDevice(0, sBuffer, Len(sBuffer)) sBuffer = Chr$(0) & sBuffer For lIdx = 1 To 255 If InStr(1, sBuffer, Chr$(0) & "COM" & lIdx & Chr$(0), vbTextCompare) > 0 Then vRet(lCount) = "COM" & lIdx lCount = lCount + 1 End If Next Else For lIdx = 1 To 255 hFile = CreateFile("COM" & lIdx, GENERIC_READ Or GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0) If hFile = INVALID_HANDLE_VALUE Then Select Case Err.LastDllError Case ERROR_ACCESS_DENIED, ERROR_GEN_FAILURE, ERROR_SHARING_VIOLATION, ERROR_SEM_TIMEOUT hFile = 0 End Select Else Call CloseHandle(hFile) hFile = 0 End If If hFile = 0 Then vRet(lCount) = "COM" & lIdx lCount = lCount + 1 End If Next End If If lCount = 0 Then EnumSerialPorts = Split(vbNullString) Else ReDim Preserve vRet(0 To lCount - 1) As Variant EnumSerialPorts = vRet End If Exit Function EH: PrintError FUNC_NAME Resume Next End Function The snippet falls back to CreateFile on 9x. IsNT function is stubbed for brevity.
Using VB6 or VBScript to enumerate available COM ports can be as simple as using VB.NET, and this can be done by enumerating values from registry path HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM. It's better than calling QueryDosDevice() and doing string comparison to filter out devices which's name is leading by COM since you will get something like CompositeBattery (or other stuff which have full upper case name leading by COM) that isn't a COM port. Another benefit of doing this is that the registry values also containing USB to COM devices, which could not be detected by using the codes such as WMIService.ExecQuery("Select * from Win32_SerialPort"). If you try to plug the USB to COM devices in or out of the computer, you can see the registry values also appear or disappear immediately, since it's keeping updated. Option Explicit Sub ListComPorts() List1.Clear Dim Registry As Object, Names As Variant, Types As Variant Set Registry = GetObject("winmgmts:\\.\root\default:StdRegProv") If Registry.EnumValues(&H80000002, "HARDWARE\DEVICEMAP\SERIALCOMM", Names, Types) <> 0 Then Exit Sub Dim I As Long If IsArray(Names) Then For I = 0 To UBound(Names) Dim PortName As Variant Registry.GetStringValue &H80000002, "HARDWARE\DEVICEMAP\SERIALCOMM", Names(I), PortName List1.AddItem PortName & " - " & Names(I) Next End If End Sub Private Sub Form_Load() ListComPorts End Sub The code above is using StdRegProv class to enumerate the values of a registry key. I've tested the code in XP, Windows 7, Windows 10, and it works without any complainant. The items which were added to the Listbox looks like below: COM1 - \Device\Serial0 COM3 - \Device\ProlificSerial0 The downside of this code is that it could not detect which port is already opened by other programs since every port could only be opened once. The way to detect a COM port is opened by another program or not can be done by calling the API CreateFile. Here is an example.
Modern day, unicode-friendly ".ini file" to store config data in VB6
I'd like to store the contents of a data structure, a few arrays, and a dozen or so variables in a file that can be saved and reloaded by my software as well as optionally edited in a text editor by a user reloaded. For the text editing, I need the data to be clearly labeled, like in a good ole .ini file: AbsMaxVoltage = 17.5 There's a GUI, and one could argue that the user should just load and save and modify from the GUI, but the customer wants to be able to read and modify the data as text. It's easy enough to write code to save it and reload it (assuming all the labels are in the same place and only the data has changed). With more work (or using some of the INI R/W code that's already out there I could pay attention to the label so if a line gets deleted or moved around the variables still get stuffed correctly, but both of these approaches seem pretty old-school. So I'm interested in how the brightest minds in programming would approach this today (using decade-old VB6 which I have to admit I still love). Disclaimer: I'm an electrical engineer, not a programmer. This isn't my day job. Well maybe it's a few % of my day job. Cheers!
Lots of people will recommend XML to you. The problem is XML is still so trendy, some people use it everywhere without really thinking about it. Like Jeff Atwood said, it's hard for non-programmers to read XML and particularly to edit it. There are too many rules, like escaping special characters and closing the tags in the correct order. Some experts recommend you treat XML as a binary format, not a text format at all. I recommend using INI files, provided the maximum size limit of 32K is not a problem. I've never reached that limit in many similar situations in my own VB6. INI files are easy for ordinary folk to edit, and it's easy to read and write them from VB6. Just use some of the excellent drop-in code freely available on the web. I'm sure the class Jay Riggs provided in his answer is excellent, because it's from VBAccelerator. I would also recommend this class, because anything by Karl Peterson will be excellent too. A couple of other points to think about: Have you considered which directory to put the files into? You mentioned "Unicode-friendly" in the question. INI files aren't Unicode, but that doesn't matter in practise. Unless you want to store characters that aren't supported on the current code page - like Chinese on an English computer - an unusual requirement, and one that will cause you other problems in a VB6 program anyway. Legendary Windows guru Raymond Chen described the advantages of XML configuration files over INI files. Many of them rely on the XML file being read-only. The one legitimate advantage is if the data is highly structured - class heirarchies or the like. From your description that doesn't apply.
Consider using XML. It's completely standard, many text editors will highlight/manage it properly, every programming language and script language on Earth has good support for reading it, and it handles Unicode perfectly. For simple name/value pairs as you suggest, it's quite readable. But you have the added advantage that if someday you need something more complex -- e.g. multi-lined values or a list of distinct values -- XML provides natural, easy ways of representing that. P.S. Here's how to read XML in VB6.
Back in the olden days this class helped me use INI files with my VB6 programs: VERSION 1.0 CLASS BEGIN MultiUse = -1 'True END Attribute VB_Name = "cInifile" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = True Attribute VB_PredeclaredId = False Attribute VB_Exposed = False Option Explicit ' ========================================================= ' Class: cIniFile ' Author: Steve McMahon ' Date : 21 Feb 1997 ' ' A nice class wrapper around the INIFile functions ' Allows searching,deletion,modification and addition ' of Keys or Values. ' ' Updated 10 May 1998 for VB5. ' * Added EnumerateAllSections method ' * Added Load and Save form position methods ' ========================================================= Private m_sPath As String Private m_sKey As String Private m_sSection As String Private m_sDefault As String Private m_lLastReturnCode As Long #If Win32 Then ' Profile String functions: 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 Private Declare Function GetPrivateProfileString Lib "KERNEL32" Alias "GetPrivateProfileStringA" (ByVal lpApplicationName As Any, ByVal lpKeyName As Any, ByVal lpDefault As Any, ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long #Else ' Profile String functions: Private Declare Function WritePrivateProfileString Lib "Kernel" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpString As Any, ByVal lpFileName As String) As Integer Private Declare Function GetPrivateProfileString Lib "Kernel" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, ByVal lpDefault As Any, ByVal lpReturnedString As String, ByVal nSize As Integer, ByVal lpFileName As String) As Integer #End If Property Get LastReturnCode() As Long LastReturnCode = m_lLastReturnCode End Property Property Get Success() As Boolean Success = (m_lLastReturnCode <> 0) End Property Property Let Default(sDefault As String) m_sDefault = sDefault End Property Property Get Default() As String Default = m_sDefault End Property Property Let Path(sPath As String) m_sPath = sPath End Property Property Get Path() As String Path = m_sPath End Property Property Let Key(sKey As String) m_sKey = sKey End Property Property Get Key() As String Key = m_sKey End Property Property Let Section(sSection As String) m_sSection = sSection End Property Property Get Section() As String Section = m_sSection End Property Property Get Value() As String Dim sBuf As String Dim iSize As String Dim iRetCode As Integer sBuf = Space$(255) iSize = Len(sBuf) iRetCode = GetPrivateProfileString(m_sSection, m_sKey, m_sDefault, sBuf, iSize, m_sPath) If (iSize > 0) Then Value = Left$(sBuf, iRetCode) Else Value = "" End If End Property Property Let Value(sValue As String) Dim iPos As Integer ' Strip chr$(0): iPos = InStr(sValue, Chr$(0)) Do While iPos <> 0 sValue = Left$(sValue, (iPos - 1)) & Mid$(sValue, (iPos + 1)) iPos = InStr(sValue, Chr$(0)) Loop m_lLastReturnCode = WritePrivateProfileString(m_sSection, m_sKey, sValue, m_sPath) End Property Public Sub DeleteKey() m_lLastReturnCode = WritePrivateProfileString(m_sSection, m_sKey, 0&, m_sPath) End Sub Public Sub DeleteSection() m_lLastReturnCode = WritePrivateProfileString(m_sSection, 0&, 0&, m_sPath) End Sub Property Get INISection() As String Dim sBuf As String Dim iSize As String Dim iRetCode As Integer sBuf = Space$(8192) iSize = Len(sBuf) iRetCode = GetPrivateProfileString(m_sSection, 0&, m_sDefault, sBuf, iSize, m_sPath) If (iSize > 0) Then INISection = Left$(sBuf, iRetCode) Else INISection = "" End If End Property Property Let INISection(sSection As String) m_lLastReturnCode = WritePrivateProfileString(m_sSection, 0&, sSection, m_sPath) End Property Property Get Sections() As String Dim sBuf As String Dim iSize As String Dim iRetCode As Integer sBuf = Space$(8192) iSize = Len(sBuf) iRetCode = GetPrivateProfileString(0&, 0&, m_sDefault, sBuf, iSize, m_sPath) If (iSize > 0) Then Sections = Left$(sBuf, iRetCode) Else Sections = "" End If End Property Public Sub EnumerateCurrentSection(ByRef sKey() As String, ByRef iCount As Long) Dim sSection As String Dim iPos As Long Dim iNextPos As Long Dim sCur As String iCount = 0 Erase sKey sSection = INISection If (Len(sSection) > 0) Then iPos = 1 iNextPos = InStr(iPos, sSection, Chr$(0)) Do While iNextPos <> 0 sCur = Mid$(sSection, iPos, (iNextPos - iPos)) If (sCur <> Chr$(0)) Then iCount = iCount + 1 ReDim Preserve sKey(1 To iCount) As String sKey(iCount) = Mid$(sSection, iPos, (iNextPos - iPos)) iPos = iNextPos + 1 iNextPos = InStr(iPos, sSection, Chr$(0)) End If Loop End If End Sub Public Sub EnumerateAllSections(ByRef sSections() As String, ByRef iCount As Long) Dim sIniFile As String Dim iPos As Long Dim iNextPos As Long Dim sCur As String iCount = 0 Erase sSections sIniFile = Sections If (Len(sIniFile) > 0) Then iPos = 1 iNextPos = InStr(iPos, sIniFile, Chr$(0)) Do While iNextPos <> 0 If (iNextPos <> iPos) Then sCur = Mid$(sIniFile, iPos, (iNextPos - iPos)) iCount = iCount + 1 ReDim Preserve sSections(1 To iCount) As String sSections(iCount) = sCur End If iPos = iNextPos + 1 iNextPos = InStr(iPos, sIniFile, Chr$(0)) Loop End If End Sub Public Sub SaveFormPosition(ByRef frmThis As Object) Dim sSaveKey As String Dim sSaveDefault As String On Error GoTo SaveError sSaveKey = Key If Not (frmThis.WindowState = vbMinimized) Then Key = "Maximised" Value = (frmThis.WindowState = vbMaximized) * -1 If (frmThis.WindowState <> vbMaximized) Then Key = "Left" Value = frmThis.Left Key = "Top" Value = frmThis.Top Key = "Width" Value = frmThis.Width Key = "Height" Value = frmThis.Height End If End If Key = sSaveKey Exit Sub SaveError: Key = sSaveKey m_lLastReturnCode = 0 Exit Sub End Sub Public Sub LoadFormPosition(ByRef frmThis As Object, Optional ByRef lMinWidth = 3000, Optional ByRef lMinHeight = 3000) Dim sSaveKey As String Dim sSaveDefault As String Dim lLeft As Long Dim lTOp As Long Dim lWidth As Long Dim lHeight As Long On Error GoTo LoadError sSaveKey = Key sSaveDefault = Default Default = "FAIL" Key = "Left" lLeft = CLngDefault(Value, frmThis.Left) Key = "Top" lTOp = CLngDefault(Value, frmThis.Top) Key = "Width" lWidth = CLngDefault(Value, frmThis.Width) If (lWidth < lMinWidth) Then lWidth = lMinWidth Key = "Height" lHeight = CLngDefault(Value, frmThis.Height) If (lHeight < lMinHeight) Then lHeight = lMinHeight If (lLeft < 4 * Screen.TwipsPerPixelX) Then lLeft = 4 * Screen.TwipsPerPixelX If (lTOp < 4 * Screen.TwipsPerPixelY) Then lTOp = 4 * Screen.TwipsPerPixelY If (lLeft + lWidth > Screen.Width - 4 * Screen.TwipsPerPixelX) Then lLeft = Screen.Width - 4 * Screen.TwipsPerPixelX - lWidth If (lLeft < 4 * Screen.TwipsPerPixelX) Then lLeft = 4 * Screen.TwipsPerPixelX If (lLeft + lWidth > Screen.Width - 4 * Screen.TwipsPerPixelX) Then lWidth = Screen.Width - lLeft - 4 * Screen.TwipsPerPixelX End If End If If (lTOp + lHeight > Screen.Height - 4 * Screen.TwipsPerPixelY) Then lTOp = Screen.Height - 4 * Screen.TwipsPerPixelY - lHeight If (lTOp < 4 * Screen.TwipsPerPixelY) Then lTOp = 4 * Screen.TwipsPerPixelY If (lTOp + lHeight > Screen.Height - 4 * Screen.TwipsPerPixelY) Then lHeight = Screen.Height - lTOp - 4 * Screen.TwipsPerPixelY End If End If If (lWidth >= lMinWidth) And (lHeight >= lMinHeight) Then frmThis.Move lLeft, lTOp, lWidth, lHeight End If Key = "Maximised" If (CLngDefault(Value, 0) <> 0) Then frmThis.WindowState = vbMaximized End If Key = sSaveKey Default = sSaveDefault Exit Sub LoadError: Key = sSaveKey Default = sSaveDefault m_lLastReturnCode = 0 Exit Sub End Sub Public Function CLngDefault(ByVal sString As String, Optional ByVal lDefault As Long = 0) As Long Dim lR As Long On Error Resume Next lR = CLng(sString) If (Err.Number <> 0) Then CLngDefault = lDefault Else CLngDefault = lR End If End Function
Would and XML file be acceptable:- <config> <someAppPart AbsMaxVoltage="17.5" AbsMinVoltage="5.5" /> <someOtherAppPart ForegroundColor="Black" BackgroundColor="White" /> </config> Its very easy to consume in VB6, you don't need to worry about positioning etc. The downside is its the user tweaking it can make it unparsable, however thats true if you write your own parser for a config file.
If we can assume your saved settings are simply a set of name/value pairs without a two-level hierarchy requirement (i.e. INI "Keys" within "Sections") you might just persist them as such: AbsMaxVoltage=17.5 AbsMinVoltage=5.5 For writing the persistence format this is a case where you might consider the FSO, since the access volume is low anyway. The FSO can handle read/writing Unicode text files. I think I'd do something like read lines and parse them using a Split() on "=" specifying just 2 parts (thus allowing "=" within values as well). For loading these I'd store them into a simple Class instance where the Class has two properties (Name and Value) and add each one to a Collection using Name as the Key. Make Value the default property if desired. Maybe even implement some form of comment text line too using a generated sequence-numbered special Name value stored as say Name="%1" Value="comment text" with generated unique Names to avoid Collection Key collisions. Blank lines might be similarly preserved. Then persisting as necessary means simply using a For Each on the Collection and using the FSO to write Name=Value out to disk. To simulate a hierarchy you could simply use Names like: %Comment: somAppPart settings someAppPart.AbsMaxVoltage=17.5 someAppPart.AbsMinVoltage=5.5 %someOtherPart settings someOtherAppPart.ForegroundColor=Black someOtherAppPart.BackgroundColor=White The parsing is cheap, so any probing of the Collection might be preceded by a full reparse (as the INI API calls do). Any changing of values in the program might do a full rewrite to disk (like the INI API calls do). Some of this can be automated by just wrapping the Collection with some logic in another Class. The result could be syntax like: Settings("someOtherAppPart", "ForegroundColor") = "Red" aka Settings.Value("someOtherAppPart", "ForegroundColor") = "Red" This would reload the Collection, then probe the Collection for an Item keyed "someOtherAppPart.ForegroundColor" and create it or set its Value to "Red" and then flush the Collection to disk. Or you might eschew frequent rewriting and use distinct Load and Save methods. Make it as simple or fancy as desired. In any case, the result is a text file users can hack at with Notepad. The only reason for the FSO is to have an easy way of read/writing Unicode text. One could also screw around with Byte array I/O and explicit conversions (array to String) and line level parsing as required to avoid the FSO. If so just don't forget about the UTF-16LE BOM.