[Win32utils-devel] Prototyping the Dir class

Daniel Berger djberg96 at gmail.com
Thu Oct 4 00:11:34 EDT 2007


Hi all,

I've been prototyping a Windows-only Dir class for Ruby. Below is what 
I've got so far. The class methods were easy, except for Dir.glob. The 
code in dir.c is just nasty, and I can't help but think that it could be 
heavily refactored. I did come across this link:

http://www.codeproject.com/file/fileglob.asp

But I haven't investigated it yet. Anyone who wants to take a stab at a 
pure Ruby Dir.glob is welcome to try. I made one attempt to convert the 
pattern to Regexp objects, but it got pretty hairy, especially with 
'[a-z]' type ranges, i.e. match only one character instead of any one 
character. I'd be interested to see what techniques anyone else comes up 
with.

The other issue is how to design Dir.new. I thought the smart thing to 
do would be to store the handle returned by FindFirstFile(), but then I 
realized that the handle returned by FindFirstFile() isn't compatible 
with the handle used for SetFilePointer(), which would make seek/tell 
difficult to implement. On the other hand, if we use CreateFile(), its 
handle is not compatible with FindFirstFile() or FindNextFile(), making 
read/each difficult to implement.

I make two refactoring decisions for now - Dir.foreach has been skipped. 
The Dir.entries now takes an optional block. Dir.open has been skipped. 
The Dir.new method now takes an optional block.

Anyway, suggestions welcome on the code below. BTW, you'll want to grab 
the latest windows-pr code from CVS for some of this code to work.

Regards,

Dan

# dir.rb

require 'windows/file'
require 'windows/error'
require 'windows/unicode'
require 'windows/directory'
require 'windows/process'
require 'windows/handle'

# Struct sizes (ANSI/Wide)
#
# WIN32_FIND_DATA: 320/592
#    [0,4]    => dwFileAttributes
#    [4,8]   => ftCreationTime
#    [12,8]  => ftLastAccessTime
#    [20,8]  => ftLastWriteTime
#    [28,4]   => nFileSizeHigh
#    [32,4]   => nFileSizeLow
#    [36,4]   => dwReserved0
#    [40,4]   => dwReserved1
#    [44,260/520] => cFileName[MAX_PATH]
#    [304,14] => cAlternateFileName[14]

class MyDir
    include Windows::Error
    include Windows::File
    include Windows::Unicode
    include Windows::Directory
    include Windows::Process
    include Windows::Handle
    extend Windows::Error
    extend Windows::File
    extend Windows::Unicode
    extend Windows::Directory
    extend Windows::Process
    extend Windows::Handle

    MAX_PATH = 260

    def self.chdir(dir = nil, &block)
       if dir.nil?
          buf = 0.chr * 1024 # 32k is the official limit

          if GetEnvironmentVariable('USERPROFILE', buf, buf.size) == 0
             if GetEnvironmentVariable('HOME', buf, buf.size) == 0
                raise ArgumentError, 'USERPROFILE/HOME not set'
             end
          end

          dir = buf.unpack("Z*")[0]
       end

       if block_given?
          begin
             buf = 0.chr * MAX_PATH

             if GetCurrentDirectory(buf.length, buf) == 0
                raise ArgumentError, get_last_error
             else
                # MSDN says the drive letter could be dropped,
                # and that GetFullPathName should be called just in case.
                current = buf.unpack("Z*")[0]
                buf2 = 0.chr * MAX_PATH

                if GetFullPathName(current, buf2.length, buf2, 0) == 0
                   raise ArgumentError, get_last_error
                end

                current = buf2.unpack("Z*")[0]
             end

             unless SetCurrentDirectory(dir)
                raise ArgumentError, get_last_error
             end

             block.call
          ensure
             SetCurrentDirectory(current)
          end
       else
          unless SetCurrentDirectory(dir)
             raise ArgumentError, get_last_error
          end
       end
    end

    def self.delete(dirname)
       unless RemoveDirectory(dirname)
          raise ArgumentError, get_last_error
       end
    end

    # Blend entries and foreach into one method
    def self.entries(dirname)
       dirname += "\\*"
       fdata = 0.chr * 320 # 580 if wide
       array = block_given? ? [] : nil

       hfind = FindFirstFile(dirname, fdata)

       if hfind == INVALID_HANDLE_VALUE
          raise ArgumentError, get_last_error
       end

       file = fdata[44, MAX_PATH].unpack("Z*")[0]

       if block_given?
          yield file
       else
          array << file
       end

       while FindNextFile(hfind, fdata)
          file = fdata[44, MAX_PATH].unpack("Z*")[0]
          if block_given?
          	yield file
          else
             array << file
          end
       end

       error = GetLastError()

       FindClose(hfind)

       if error != ERROR_NO_MORE_FILES
          raise get_last_error(error)
       end

       array
    end

    def self.getwd
       buf = 0.chr * MAX_PATH

       if GetCurrentDirectory(buf.length, buf) == 0
          raise ArgumentError, get_last_error
       end

       buf.unpack("Z*")[0]
    end

    # The 'permissions' could be a Security::Attributes object
    # of some sort.
    #
    def self.mkdir(dirname, permissions = nil)
       unless CreateDirectory(dirname, permissions)
          raise ArgumentError, get_last_error
       end
    end

    attr_reader :path

    # Blend new and open into one method
    def initialize(path)
       @path = path + "\\*"
       @path.tr!(File::SEPARATOR, File::ALT_SEPARATOR)

       @fdata  = 0.chr * 320 # 580 if wide
       @handle = FindFirstFile(@path, @fdata)

       if @handle == INVALID_HANDLE_VALUE
           raise ArgumentError, get_last_error
       end

       if block_given?
          begin
             yield @handle
          ensure
             close
          end
       end

       @pos = 0

       @handle
    end

    def close
       FindClose(@handle)
    end

    # Broken because the @handle isn't valid
    def pos
       SetFilePointer(@handle, 0, 0, FILE_CURRENT)
    end

    def read
       if @pos > 0
          @fdata = 0.chr * 320
          unless FindNextFile(@handle, @fdata)
             raise ArgumentError, get_last_error
          end
       end
       @pos += 1
       @fdata[44, MAX_PATH].unpack("Z*")[0]
    end

    def each
       @fdata = 0.chr * 320
       while FindNextFile(@handle, @fdata)
          yield @fdata[44, MAX_PATH].unpack("Z*")[0]
       end
    end

    def rewind
       unless SetFilePointerEx(@handle,0,nil,0)
          raise ArgumentError, get_last_error
       end
    end

    # class level aliases

    class << self
       alias open new
       alias foreach entries
       alias pwd getwd
       alias rmdir delete
       alias unlink delete
    end
end

if $0 == __FILE__
    dir = MyDir.new(Dir.pwd)
    p dir.pos
    dir.read
    p dir.pos
    dir.close
end


More information about the win32utils-devel mailing list