'  
'   Rebex Sample Code License
' 
'   Copyright 2026, REBEX CR s.r.o.
'   All rights reserved.
'   https://www.rebex.net/
' 
'   Permission to use, copy, modify, and/or distribute this software for any
'   purpose with or without fee is hereby granted.
' 
'   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
'   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
'   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
'   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
'   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
'   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
'   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
'   OTHER DEALINGS IN THE SOFTWARE.
' 

Imports System
Imports System.IO
Imports System.Collections.Generic
Imports System.Text

Imports Rebex.IO
Imports Rebex.IO.Compression

''' <summary>
''' The working class of the simple command line ZIP archive manipulation utility.
''' </summary>
Public Class ZipSession
    ''' <summary>
    ''' ZIP archive root path.
    ''' </summary>
    Private ReadOnly RootPath = Path.DirectorySeparatorChar.ToString()

    ''' <summary>
    ''' ZIP archive.
    ''' </summary>
    Private _archive As ZipArchive

    ''' <summary>
    ''' Recursive operation mode.
    ''' </summary>
    Private _recursive As TraversalMode

    ''' <summary>
    ''' Current directory - ZipArchive doesn't have this concept, so we have to simulate it.
    ''' </summary>
    Private _currentDirectory As String

#Region "Helper methods"

    ''' <summary>
    ''' Parses arguments string into its parts.
    ''' </summary>
    ''' <param name="arguments">Raw arguments string.</param>
    ''' <param name="minParts">Minimum allowed parts.</param>
    ''' <param name="maxParts">Maximum allowed parts.</param>
    ''' <returns>Argument list.</returns>
    Private Shared Function Split(ByVal arguments As String, ByVal minParts As Integer, ByVal maxParts As Integer) As String()
        ' sample input: "one two three"
        ' sample output: {"one", "two", "three"}

        ' sample input: "one ""two three"""
        ' sample output: {"one", "two three"}

        Dim list As New List(Of String)()
        Dim quoted As Boolean = False
        Dim previous As Char = " "c
        Dim argument As New StringBuilder()
        For i As Integer = 0 To arguments.Length - 1
            Dim current As Char = arguments(i)
            Select Case current
                Case " "c
                    If quoted Then
                        GoTo case_else
                    End If

                    If previous <> " "c Then
                        list.Add(argument.ToString())
                        argument.Length = 0
                    End If
                    Exit Select
                Case """"c

                    If previous = """"c Then
                        argument.Append(""""c)
                    ElseIf Not quoted AndAlso previous <> " "c Then
                        Throw New SampleException(String.Format("Unexpected character in an argument at position {0}.", i))
                    End If

                    quoted = Not quoted
                    Exit Select
                Case Else
case_else:
                    If Not quoted AndAlso previous = """"c Then
                        Throw New SampleException(String.Format("Unexpected character in an argument at position {0}.", i))
                    End If

                    argument.Append(current)
                    Exit Select
            End Select
            previous = current
        Next

        If previous <> " "c Then
            list.Add(argument.ToString())
        End If

        If list.Count < minParts OrElse list.Count > maxParts Then
            Throw New SampleException("Invalid number of arguments.")
        End If

        Return list.ToArray()
    End Function

    ''' <summary>
    ''' Parses arguments string with a single argument.
    ''' </summary>
    ''' <param name="arguments">Raw arguments string.</param>
    ''' <returns>Argument value.</returns>
    Private Shared Function One(ByVal arguments As String) As String
        Return Split(arguments, 1, 1)(0)
    End Function

    ''' <summary>
    ''' Parses arguments string with a zero or one argument.
    ''' </summary>
    ''' <param name="arguments">Raw arguments string.</param>
    ''' <returns>Argument value or null.</returns>
    Private Shared Function OneOrNone(ByVal arguments As String) As String
        Dim parts As String() = Split(arguments, 0, 1)
        If parts.Length = 1 Then
            Return parts(0)
        Else
            Return Nothing
        End If
    End Function

    ''' <summary>
    ''' Reads password without displaying its characters.
    ''' </summary>
    ''' <returns>Password.</returns>
    Private Shared Function ReadPassword() As String
        Dim line As StringBuilder = New StringBuilder
        Dim complete As Boolean = False

        While (Not complete)
            Dim key As ConsoleKeyInfo = Console.ReadKey(True)
            Select Case key.Key
                Case ConsoleKey.Escape
                    Console.WriteLine()
                    Return Nothing

                Case ConsoleKey.Enter
                    complete = True

                Case ConsoleKey.Backspace
                    If (line.Length > 0) Then
                        line = line.Remove(line.Length - 1, 1)
                        Console.Write(key.KeyChar)
                        Console.Write(" ")
                        Console.Write(key.KeyChar)
                    End If

                Case Else
                    If ((key.KeyChar >= " "c) OrElse (key.Key = ConsoleKey.Tab)) Then
                        line = line.Append(key.KeyChar)
                        Console.Write("*")
                    End If

            End Select
        End While

        Console.WriteLine()
        Return line.ToString()
    End Function


#End Region

    ''' <summary>
    ''' Checks whether a ZIP archive is currently open.
    ''' </summary>
    Private Sub CheckArchiveOpen()
        If _archive Is Nothing Then
            Throw New SampleException("No ZIP archive is currently open.")
        End If
    End Sub

    ''' <summary>
    ''' Displays help - a list of supported commands and their arguments.
    ''' </summary>
    Public Sub Help()
        Console.WriteLine("Supported commands:")
        Console.WriteLine("  open zip-file-path        - open an existing ZIP archive")
        Console.WriteLine("  create zip-file-path      - create a new ZIP archive")
        Console.WriteLine("  close                     - close the currently opened ZIP archive")
        Console.WriteLine("  exit                      - exit this utility")
        Console.WriteLine("  recursive [on|off|deep]   - toggle or display recursive mode")
        Console.WriteLine("  level [0-9]               - set or display compression level")
        Console.WriteLine("  password [password]       - set password for add/extract")
        Console.WriteLine("  dir                       - display the list of items in the current directory")
        Console.WriteLine("  cd archive-dir            - change the current directory")
        Console.WriteLine("  mkdir archive-dir         - create a new directory in a ZIP archive")
        Console.WriteLine("  rmdir archive-dir         - delete an empty directory from a ZIP archive")
        Console.WriteLine("  delete path               - delete a file in a ZIP archive")
        Console.WriteLine("  info archive-item         - display information about the specified ZIP item")
        Console.WriteLine("  move from-path to-path    - move/rename an item in a ZIP archive")
        Console.WriteLine("  extract archive-path-or-mask [target-dir]    - extract the specified ZIP item(s)")
        Console.WriteLine("  add source-path-or-mask [archive-dir]        - add the specified item(s) into the ZIP archive")
    End Sub

    ''' <summary>
    ''' Opens or creates a ZIP archive at the specified path.
    ''' </summary>
    ''' <param name="path">Path to a ZIP archive.</param>
    ''' <param name="createNew">True to create a file, false to open existing file.</param>
    Public Sub Open(ByVal path As String, ByVal createNew As Boolean)
        ' close a currently opened ZIP archive (if open)
        Close()

        Try
            ' set ZIP archive mode
            Dim mode As ArchiveOpenMode
            If createNew Then
                mode = ArchiveOpenMode.CreateNew
            Else
                mode = ArchiveOpenMode.Open
            End If

            ' open a ZIP archive - create an instance of ZipArchive object
            _archive = New ZipArchive(path, mode)

            ' save all changes immediately
            _archive.SaveMode = ArchiveSaveMode.Immediate

            ' set up event handlers
            AddHandler _archive.ProgressChanged, AddressOf Event_ProgressChanged
            AddHandler _archive.ProblemDetected, AddressOf Event_ProblemDetected
            AddHandler _archive.PasswordRequired, AddressOf Event_PasswordRequired
        Catch [error] As UnauthorizedAccessException
            ' display an error if access denied
            Console.WriteLine([error].Message)
            Return
        Catch [error] As DirectoryNotFoundException
            ' display directory-not-found error
            Console.WriteLine([error].Message)
            Return
        Catch [error] As FileNotFoundException
            ' display file-not-found error
            Console.WriteLine([error].Message)
            Return
        End Try

        ' display basic information about the ZIP archive
        If Not createNew Then
            Console.WriteLine("ZIP archive at '{0}' opened.", _archive.FilePath)
            If Not String.IsNullOrEmpty(_archive.Comment) Then
                Console.WriteLine("Comment: {0}", _archive.Comment)
            End If
        Else
            Console.WriteLine("ZIP archive created at '{0}'.", _archive.FilePath)
        End If

        ' set current directory to ZIP archive's root
        _currentDirectory = RootPath
    End Sub

    ''' <summary>
    ''' Closes the currently opened ZIP archive.
    ''' </summary>
    Public Sub Close()
        If _archive Is Nothing Then
            Return
        End If

        Console.WriteLine("Closing ZIP archive {0}.", _archive.FilePath)

        ' close the ZIP archive
        _archive.Close(ArchiveSaveAction.None)
        _archive = Nothing
    End Sub

    ''' <summary>
    ''' Displays a list of files and subdirectories in the current directory in a ZIP archive.
    ''' </summary>
    Public Sub Dir()
        CheckArchiveOpen()

        ' get the list of items in the current directory
        Dim items As ZipItemCollection = _archive.GetItems(Path.Combine(_currentDirectory, "*"), TraversalMode.NonRecursive)

        ' sort the files by path (directories first)
        items.Sort()

        ' display directory path and number of items
        Console.WriteLine("Directory '{0}' contains {1} item{2}.", _currentDirectory, items.Count, IIf(items.Count <> 1, "s", ""))

        ' display the items
        For Each item As ZipItem In items
            If item.LastWriteTime.HasValue Then
                Console.Write(item.LastWriteTime.Value.ToString("u").TrimEnd("Z"c))
            Else
                Console.Write(" ".PadRight(19, " "c))
            End If

            If item.IsDirectory Then
                Console.Write("   <DIR>".PadRight(15, " "c))
            Else
                Console.Write(item.Length.ToString().PadLeft(15, " "c))
            End If

            Console.Write(" {0}", item.Name)
            Console.WriteLine()
        Next
    End Sub

    ''' <summary>
    ''' Changes the current directory in a ZIP archive.
    ''' </summary>
    ''' <param name="filePath">Path in a ZIP archive.</param>
    Public Sub ChangeDirectory(ByVal filePath As String)
        CheckArchiveOpen()

        ' change the current directory if an argument was specified
        If filePath IsNot Nothing Then
            ' expand path
            filePath = Path.Combine(_currentDirectory, filePath)

            ' get item info - fail if the filePath doesn't exist or if it's not a directory
            Dim item As ZipItem = _archive.GetItem(filePath)
            If item Is Nothing OrElse Not item.IsDirectory Then
                Throw New SampleException("Directory doesn't exist.")
            End If

            ' set the current directory to the item's filePath
            _currentDirectory = item.Path
        End If

        ' display current directory
        Console.WriteLine("Current filePath is '{0}'.", _currentDirectory)
    End Sub

    ''' <summary>
    ''' Creates a new directory in a ZIP archive.
    ''' </summary>
    ''' <param name="filePath">Path in a ZIP archive.</param>
    Public Sub CreateDirectory(ByVal filePath As String)
        CheckArchiveOpen()

        ' expand path
        filePath = Path.Combine(_currentDirectory, filePath)

        ' create a new directory
        Dim directory As ZipItem = _archive.CreateDirectory(filePath)

        ' display its filePath
        Console.WriteLine("Directory '{0}' was created.", directory.Path)
    End Sub

    ''' <summary>
    ''' Deletes the specified directory in a ZIP archive.
    ''' </summary>
    ''' <param name="filePath">Path in a ZIP archive.</param>
    Public Sub RemoveDirectory(ByVal filePath As String)
        CheckArchiveOpen()

        ' expand filePath
        filePath = Path.Combine(_currentDirectory, filePath)

        ' get item info - fail if the filePath doesn't exist or if it's not a directory
        Dim item As ZipItem = _archive.GetItem(filePath)
        If item Is Nothing OrElse Not item.IsDirectory Then
            Throw New SampleException("Directory doesn't exist.")
        End If

        ' if the directory is not empty, ask whether to delete it anyway
        If Not item.IsEmptyDirectory Then
            Console.Write("Directory is not empty. Remove anyway? [y/n] ")
            Dim answer As String = Console.ReadLine()
            If Not answer.Equals("y", StringComparison.InvariantCultureIgnoreCase) Then
                Return
            End If
        End If

        ' delete the item recursively and remove unused space
        Dim result As ArchiveOperationResult = item.Delete(TraversalMode.Recursive, ArchiveSaveAction.Shrink)

        ' display information about the results
        Console.WriteLine("Directory '{0}' (and {1} file{2}) was removed.", item.Path, result.FilesAffected, IIf(result.FilesAffected = 1, "s", ""))

        ' in case the current directory was deleted, find the first upper-level directory that exists
        Dim currentDirectoryRemoved As Boolean = False
        Dim currentDir As ZipItem = _archive.GetItem(_currentDirectory)
        While currentDir Is Nothing OrElse Not currentDir.IsDirectory
            _currentDirectory = Path.Combine(_currentDirectory, "..")
            currentDir = _archive.GetItem(_currentDirectory)
            currentDirectoryRemoved = True
        End While

        ' display a new current directory if the original one was removed
        If currentDirectoryRemoved Then
            _currentDirectory = currentDir.Path
            Console.WriteLine("Current filePath changed to '{0}'.", _currentDirectory)
        End If
    End Sub

    ''' <summary>
    ''' Deletes the specified file in a ZIP archive.
    ''' </summary>
    ''' <param name="filePath">Path in a ZIP archive.</param>
    Public Sub DeleteFile(ByVal filePath As String)
        CheckArchiveOpen()

        ' expand path
        filePath = Path.Combine(_currentDirectory, filePath)

        ' get item info - fail if the filePath doesn't exist or if it's not a file
        Dim item As ZipItem = _archive.GetItem(filePath)
        If item Is Nothing OrElse Not item.IsFile Then
            Throw New SampleException("File doesn't exist.")
        End If

        ' delete the item and remove unused space
        item.Delete(ArchiveSaveAction.Shrink)

        Console.WriteLine("File '{0}' was deleted.", item.Path)
    End Sub

    ''' <summary>
    ''' Move an item (file or directory) in a ZIP archive to a new path.
    ''' </summary>
    ''' <param name="arguments"></param>
    Public Sub Move(ByVal arguments As String())
        CheckArchiveOpen()

        ' expand paths
        Dim fromPath As String = Path.Combine(_currentDirectory, arguments(0))
        Dim toPath As String = Path.Combine(_currentDirectory, arguments(1))

        ' move the item
        _archive.Move(fromPath, toPath)

        Console.WriteLine("Moved successfully.")
    End Sub

    ''' <summary>
    ''' Displays information about an item (file or directory) in a ZIP archive.
    ''' </summary>
    ''' <param name="filePath">Path in a ZIP archive.</param>
    Public Sub Info(ByVal filePath As String)
        CheckArchiveOpen()

        ' expand filePath
        filePath = Path.Combine(_currentDirectory, filePath)

        ' get item info - fail if the filePath doesn't exist
        Dim item As ZipItem = _archive.GetItem(filePath)
        If item Is Nothing Then
            Throw New SampleException("Item doesn't exist.")
        End If

        ' display item info

        Console.WriteLine("Item filePath: {0}", item.Path)
        Console.WriteLine("Item type: {0}", item.ItemType)
        If item.IsEncrypted Then
            Console.WriteLine("Encryption algorithm: {0}", item.EncryptionAlgorithm)
        End If
        If item.IsFile Then
            Console.WriteLine("Original length: {0}", item.Length)
            Console.WriteLine("Compressed length: {0}", item.CompressedLength)
            Console.WriteLine("Compression method: {0}", item.CompressionMethod)
            Console.WriteLine("CRC value: 0x{0:X8}", item.Crc32)
        End If
        If item.CreationTime.HasValue Then
            Console.WriteLine("Creation date: {0}", item.CreationTime.Value.ToString("u").TrimEnd("Z"c))
        End If
        If item.LastWriteTime.HasValue Then
            Console.WriteLine("Last modification date: {0}", item.LastWriteTime.Value.ToString("u").TrimEnd("Z"c))
        End If
        If item.LastAccessTime.HasValue Then
            Console.WriteLine("Last access date: {0}", item.LastAccessTime.Value.ToString("u").TrimEnd("Z"c))
        End If
        If Not String.IsNullOrEmpty(item.Comment) Then
            Console.WriteLine("Comment: {0}", item.Comment)
        End If
    End Sub

    ''' <summary>
    ''' Sets or displays recursive level.
    ''' </summary>
    ''' <param name="mode">Mode.</param>
    Public Sub Recursive(ByVal mode As String)
        ' set recursive mode if specified
        Select Case mode
            Case Nothing
                Exit Select
            Case "on", "1"
                _recursive = ArchiveTraversalMode.Recursive
                Exit Select
            Case "off", "0"
                _recursive = ArchiveTraversalMode.NonRecursive
                Exit Select
            Case "deep"
                _recursive = ArchiveTraversalMode.DeepMatch
                Exit Select
            Case Else
                Throw New SampleException("Invalid argument.")
        End Select

        ' display current recursive mode
        Console.Write("Recursive mode: ")
        Select Case _recursive
            Case ArchiveTraversalMode.Recursive
                Console.Write("on")
                Exit Select
            Case ArchiveTraversalMode.DeepMatch
                Console.Write("deep")
                Exit Select
            Case ArchiveTraversalMode.NonRecursive
                Console.Write("off")
                Exit Select
        End Select
        Console.WriteLine()
    End Sub

    ''' <summary>
    ''' Sets or displays compression level.
    ''' </summary>
    ''' <param name="rawlevel">Compression level.</param>
    Public Sub Level(ByVal rawLevel As String)
        CheckArchiveOpen()

        If rawLevel IsNot Nothing Then
            ' if an argument was specified, try parsing it as a value of 0 to 9
            Dim level As Integer
            If Not Integer.TryParse(rawLevel, level) OrElse level < 0 OrElse level > 9 Then
                Throw New SampleException("Invalid level.")
            End If

            ' set the compression level
            _archive.CompressionLevel = level
        End If

        ' display current compression level
        Console.WriteLine("Compression level: {0}", _archive.CompressionLevel)
    End Sub

    ''' <summary>
    ''' Sets password to be used for compression (and decompression if needed).
    ''' </summary>
    Public Sub Password(ByVal newPassword As String)
        CheckArchiveOpen()

        ' if no password was specified, ask for it
        If newPassword Is Nothing Then
            Console.Write("Enter password (Esc to reset): ")
            newPassword = ReadPassword()
        End If

        ' set password for compression and decompression
        _archive.Password = newPassword

        If Not newPassword Is Nothing Then
            Console.WriteLine("Password set.")
        Else
            Console.WriteLine("Password reset.")
        End If
    End Sub

    ''' <summary>
    ''' Extracts (decompresses) items from a ZIP archive.
    ''' </summary>
    ''' <param name="arguments"></param> 
    Public Sub Extract(ByVal arguments As String())
        CheckArchiveOpen()

        ' expand paths
        Dim archivePathOrMask As String = Path.Combine(_currentDirectory, arguments(0))
        Dim targetDirectoryPath As String
        If arguments.Length = 2 Then
            targetDirectoryPath = arguments(1)
        Else
            targetDirectoryPath = "."
        End If

        ' extract items from a ZIP archive
        Dim result As ArchiveOperationResult = _archive.Extract(archivePathOrMask, targetDirectoryPath, _recursive, TransferMethod.Copy, ActionOnExistingFiles.ThrowException)

        ' display information about the results
        Console.WriteLine("{0} file{1} extracted to '{2}'.", _
            result.FilesAffected, _
            IIf(result.FilesAffected = 1, " was", "s were"), _
            Path.GetFullPath(targetDirectoryPath))
    End Sub

    ''' <summary>
    ''' Adds (compresses) items to a ZIP archive.
    ''' </summary>
    ''' <param name="arguments"></param>
    Public Sub AddFiles(ByVal arguments As String())
        CheckArchiveOpen()

        ' expand paths
        Dim sourcePathOrMask As String = arguments(0)
        Dim archiveDirectoryPath As String
        If arguments.Length = 2 Then
            archiveDirectoryPath = Path.Combine(_currentDirectory, arguments(1))
        Else
            archiveDirectoryPath = _currentDirectory
        End If

        ' add items to a ZIP archive
        Dim result As ArchiveOperationResult = _archive.Add(sourcePathOrMask, archiveDirectoryPath, _recursive, TransferMethod.Copy, ActionOnExistingFiles.ThrowException)

        ' display information about the results
        Console.WriteLine("{0} file{1} added.", result.FilesAffected, IIf(result.FilesAffected = 1, " was", "s were"))
    End Sub

    ''' <summary>
    ''' Event handler that displays progress of extract (decompress) or add (compress) operations.
    ''' </summary>
    ''' <param name="sender">Sender object.</param>
    ''' <param name="e">Event arguments.</param>
    Private Sub Event_ProgressChanged(ByVal sender As Object, ByVal e As ZipProgressChangedEventArgs)
        ' determine source path
        Dim sourcePath As String
        If e.Operation = ArchiveOperation.Extract Then
            sourcePath = e.ArchiveItemPath
        Else
            sourcePath = e.ExternalItemPath
        End If

        ' display info about file processing
        Select Case e.OperationStep
            Case ArchiveOperationStep.FileProcessing
                ' file processing started
                Console.Write("{0} {1}: ", e.Operation, sourcePath)
                Exit Select
            Case ArchiveOperationStep.FileDataBlockProcessed
                ' file is being processed
                Console.Write("" & Chr(13) & "{0} {1}: {2}%", e.Operation, sourcePath, e.CurrentFileProgressPercentage)
                Exit Select
            Case ArchiveOperationStep.FileProcessed
                ' file processing done
                Console.WriteLine()
                Exit Select
        End Select
    End Sub

    ''' <summary>
    ''' Event handler that is called when a problem is detected during extract (decompress) or add (compress) operations.
    ''' </summary>
    ''' <param name="sender">Sender object.</param>
    ''' <param name="e">Event arguments.</param>
    Private Sub Event_ProblemDetected(ByVal sender As Object, ByVal e As ZipProblemDetectedEventArgs)
        ' we only handle 'file exists' problems
        If e.ProblemType = ArchiveProblemType.FileExists Then
            ' determine the target path
            Dim targetPath As String
            If e.Operation = ArchiveOperation.Add Then
                targetPath = e.ArchiveItemPath
            Else
                targetPath = e.ExternalItemPath
            End If

            ' construct a question about possible actions
            Dim question As New StringBuilder()
            question.AppendFormat("Replace {0}?", targetPath)

            If e.IsActionPossible(ArchiveProblemActions.Overwrite) Then
                question.Append(" [y]es")
            End If

            If e.IsActionPossible(ArchiveProblemActions.Skip) Then
                question.Append(" [n]o")
            End If

            If e.IsActionPossible(ArchiveProblemActions.Cancel) Then
                question.Append(" [c]ancel")
            End If

            question.Append(": ")


            Dim action As ArchiveProblemActions = 0
            While action = 0
                ' display the question and read its response
                Console.Write(question)
                Dim answer As String = Console.ReadLine()

                Select Case answer.ToLowerInvariant()
                    Case "y"
                        action = ArchiveProblemActions.Overwrite
                        Exit Select
                    Case "n"
                        action = ArchiveProblemActions.Skip
                        Exit Select
                    Case "c"
                        action = ArchiveProblemActions.Cancel
                        Exit Select
                End Select
            End While

            ' set the desired action
            e.Action = action
        End If
    End Sub

    ''' <summary>
    '''Event handler that is called when a password is required during extract (decompress) operation.
    ''' </summary>
    ''' <param name="sender">Sender object.</param>
    ''' <param name="e">Event arguments.</param>
    Private Sub Event_PasswordRequired(ByVal sender As Object, ByVal e As ZipPasswordRequiredEventArgs)
        ' display the reason
        Select Case e.Reason
            Case ArchivePasswordReason.IncorrectPasswordOrCorruptedData
                Console.WriteLine("Incorrect password or corrupted data.")
            Case ArchivePasswordReason.PasswordRequired
                Console.WriteLine("Password required.")
        End Select

        ' ask for password
        Console.Write("Enter password (Esc to cancel): ")
        Dim password As String = ReadPassword()

        ' set password or cancel operation
        If password Is Nothing Then
            e.Action = ArchivePasswordActions.Cancel
        Else
            e.Password = password
        End If
    End Sub

    ''' <summary>
    ''' The main working group.
    ''' </summary>
    Public Sub Run()
        Console.WriteLine("Type 'help' to see a list of available commands.")

        ' main command loop
        While True
            Console.Write("> ")

            ' read a command
            Dim command As String = Console.ReadLine()
            If command Is Nothing Then
                Exit While
            End If

            ' parse the command into "command" and "arguments" parts
            Dim arguments As String
            Dim firstSpace As Integer = command.IndexOf(" "c)
            If firstSpace >= 0 Then
                arguments = command.Substring(firstSpace + 1)
                command = command.Substring(0, firstSpace)
            Else
                arguments = ""
            End If

            Try
                ' execute the desired command
                Select Case command.ToLowerInvariant()
                    Case "help"
                        Help()
                    Case "open"
                        Open(One(arguments), False)
                    Case "create"
                        Open(One(arguments), True)
                    Case "close"
                        Close()
                    Case "quit", "exit"
                        Close()
                        Return
                    Case "dir"
                        Dir()
                    Case "cd"
                        ChangeDirectory(OneOrNone(arguments))
                    Case "mkdir", "md"
                        CreateDirectory(One(arguments))
                    Case "rmdir", "rm"
                        RemoveDirectory(One(arguments))
                    Case "delete"
                        DeleteFile(One(arguments))
                    Case "move"
                        Move(Split(arguments, 2, 2))
                    Case "info"
                        Info(One(arguments))
                    Case "recursive"
                        Recursive(OneOrNone(arguments))
                    Case "level"
                        Level(OneOrNone(arguments))
                    Case "password"
                        Password(OneOrNone(arguments))
                    Case "extract"
                        Extract(Split(arguments, 1, 2))
                    Case "add"
                        AddFiles(Split(arguments, 1, 2))
                    Case Else
                        Console.WriteLine("Unknown command '{0}'.", command)
                End Select
            Catch [error] As SampleException
                ' display an error reported by the ZipConsole sample
                Console.WriteLine([error].Message)
            Catch [error] As ZipException
                ' display an error reported by the ZIP component
                Console.WriteLine([error].Message)
            End Try
        End While
    End Sub
End Class
