4

The below code is for converting a zip file to base64 format.

Dim inByteArray, base64Encoded,
Const TypeBinary = 1
inByteArray = readBytes("F:path/file.zip")
base64Encoded = encodeBase64(inByteArray)

Private Function readBytes(file)
    Dim inStream
    ' ADODB stream object used
    Set inStream = CreateObject("ADODB.Stream")
    ' open with no arguments makes the stream an empty container 
    inStream.Open
    inStream.Type = TypeBinary
    inStream.LoadFromFile(file)
    readBytes = inStream.Read()
End Function

Private Function encodeBase64(bytes)
    Dim DM, EL
    Set DM = CreateObject("Microsoft.XMLDOM")
    ' Create temporary node with Base64 data type
    Set EL = DM.CreateElement("tmp")
    EL.DataType = "bin.base64"
    ' Set bytes, get encoded String
    EL.NodeTypedValue = bytes
    encodeBase64 = EL.Text
End Function

I tried first with a zip file of size 3MB It worked fine . But when I try with a zip file of size 34 MB it says

Not enough storage is available to complete this operation!

at line

encodeBase64 = EL.Text

Is there any way that I can handle zip files of all sizes because my file sizes are mostly 30MB or more.

user692942
  • 16,398
  • 7
  • 76
  • 175
rahul raj
  • 105
  • 1
  • 1
  • 13

1 Answers1

2

edited 2017/01/10 - (original answer keeped at bottom)

edited 2017/01/10 - (again) - some (not all) of my problems with timeouts were caused by a disk failure.

Problems with input data were handled by splitting the conversion operations. Now code has been changed to handle buffering in two different ways: for small files (by default configured for files up to 10MB) a memory stream is used to store the output, but for big files (greater than 10MB) a temporary file is used (see notes after code).

Option Explicit

Dim buffer
    buffer = encodeFileBase64( "file.zip" ) 

    WScript.StdOut.WriteLine( CStr(Len(buffer)) )


Private Function encodeFileBase64( file )
    ' Declare ADODB used constants
    Const adTypeBinary = 1
    Const adTypeText = 2

    ' Declare FSO constants
    Const TEMP_FOLDER = 2

    ' Initialize output
    encodeFileBase64 = ""

    ' Instantiate FileSystemObject
    Dim fso
    Set fso = WScript.CreateObject("Scripting.FileSystemObject")

    ' Check input file exists
    If Not fso.FileExists( file ) Then 
        Exit Function
    End If 

    ' Determine how we will handle data buffering.
    ' Use a temporary file for large files 
    Dim useTemporaryFile
    useTemporaryFile = fso.GetFile( file ).Size > 10 * 1048576

    ' Instantiate the B64 conversion component
    Dim b64 
    Set b64 = WScript.CreateObject("Microsoft.XMLDOM").CreateElement("tmp")
        b64.DataType = "bin.base64"

    Dim outputBuffer, outputBufferName
    If useTemporaryFile Then 
        ' Create a temporary file to be used as a buffer
        outputBufferName = fso.BuildPath( _ 
            fso.GetSpecialFolder( TEMP_FOLDER ), _ 
            fso.GetTempName() _ 
        )
        Set outputBuffer = fso.CreateTextFile( outputBufferName, True )
    Else 
        ' Instantiate a text stream to be used as a buffer to avoid string 
        ' concatenation operations that were generating out of memory problems
        Set outputBuffer = WScript.CreateObject("ADODB.Stream")
        With outputBuffer
            ' Two bytes per character, BOM prefixed buffer
            .Type = adTypeText
            .Charset = "Unicode"
            .Open
        End With 
    End If 

    ' Instantiate a binary stream object to read input file 
    With WScript.CreateObject("ADODB.Stream")
        .Open
        .Type = adTypeBinary
        .LoadFromFile(file)

        ' Iterate over input file converting the file, converting each readed
        ' block to base64 and appending the converted text into the output buffer
        Dim inputBuffer
        Do
            inputBuffer = .Read(3145716)
            If IsNull( inputBuffer ) Then Exit Do

            b64.NodeTypedValue = inputBuffer
            If useTemporaryFile Then 
                Call outputBuffer.Write( b64.Text )
            Else 
                Call outputBuffer.WriteText( b64.Text )
            End If 
        Loop 

        ' Input file has been readed, close its associated stream
        Call .Close()
    End With

    ' It is time to retrieve the contents of the text output buffer into a 
    ' string. 

    If useTemporaryFile Then 
        ' Close output file 
        Call outputBuffer.Close()
        ' Read all the data from the buffer file
        encodeFileBase64 = fso.OpenTextFile( outputBufferName ).ReadAll()
        ' Remove temporary file
        Call fso.DeleteFile( outputBufferName )

    Else 

        ' So, as we already have a Unicode string inside the stream, we will
        ' convert it into binary and directly retrieve the data with the .Read() 
        ' method.
        With outputBuffer
            ' Type conversion is only possible while at the start of the stream
            .Position = 0
            ' Change stream type from text to binary
            .Type = adTypeBinary
            ' Skip BOM
            .Position = 2
            ' Retrieve buffered data
            encodeFileBase64 = CStr(.Read())
            ' Ensure we clear the stream contents
            .Position = 0
            Call .SetEOS()
            ' All done, close the stream
            Call .Close()
        End With 
    End If 

End Function

Will the memory be a problem?

Yes. Available memory is still a limit. Anyway I have tested the code with cscript.exe running as a 32bit process with 90MB files and in 64bit mode with 500MB files without problems.

Why two methods?

  • The stream method is faster (all operations are done in memory without string concatenations), but it requires more memory as it will have two copies of the same data at the end of the function: there will be one copy inside the stream and one in the string that will be returned

  • The temporary file method is slower as the buffered data will be written to disk, but as there is only one copy of the data, it requires less memory.

The 10MB limit used to determine if we will use or not a temporary file is just a pesimistic configuration to prevent problems in 32bit mode. I have processed 90MB files in 32bit mode without problems, but just to be safe.

Why the stream is configured as Unicode and the data is retrieved via .Read() method?

Because the stream.ReadText() is slow. Internally it makes a lot of string conversions/checks (yes, it is advised in the documentation) that make it unusable in this case.


Below it is the original answer. It is simpler and avoids the memory problem in the conversion but, for large files, it is not enough.


Split the read/encode process

Option Explicit
Const TypeBinary = 1

Dim buffer
    buffer = encodeFileBase64( "file.zip" ) 
    WScript.StdOut.WriteLine( buffer )

Private Function encodeFileBase64( file )

    Dim b64 
    Set b64 = WScript.CreateObject("Microsoft.XMLDOM").CreateElement("tmp")
        b64.DataType = "bin.base64"

    Dim outputBuffer
    Set outputBuffer = WScript.CreateObject("Scripting.Dictionary")

    With WScript.CreateObject("ADODB.Stream")
        .Open
        .Type = TypeBinary
        .LoadFromFile(file)

        Dim inputBuffer
        Do
            inputBuffer = .Read(3145716)
            If IsNull( inputBuffer ) Then Exit Do

            b64.NodeTypedValue = inputBuffer
            outputBuffer.Add outputBuffer.Count + 1, b64.Text 
        Loop 

        .Close
    End With

    encodeFileBase64 = Join(outputBuffer.Items(), vbCrLf)
End Function

Notes:

  • No, it is not bulletproof. You are still limited by the space needed to construct the output string. For big files, you will need to use an output file, writing partial results until all the input has been processed.

  • 3145716 is just the nearest multiple of 54 (the number of input bytes for each base64 output line) lower than 3145728 (3MB).

MC ND
  • 69,615
  • 8
  • 84
  • 126
  • I tried this but gives me error Out of string space: 'Join' Line (91): "encodeBase64 = Join(outputBuffer.Items(), vbCrLf)". The requirement is to pass the encoded value to next API test which I am doing it now after assigning the value to a variable and passing it on. Writing to a an external file would not help me I think as the API test doesnt read runtime updated values from excel data sheet in UFT on run time. Please correct me if I am wrong – rahul raj Dec 21 '16 at 10:28
  • I ran it again and now the error is out of memory when I do MsgBox the final encoded variable. – rahul raj Dec 21 '16 at 10:44
  • `Dim readBytes Do readBytes = inStream.Read(3145716) If IsNull(readBytes) Then Exit Do ' Set bytes, get encoded String EL.NodeTypedValue = readBytes outputBuffer.Add outputBuffer.Count + 1, EL.Text Loop inStream.Close encodeBase64 = Join(outputBuffer.Items(), vbCrLf) msgBox(encodeBase64) 'encodeBase64 = EL.Text msgBox(encodeBase64) RunAPITest "Demoservice" ,encodeBase64,fieldvalue2 next objExcel.Quit` – rahul raj Dec 21 '16 at 10:49
  • 1
    @rahulraj, I've tried with the posted code and with a `StringBuilder` version. In both cases the string concatenation approach fails. In 32 bits the limit is reached really fast, in 64 bits a 380MB file requests near 2GB memory. Your best approach is the segmented read input / write temporary file, and once done read the temporary file into memory to pass the data to the next step. – MC ND Dec 21 '16 at 11:16
  • 1
    Heard from different sources that if you are using "ADODB.Stream"the system memory gets eaten up. For me the out of memory is happening inconsistently . Is there any other way we can do a base64 conversion? – rahul raj Jan 09 '17 at 09:16
  • @rahulraj, Have you tried the approach I pointed in my previous comment? – MC ND Jan 09 '17 at 10:35
  • am very very new to VBS . I have to figure out how to do this now. – rahul raj Jan 09 '17 at 13:31
  • Out of memory: 'CStr' Line (128): "encodeFileBase64 = CStr(.Read())". – rahul raj Jan 10 '17 at 15:16
  • my file size is 34 MB – rahul raj Jan 10 '17 at 15:16
  • @rahulraj, Code updated. I have tested it in 32bit with a 60MB file and in 64bit with a 500MB file. In both cases without problems. – MC ND Jan 10 '17 at 16:06
  • @rahulraj, I've been testing over a set (500) of varying size files (1MB to 600MB), with the same script instance iterating over the set of files to check if there were memory leaks. The test machine is W10 64bits with 4GB RAM, and the tests were run both in 32bits and 64bits. Both `ADODB.Stream` and `XMLDOM` work flawlessly – MC ND Jan 11 '17 at 19:05
  • I tried running a 56MB zip file on 8GBRAM 32Bitmachine encodeBase64 = fso.OpenTextFile( outputBufferName ).ReadAll()--- Here I got the "Out of memory" error -- @MC ND – rahul raj Jan 23 '17 at 15:36
  • @rahulraj, I have tested it with bigger files in machines with less memory. Have you tested the code "as is", out of any environment? Just to ensure it is or not the rest of your execution environment who is "eating" the memory. – MC ND Jan 23 '17 at 15:53
  • Shall I share the complete code? Meanwhile I check on possible memory leaks @MC ND – rahul raj Jan 24 '17 at 12:12
  • I checked the temp file being created 'outputBufferName' -- This is of 46KB and its in my C drive C:\Users\C28279\AppData\Local\Temp. The C drive memory left is only 7GB and is in red. But its enough ? – rahul raj Jan 24 '17 at 12:25
  • @rahulraj, No, at this point there is not any need for the full code. But as pointed you should test the posted code as is, out of any environment to ensure it works in your machines. If it works, then check the environment. If it does not work, it is necessary to search an alternative. – MC ND Jan 24 '17 at 12:25
  • @rahulraj, Your problem is not the storage space available in disk, but the available `RAM` – MC ND Jan 24 '17 at 12:26
  • 1.I am not using a function call. I am doing this in the code body itself. 2.I am reading the input file location from an excel sheet iterating through the rows of the sheet 3. See my latest comment on c drive above – rahul raj Jan 24 '17 at 12:34
  • Please see the below picture when i tried to see the array structure of the base64 variable – rahul raj Jan 27 '17 at 10:06
  • @rahulraj, Sorry, I was not processing the output and in the case of files smaller than 10MB an `Byte()` variable was returned. Code changed to `encodeFileBase64 = CStr(.Read())` – MC ND Jan 27 '17 at 10:19
  • Can you please check my latest update in the below answer. I have attached a new screenshot – rahul raj Feb 22 '17 at 15:02
  • @rahulraj, a 34MB files requires aprox. 45MB memory in b64 format. I have read files five times bigger with batch files! Your problem is not in the code. Your environment is eating your available memory. – MC ND Feb 22 '17 at 15:29