Compressed Folders in WSH

Windows XP provides built-in zip file support in a feature referred to as Compressed Folders. While the compression of a Compressed Folder is not as effective as many third-party implementations, it does offer great compatibility since it is natively supported on all modern Windows platforms.

For whatever reason, Microsoft did not include any scripting support for creating Compressed Folders.  There isn’t any viable command line for creating them either.  So that’s that; we shed our tears and walk away.

But then again, that wouldn’t make for a very interesting article, and there’s a good chance my editors might have something to say about it when it comes time to pay me.  So I guess we’ll have to make a workaround.

Compressed folders are implemented as a Windows Shell extension.  They are available in the context menu from within Windows Explorer.  Typically we would start looking here for a workaround, since we can often execute context menu items using the InvokeVerb method of the Windows Shell’s scripting interface.  However, the Compressed Folder item appears in the New menu and we don’t have access to that.

That pretty much exhausts every avenue of automating the feature itself.  Let’s take a look at creating the file directly.  To do that you’ll need a hex editor like XVI32.  A hex editor allows you to open and view binary files like the Compress Folders in which we’re so interested.

If you were to create a Compressed Folder in Windows Explorer, you would notice that it is nothing more than a .zip file.  If we open that empty .zip file in a hex editor, we may be able to see its binary composition and duplicate it to create our own zip files.

As it turns out, Compressed Folders are actually very basic binary files that consist of a series of twenty-two bytes as follows:

50 4B 05 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Each two-digit pair represents a single byte.  This number is a hex value that represents some character.  For example, 50 in hex equals 80 in decimal.  The ASCII code for character 80 is the capital letter P.  I’m going to spare myself the lengthy explanation here since we’re going to be working with the values that we see here.  Essentially, this file has the letters PK followed by a series of non-printable characters.

{mospagebreak title=Creating a Binary file}

Now that we can see the binary makeup of the zip file, we can write that binary string to a new file on disk to create our own zip file—except that neither VBScript nor WSH provide a method for writing binary data.  And so, we have problem number two.

Many of the solutions that I’ve seen in the past for this problem have simply taken the binary data and written its decimal equivalent (as ASCII characters) to a text file using the FileSystemObject.

Const ForWriting = 2

Set objFSO = CreateObject("Scripting.FileSystemObject")

Set objFile = objFSO.OpenTextFile("New Folder.zip", ForWriting, True)

objFile.Write "PK" & Chr(5) & Chr(6) & String(18, Chr(0))

objFile.Close

Sure enough, this approach will work.  This zip format is simple enough that it works on most systems.  However, the problem lies a little deeper.  The FileSystemObject’s methods are designed for creating and writing text files, not binary files.  In other words, a script written on one machine may not work on a machine using a different codebase.  The simple truth is that, while this method works in this particular instance, it’s not a workable solution for creating binary files.  To do that we’ll need to look elsewhere.

The only native scriptable object capable of handling binary data is the ADO Stream object.  Just our luck, it also provides a method of writing its stream data to a file.

strPath = "C:zip.zip"

 

Const adTypeBinary = 1

Const adTypeText = 2

Const adSaveCreateNotExist = 1

Const adSaveCreateOverwrite = 2

Set objStream = CreateObject("ADODB.Stream")

You can create an ADO Stream object by connecting to the ADODB.Stream ProgID.  To make it usable, you’ll need to open the stream and define its type as either text or binary.  The choice would seem quite obvious, but herein lies another problem.  If you choose a binary stream type, you must provide a byte array to write to the stream.  VBScript does not provide any method of creating a byte array, so it’s time for yet another workaround.

objStream.Open

objStream.Type = adTypeText

For now, let’s go ahead and write our file as a text stream.  The Open method opens the Stream object and the Type property is used to set the Stream type as text.

objStream.WriteText ChrB(&h50) & ChrB(&h4B) & ChrB(&h5) & ChrB(&h6)

For i = 1 To 18

   objStream.WriteText ChrB(&h0)

Next

The WriteText method is used to write a string of characters to the Stream object.  Rather than trying to convert every character in the original binary string that we found, I’m just adding each character by its hex value.  VBScript’s ChrB function is fairly undocumented; it returns a binary character for a provided value.  I’m using VBScript’s &h notation to indicate that the provided numbers should be interpreted as hexadecimal values.

objStream.SaveToFile strPath, adSaveCreateNotExist

objStream.Close

Now we can use the Stream object’s SaveToFile method to write to stream data to a file.  The first method is the full path and file name of the file to be written.  The second is a constant value that indicates whether the file should be created if it doesn’t exist or if it should be overwritten.  Finally, the stream is closed with the Close method.

At this point, we’ve written our binary characters to a file.  However, further examination with our trusty hex editor shows that the ADO Stream object has written two extra unwanted bytes at the beginning of the file.  This is no good.  It turns out that we need to write true binary data.  It also turns out that the ADO Stream object does provide a method of returning a byte array from an existing file, and we can use the dummy file that we’ve just created.

{mospagebreak title=Getting a Byte Array in VBScript}

We know at this point that we need a byte array to write a binary stream to a file.  VBScript doesn’t provide a method of creating one, but we can read a byte array from a dummy file that we create with the information that we would like in it.

objStream.Open

objStream.Type = adTypeBinary

objStream.LoadFromFile strPath

We need to reopen the Stream object by using its Open method again.  This time we’ll set its Type to binary so that it reads in a binary byte array.  Finally, we can use the LoadFromFile method to load the contents of our dummy file into the data stream.

objStream.Position = 2

arrBytes = objStream.Read

Remember that we want to skip the first two unwanted bytes of information.  To do this we’ll use the Stream object’s Position property to set the cursor position to the third character.  Positions are zero-based so we’ll set the property value to 2.  The Read method is then used to return a byte array from the stream.

objStream.Position = 0

objStream.SetEOS

objStream.Write arrBytes

Now we need to write our byte array back to the file in binary form.  We can reuse the binary stream that we already have open by setting the cursor position back to the beginning and using the SetEOS method. This method sets the End Of Stream marker to the current cursor position.  By setting it to the beginning of the stream, we can effectively dump the existing stream data.

objStream.SaveToFile strPath, adSaveCreateOverwrite

objStream.Close

Set objStream = Nothing

Now you can use the SaveToFile method to write the stream to file in true binary fashion.  Notice that we’re now overwriting the dummy file.  We’re all finished, so you can close the stream and disconnect its object.

Voila!  A series of workarounds has enabled us to create a true byte array and write a binary file in VBScript using only native objects.  A quick peek with the hex editor shows that our zip file is exactly like the original one that we inspected when we began.

{mospagebreak title=Adding and Extracting files}

It’s probably a good idea to package this code in a function.  You also make it a little more friendly by using a With structure to eliminate some redundancies.  Here’s what my function looks like.

Function NewCompressedFolder(strPath)

   Const adTypeBinary = 1

   Const adTypeText = 2

   Const adSaveCreateNotExist = 1

   Const adSaveCreateOverwrite = 2

   With CreateObject("ADODB.Stream")

       .Open

       .Type = adTypeText

       .WriteText ChrB(&h50) & ChrB(&h4B) & ChrB(&h5) & ChrB(&h6)

       For i = 1 To 18

          .WriteText ChrB(&h0)

       Next

       .SaveToFile strPath, adSaveCreateNotExist

       .Close

       .Open

       .Type = adTypeBinary

       .LoadFromFile strPath

       .Position = 2

       arrBytes = .Read

       .Position = 0

       .SetEOS

       .Write arrBytes

       .SaveToFile strPath, adSaveCreateOverwrite

       .Close

   End With

End Function

Finally, you probably wouldn’t be trying to create a compressed folder if you had no intentions of using it.  Remember, compressed folder support is native to the Windows Shell, so we’ll need to use its scripting object to work with the file.  This isn’t as hard as it sounds.  You can see that Windows Explorer treats these zip files as folders.  As it turns out, that extends to the Shell object as well.

Function AddFile(strFolder, strFile, blnKeepOriginal)

   Set objShell = CreateObject("Shell.Application")

   Set objFolder = objShell.NameSpace(strFolder)

   intCount = objFolder.Items.Count

   Select Case blnKeepOriginal

       Case True

          objFolder.CopyHere strFile, 256

       Case False

          objFolder.MoveHere strFile, 256

   End Select

   Do Until objFolder.Items.Count = intCount + 1

      WScript.Sleep 200

   Loop

End Function

My function requires that you provide the path to the zip file, the path to the file to add, and a Boolean value that indicates whether to keep the original file.  It uses the Shell object’s NameSpace method to return a folder object for the zip file.  It then either copies or moves a file into the compressed folder based upon the Boolean value that was supplied.

Here’s the catch: a Shell Copy or a Shell Move process will not halt execution of the script.  Therefore, you will need to create a timer routine to pause the script until it completes.  I’ve used a simple mechanism that checks the number of files in the zip folder beforehand and loops until that number is increased by 1.

Extracting files from a compressed folder is a matter of copying them back out using the Shell object.  I’ve chosen to create an ExtractAll method that will dump a compressed folder’s contents to a new folder.

Function ExtractAll(strZipFile, strFolder)

   Set objShell = CreateObject("Shell.Application")

   Set objFso = CreateObject("Scripting.FileSystemObject")

 

   If Not objFso.FolderExists(strFolder) Then

       objFso.CreateFolder(strFolder)

   End If

 

   intCount = objShell.NameSpace(strFolder).Items.Count

   Set colItems = objShell.NameSpace(strZipFile).Items

   objShell.NameSpace(strFolder).CopyHere colItems, 256

   Do Until objShell.NameSpace(strFolder).Items.Count = intCount + colItems.Count

       WScript.Sleep 200

   Loop

End Function

This is very similar to the AddFile function I just showed you.  Here, I’m copying files out of the compressed folder using the Shell object.  This time my timer relies on a file count of the destination folder.  Since the folder could have existing files, I check the number of files beforehand and then wait until it is increased by the number of items in the compressed folder.

This is not a perfect solution.  If a file with the same name exists, whether it is overwritten or not, the item count in the destination folder will be one short.

You will also notice that I’m not supplying each item individually, nor am I looping through the items.  I’m simple providing the whole collection to the CopyHere method and letting the Shell object sort it out.  Providing a collection to the Shell object’s CopyHere or MoveHere methods will copy or move a group of files in a single process.

If you want to allow your users to select a destination folder for the extraction process, you could use the function above along with a Browse For Folder dialog box, or you could simply harness the Windows Shell’s Compressed Folder support once again by invoking the Extraction Wizard.

Function ExtractAllW(strZipFile)

   Set objShell = CreateObject("Shell.Application")

   Set WshShell = CreateObject("WScript.Shell")

   objShell.NameSpace(strZipFile).Items.Item.InvokeVerb("Extract &All…")

   Do While WshShell.AppActivate("Extraction Wizard")

       WScript.Sleep 200

   Loop

End Function

This time I’ve named the function slightly differently, and we only need to provide a single string parameter containing a path to the compressed folder.  A little Shell magic is used to conjure the Extraction Wizard from the compressed folder’s context menu.

In order to create a timer loop this time, I’ve employed the WshShell object.  Its AppActivate method will bring a specified window into focus.  This method returns true if it completes successfully and false otherwise.  This Boolean return value can be used to determine whether or not the Extraction Wizard is still open.

So there you have it.  You can create compressed folders from within WSH using only native objects.  I’ve provided several very useful workarounds in this article that can be applied in countless other ways.  Take time to really play with them.  Until next time, keep coding!

4 thoughts on “Compressed Folders in WSH

  1. I was searching for this for a long time….

    Now, I know, I am asking for too much…But can we add a Password to this zip file too?

  2. absolute great. i am new to vbs. is there a sample file to download? i do not understand where to add the location of the .zip file to extract and the destination path to specify. i was looking for a function to extract zip files via a command line. thank you for the details :-)

[gp-comments width="770" linklove="off" ]