* transcode_data.h (rb_transcoder_stateful_type_t): defined.
[ruby-svn.git] / lib / net / ftp.rb
blobe0aa6f6adb0b8e7b8af436a6c2835b6f02e8370f
1
2 # = net/ftp.rb - FTP Client Library
3
4 # Written by Shugo Maeda <shugo@ruby-lang.org>.
6 # Documentation by Gavin Sinclair, sourced from "Programming Ruby" (Hunt/Thomas)
7 # and "Ruby In a Nutshell" (Matsumoto), used with permission.
8
9 # This library is distributed under the terms of the Ruby license.
10 # You can freely distribute/modify this library.
12 # It is included in the Ruby standard library.
14 # See the Net::FTP class for an overview.
17 require "socket"
18 require "monitor"
20 module Net
22   # :stopdoc:
23   class FTPError < StandardError; end
24   class FTPReplyError < FTPError; end
25   class FTPTempError < FTPError; end 
26   class FTPPermError < FTPError; end 
27   class FTPProtoError < FTPError; end
28   # :startdoc:
30   #
31   # This class implements the File Transfer Protocol.  If you have used a
32   # command-line FTP program, and are familiar with the commands, you will be
33   # able to use this class easily.  Some extra features are included to take
34   # advantage of Ruby's style and strengths.
35   #
36   # == Example
37   # 
38   #   require 'net/ftp'
39   #
40   # === Example 1
41   #  
42   #   ftp = Net::FTP.new('ftp.netlab.co.jp')
43   #   ftp.login
44   #   files = ftp.chdir('pub/lang/ruby/contrib')
45   #   files = ftp.list('n*')
46   #   ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
47   #   ftp.close
48   #
49   # === Example 2
50   #
51   #   Net::FTP.open('ftp.netlab.co.jp') do |ftp|
52   #     ftp.login
53   #     files = ftp.chdir('pub/lang/ruby/contrib')
54   #     files = ftp.list('n*')
55   #     ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
56   #   end
57   #
58   # == Major Methods
59   #
60   # The following are the methods most likely to be useful to users:
61   # - FTP.open
62   # - #getbinaryfile
63   # - #gettextfile
64   # - #putbinaryfile
65   # - #puttextfile
66   # - #chdir
67   # - #nlst
68   # - #size
69   # - #rename
70   # - #delete
71   #
72   class FTP
73     include MonitorMixin
74     
75     # :stopdoc:
76     FTP_PORT = 21
77     CRLF = "\r\n"
78     DEFAULT_BLOCKSIZE = 4096
79     # :startdoc:
80     
81     # When +true+, transfers are performed in binary mode.  Default: +true+.
82     attr_reader :binary
84     # When +true+, the connection is in passive mode.  Default: +false+.
85     attr_accessor :passive
87     # When +true+, all traffic to and from the server is written
88     # to +$stdout+.  Default: +false+.
89     attr_accessor :debug_mode
91     # Sets or retrieves the +resume+ status, which decides whether incomplete
92     # transfers are resumed or restarted.  Default: +false+.
93     attr_accessor :resume
95     # The server's welcome message.
96     attr_reader :welcome
98     # The server's last response code.
99     attr_reader :last_response_code
100     alias lastresp last_response_code
102     # The server's last response.
103     attr_reader :last_response
104     
105     #
106     # A synonym for <tt>FTP.new</tt>, but with a mandatory host parameter.
107     #
108     # If a block is given, it is passed the +FTP+ object, which will be closed
109     # when the block finishes, or when an exception is raised.
110     #
111     def FTP.open(host, user = nil, passwd = nil, acct = nil)
112       if block_given?
113         ftp = new(host, user, passwd, acct)
114         begin
115           yield ftp
116         ensure
117           ftp.close
118         end
119       else
120         new(host, user, passwd, acct)
121       end
122     end
123     
124     #
125     # Creates and returns a new +FTP+ object. If a +host+ is given, a connection
126     # is made. Additionally, if the +user+ is given, the given user name,
127     # password, and (optionally) account are used to log in.  See #login.
128     #
129     def initialize(host = nil, user = nil, passwd = nil, acct = nil)
130       super()
131       @binary = false
132       @passive = false
133       @debug_mode = false
134       @resume = false
135       if host
136         connect(host)
137         if user
138           login(user, passwd, acct)
139         end
140       end
141     end
143     def binary=(newmode)
144       if newmode != @binary
145         @binary = newmode
146         @binary ? voidcmd("TYPE I") : voidcmd("TYPE A")
147       end
148     end
150     def with_binary(newmode)
151       oldmode = binary
152       self.binary = newmode
153       begin
154         yield
155       ensure
156         self.binary = oldmode
157       end
158     end
159     private :with_binary
161     # Obsolete
162     def return_code
163       $stderr.puts("warning: Net::FTP#return_code is obsolete and do nothing")
164       return "\n"
165     end
167     # Obsolete
168     def return_code=(s)
169       $stderr.puts("warning: Net::FTP#return_code= is obsolete and do nothing")
170     end
172     def open_socket(host, port)
173       if defined? SOCKSsocket and ENV["SOCKS_SERVER"]
174         @passive = true
175         return SOCKSsocket.open(host, port)
176       else
177         return TCPSocket.open(host, port)
178       end
179     end
180     private :open_socket
181     
182     #
183     # Establishes an FTP connection to host, optionally overriding the default
184     # port. If the environment variable +SOCKS_SERVER+ is set, sets up the
185     # connection through a SOCKS proxy. Raises an exception (typically
186     # <tt>Errno::ECONNREFUSED</tt>) if the connection cannot be established.
187     #
188     def connect(host, port = FTP_PORT)
189       if @debug_mode
190         print "connect: ", host, ", ", port, "\n"
191       end
192       synchronize do
193         @sock = open_socket(host, port)
194         voidresp
195       end
196     end
198     #
199     # WRITEME or make private
200     #
201     def set_socket(sock, get_greeting = true)
202       synchronize do
203         @sock = sock
204         if get_greeting
205           voidresp
206         end
207       end
208     end
210     def sanitize(s)
211       if s =~ /^PASS /i
212         return s[0, 5] + "*" * (s.length - 5)
213       else
214         return s
215       end
216     end
217     private :sanitize
218     
219     def putline(line)
220       if @debug_mode
221         print "put: ", sanitize(line), "\n"
222       end
223       line = line + CRLF
224       @sock.write(line)
225     end
226     private :putline
227     
228     def getline
229       line = @sock.readline # if get EOF, raise EOFError
230       line.sub!(/(\r\n|\n|\r)\z/n, "")
231       if @debug_mode
232         print "get: ", sanitize(line), "\n"
233       end
234       return line
235     end
236     private :getline
237     
238     def getmultiline
239       line = getline
240       buff = line
241       if line[3] == ?-
242           code = line[0, 3]
243         begin
244           line = getline
245           buff << "\n" << line
246         end until line[0, 3] == code and line[3] != ?-
247       end
248       return buff << "\n"
249     end
250     private :getmultiline
251     
252     def getresp
253       @last_response = getmultiline
254       @last_response_code = @last_response[0, 3]
255       case @last_response_code
256       when /\A[123]/
257         return @last_response
258       when /\A4/
259         raise FTPTempError, @last_response
260       when /\A5/
261         raise FTPPermError, @last_response
262       else
263         raise FTPProtoError, @last_response
264       end
265     end
266     private :getresp
267     
268     def voidresp
269       resp = getresp
270       if resp[0] != ?2
271         raise FTPReplyError, resp
272       end
273     end
274     private :voidresp
275     
276     #
277     # Sends a command and returns the response.
278     #
279     def sendcmd(cmd)
280       synchronize do
281         putline(cmd)
282         return getresp
283       end
284     end
285     
286     #
287     # Sends a command and expect a response beginning with '2'.
288     #
289     def voidcmd(cmd)
290       synchronize do
291         putline(cmd)
292         voidresp
293       end
294     end
295     
296     def sendport(host, port)
297       af = (@sock.peeraddr)[0]
298       if af == "AF_INET"
299         cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",")
300       elsif af == "AF_INET6"
301         cmd = sprintf("EPRT |2|%s|%d|", host, port)
302       else
303         raise FTPProtoError, host
304       end
305       voidcmd(cmd)
306     end
307     private :sendport
308     
309     def makeport
310       sock = TCPServer.open(@sock.addr[3], 0)
311       port = sock.addr[1]
312       host = sock.addr[3]
313       resp = sendport(host, port)
314       return sock
315     end
316     private :makeport
317     
318     def makepasv
319       if @sock.peeraddr[0] == "AF_INET"
320         host, port = parse227(sendcmd("PASV"))
321       else
322         host, port = parse229(sendcmd("EPSV"))
323         #     host, port = parse228(sendcmd("LPSV"))
324       end
325       return host, port
326     end
327     private :makepasv
328     
329     def transfercmd(cmd, rest_offset = nil)
330       if @passive
331         host, port = makepasv
332         conn = open_socket(host, port)
333         if @resume and rest_offset
334           resp = sendcmd("REST " + rest_offset.to_s) 
335           if resp[0] != ?3
336             raise FTPReplyError, resp
337           end
338         end
339         resp = sendcmd(cmd)
340         # skip 2XX for some ftp servers
341         resp = getresp if resp[0] == ?2
342         if resp[0] != ?1
343           raise FTPReplyError, resp
344         end
345       else
346         sock = makeport
347         if @resume and rest_offset
348           resp = sendcmd("REST " + rest_offset.to_s) 
349           if resp[0] != ?3
350             raise FTPReplyError, resp
351           end
352         end
353         resp = sendcmd(cmd)
354         # skip 2XX for some ftp servers
355         resp = getresp if resp[0] == ?2
356         if resp[0] != ?1
357           raise FTPReplyError, resp
358         end
359         conn = sock.accept
360         sock.close
361       end
362       return conn
363     end
364     private :transfercmd
365     
366     def getaddress
367       thishost = Socket.gethostname
368       if not thishost.index(".")
369         thishost = Socket.gethostbyname(thishost)[0]
370       end
371       if ENV.has_key?("LOGNAME")
372         realuser = ENV["LOGNAME"]
373       elsif ENV.has_key?("USER")
374         realuser = ENV["USER"]
375       else
376         realuser = "anonymous"
377       end
378       return realuser + "@" + thishost
379     end
380     private :getaddress
381     
382     #
383     # Logs in to the remote host. The session must have been previously
384     # connected.  If +user+ is the string "anonymous" and the +password+ is
385     # +nil+, a password of <tt>user@host</tt> is synthesized. If the +acct+
386     # parameter is not +nil+, an FTP ACCT command is sent following the
387     # successful login.  Raises an exception on error (typically
388     # <tt>Net::FTPPermError</tt>).
389     #
390     def login(user = "anonymous", passwd = nil, acct = nil)
391       if user == "anonymous" and passwd == nil
392         passwd = getaddress
393       end
394       
395       resp = ""
396       synchronize do
397         resp = sendcmd('USER ' + user)
398         if resp[0] == ?3
399           raise FTPReplyError, resp if passwd.nil?
400           resp = sendcmd('PASS ' + passwd)
401         end
402         if resp[0] == ?3
403           raise FTPReplyError, resp if acct.nil?
404           resp = sendcmd('ACCT ' + acct)
405         end
406       end
407       if resp[0] != ?2
408         raise FTPReplyError, resp
409       end
410       @welcome = resp
411       self.binary = true
412     end
413     
414     #
415     # Puts the connection into binary (image) mode, issues the given command,
416     # and fetches the data returned, passing it to the associated block in
417     # chunks of +blocksize+ characters. Note that +cmd+ is a server command
418     # (such as "RETR myfile").
419     #
420     def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data
421       synchronize do
422         with_binary(true) do
423           conn = transfercmd(cmd, rest_offset)
424           loop do
425             data = conn.read(blocksize)
426             break if data == nil
427             yield(data)
428           end
429           conn.close
430           voidresp
431         end
432       end
433     end
434     
435     #
436     # Puts the connection into ASCII (text) mode, issues the given command, and
437     # passes the resulting data, one line at a time, to the associated block. If
438     # no block is given, prints the lines. Note that +cmd+ is a server command
439     # (such as "RETR myfile").
440     #
441     def retrlines(cmd) # :yield: line
442       synchronize do
443         with_binary(false) do
444           conn = transfercmd(cmd)
445           loop do
446             line = conn.gets
447             break if line == nil
448             if line[-2, 2] == CRLF
449               line = line[0 .. -3]
450             elsif line[-1] == ?\n
451               line = line[0 .. -2]
452             end
453             yield(line)
454           end
455           conn.close
456           voidresp
457         end
458       end
459     end
460     
461     #
462     # Puts the connection into binary (image) mode, issues the given server-side
463     # command (such as "STOR myfile"), and sends the contents of the file named
464     # +file+ to the server. If the optional block is given, it also passes it
465     # the data, in chunks of +blocksize+ characters.
466     #
467     def storbinary(cmd, file, blocksize, rest_offset = nil, &block) # :yield: data
468       if rest_offset
469         file.seek(rest_offset, IO::SEEK_SET)
470       end
471       synchronize do
472         with_binary(true) do
473           conn = transfercmd(cmd, rest_offset)
474           loop do
475             buf = file.read(blocksize)
476             break if buf == nil
477             conn.write(buf)
478             yield(buf) if block
479           end
480           conn.close
481           voidresp
482         end
483       end
484     rescue Errno::EPIPE
485       # EPIPE, in this case, means that the data connection was unexpectedly
486       # terminated.  Rather than just raising EPIPE to the caller, check the
487       # response on the control connection.  If getresp doesn't raise a more
488       # appropriate exception, re-raise the original exception.
489       getresp
490       raise
491     end
492     
493     #
494     # Puts the connection into ASCII (text) mode, issues the given server-side
495     # command (such as "STOR myfile"), and sends the contents of the file
496     # named +file+ to the server, one line at a time. If the optional block is
497     # given, it also passes it the lines.
498     #
499     def storlines(cmd, file, &block) # :yield: line
500       synchronize do
501         with_binary(false) do
502           conn = transfercmd(cmd)
503           loop do
504             buf = file.gets
505             break if buf == nil
506             if buf[-2, 2] != CRLF
507               buf = buf.chomp + CRLF
508             end
509             conn.write(buf)
510             yield(buf) if block
511           end
512           conn.close
513           voidresp
514         end
515       end
516     rescue Errno::EPIPE
517       # EPIPE, in this case, means that the data connection was unexpectedly
518       # terminated.  Rather than just raising EPIPE to the caller, check the
519       # response on the control connection.  If getresp doesn't raise a more
520       # appropriate exception, re-raise the original exception.
521       getresp
522       raise
523     end
525     #
526     # Retrieves +remotefile+ in binary mode, storing the result in +localfile+.
527     # If +localfile+ is nil, returns retrieved data.
528     # If a block is supplied, it is passed the retrieved data in +blocksize+
529     # chunks.
530     #
531     def getbinaryfile(remotefile, localfile = File.basename(remotefile),
532                       blocksize = DEFAULT_BLOCKSIZE) # :yield: data
533       result = nil
534       if localfile
535         if @resume
536           rest_offset = File.size?(localfile)
537           f = open(localfile, "a")
538         else
539           rest_offset = nil
540           f = open(localfile, "w")
541         end
542       elsif !block_given?
543         result = ""
544       end
545       begin
546         f.binmode if localfile
547         retrbinary("RETR " + remotefile, blocksize, rest_offset) do |data|
548           f.write(data) if localfile
549           yield(data) if block_given?
550           result.concat(data) if result
551         end
552         return result
553       ensure
554         f.close if localfile
555       end
556     end
557     
558     #
559     # Retrieves +remotefile+ in ASCII (text) mode, storing the result in
560     # +localfile+.
561     # If +localfile+ is nil, returns retrieved data.
562     # If a block is supplied, it is passed the retrieved data one
563     # line at a time.
564     #
565     def gettextfile(remotefile, localfile = File.basename(remotefile)) # :yield: line
566       result = nil
567       if localfile
568         f = open(localfile, "w")
569       elsif !block_given?
570         result = ""
571       end
572       begin
573         retrlines("RETR " + remotefile) do |line|
574           f.puts(line) if localfile
575           yield(line) if block_given?
576           result.concat(line + "\n") if result
577         end
578         return result
579       ensure
580         f.close if localfile
581       end
582     end
584     #
585     # Retrieves +remotefile+ in whatever mode the session is set (text or
586     # binary).  See #gettextfile and #getbinaryfile.
587     #
588     def get(remotefile, localfile = File.basename(remotefile),
589             blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
590       if @binary
591         getbinaryfile(remotefile, localfile, blocksize, &block)
592       else
593         gettextfile(remotefile, localfile, &block)
594       end
595     end
596     
597     #
598     # Transfers +localfile+ to the server in binary mode, storing the result in
599     # +remotefile+. If a block is supplied, calls it, passing in the transmitted
600     # data in +blocksize+ chunks.
601     #
602     def putbinaryfile(localfile, remotefile = File.basename(localfile),
603                       blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
604       if @resume
605         begin
606           rest_offset = size(remotefile)
607         rescue Net::FTPPermError
608           rest_offset = nil
609         end
610       else
611         rest_offset = nil
612       end
613       f = open(localfile)
614       begin
615         f.binmode
616         storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block)
617       ensure
618         f.close
619       end
620     end
621     
622     #
623     # Transfers +localfile+ to the server in ASCII (text) mode, storing the result
624     # in +remotefile+. If callback or an associated block is supplied, calls it,
625     # passing in the transmitted data one line at a time.
626     #
627     def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line
628       f = open(localfile)
629       begin
630         storlines("STOR " + remotefile, f, &block)
631       ensure
632         f.close
633       end
634     end
636     #
637     # Transfers +localfile+ to the server in whatever mode the session is set
638     # (text or binary).  See #puttextfile and #putbinaryfile.
639     #
640     def put(localfile, remotefile = File.basename(localfile),
641             blocksize = DEFAULT_BLOCKSIZE, &block)
642       if @binary
643         putbinaryfile(localfile, remotefile, blocksize, &block)
644       else
645         puttextfile(localfile, remotefile, &block)
646       end
647     end
649     #
650     # Sends the ACCT command.  TODO: more info.
651     #
652     def acct(account)
653       cmd = "ACCT " + account
654       voidcmd(cmd)
655     end
656     
657     #
658     # Returns an array of filenames in the remote directory.
659     #
660     def nlst(dir = nil)
661       cmd = "NLST"
662       if dir
663         cmd = cmd + " " + dir
664       end
665       files = []
666       retrlines(cmd) do |line|
667         files.push(line)
668       end
669       return files
670     end
671     
672     #
673     # Returns an array of file information in the directory (the output is like
674     # `ls -l`).  If a block is given, it iterates through the listing.
675     #
676     def list(*args, &block) # :yield: line
677       cmd = "LIST"
678       args.each do |arg|
679         cmd = cmd + " " + arg
680       end
681       if block
682         retrlines(cmd, &block)
683       else
684         lines = []
685         retrlines(cmd) do |line|
686           lines << line
687         end
688         return lines
689       end
690     end
691     alias ls list
692     alias dir list
693     
694     #
695     # Renames a file on the server.
696     #
697     def rename(fromname, toname)
698       resp = sendcmd("RNFR " + fromname)
699       if resp[0] != ?3
700         raise FTPReplyError, resp
701       end
702       voidcmd("RNTO " + toname)
703     end
704     
705     #
706     # Deletes a file on the server.
707     #
708     def delete(filename)
709       resp = sendcmd("DELE " + filename)
710       if resp[0, 3] == "250"
711         return
712       elsif resp[0] == ?5
713         raise FTPPermError, resp
714       else
715         raise FTPReplyError, resp
716       end
717     end
718     
719     #
720     # Changes the (remote) directory.
721     #
722     def chdir(dirname)
723       if dirname == ".."
724         begin
725           voidcmd("CDUP")
726           return
727         rescue FTPPermError => e
728           if e.message[0, 3] != "500"
729             raise e
730           end
731         end
732       end
733       cmd = "CWD " + dirname
734       voidcmd(cmd)
735     end
736     
737     #
738     # Returns the size of the given (remote) filename.
739     #
740     def size(filename)
741       with_binary(true) do
742         resp = sendcmd("SIZE " + filename)
743         if resp[0, 3] != "213" 
744           raise FTPReplyError, resp
745         end
746         return resp[3..-1].strip.to_i
747       end
748     end
749     
750     MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/  # :nodoc:
751     
752     #
753     # Returns the last modification time of the (remote) file.  If +local+ is
754     # +true+, it is returned as a local time, otherwise it's a UTC time.
755     #
756     def mtime(filename, local = false)
757       str = mdtm(filename)
758       ary = str.scan(MDTM_REGEXP)[0].collect {|i| i.to_i}
759       return local ? Time.local(*ary) : Time.gm(*ary)
760     end
761     
762     #
763     # Creates a remote directory.
764     #
765     def mkdir(dirname)
766       resp = sendcmd("MKD " + dirname)
767       return parse257(resp)
768     end
769     
770     #
771     # Removes a remote directory.
772     #
773     def rmdir(dirname)
774       voidcmd("RMD " + dirname)
775     end
776     
777     #
778     # Returns the current remote directory.
779     #
780     def pwd
781       resp = sendcmd("PWD")
782       return parse257(resp)
783     end
784     alias getdir pwd
785     
786     #
787     # Returns system information.
788     #
789     def system
790       resp = sendcmd("SYST")
791       if resp[0, 3] != "215"
792         raise FTPReplyError, resp
793       end
794       return resp[4 .. -1]
795     end
796     
797     #
798     # Aborts the previous command (ABOR command).
799     #
800     def abort
801       line = "ABOR" + CRLF
802       print "put: ABOR\n" if @debug_mode
803       @sock.send(line, Socket::MSG_OOB)
804       resp = getmultiline
805       unless ["426", "226", "225"].include?(resp[0, 3])
806         raise FTPProtoError, resp
807       end
808       return resp
809     end
810     
811     #
812     # Returns the status (STAT command).
813     #
814     def status
815       line = "STAT" + CRLF
816       print "put: STAT\n" if @debug_mode
817       @sock.send(line, Socket::MSG_OOB)
818       return getresp
819     end
820     
821     #
822     # Issues the MDTM command.  TODO: more info.
823     #
824     def mdtm(filename)
825       resp = sendcmd("MDTM " + filename)
826       if resp[0, 3] == "213"
827         return resp[3 .. -1].strip
828       end
829     end
830     
831     #
832     # Issues the HELP command.
833     #
834     def help(arg = nil)
835       cmd = "HELP"
836       if arg
837         cmd = cmd + " " + arg
838       end
839       sendcmd(cmd)
840     end
841     
842     #
843     # Exits the FTP session.
844     #
845     def quit
846       voidcmd("QUIT")
847     end
849     #
850     # Issues a NOOP command.
851     #
852     def noop
853       voidcmd("NOOP")
854     end
856     #
857     # Issues a SITE command.
858     #
859     def site(arg)
860       cmd = "SITE " + arg
861       voidcmd(cmd)
862     end
863     
864     #
865     # Closes the connection.  Further operations are impossible until you open
866     # a new connection with #connect.
867     #
868     def close
869       @sock.close if @sock and not @sock.closed?
870     end
871     
872     #
873     # Returns +true+ iff the connection is closed.
874     #
875     def closed?
876       @sock == nil or @sock.closed?
877     end
878     
879     def parse227(resp)
880       if resp[0, 3] != "227"
881         raise FTPReplyError, resp
882       end
883       left = resp.index("(")
884       right = resp.index(")")
885       if left == nil or right == nil
886         raise FTPProtoError, resp
887       end
888       numbers = resp[left + 1 .. right - 1].split(",")
889       if numbers.length != 6
890         raise FTPProtoError, resp
891       end
892       host = numbers[0, 4].join(".")
893       port = (numbers[4].to_i << 8) + numbers[5].to_i
894       return host, port
895     end
896     private :parse227
897     
898     def parse228(resp)
899       if resp[0, 3] != "228"
900         raise FTPReplyError, resp
901       end
902       left = resp.index("(")
903       right = resp.index(")")
904       if left == nil or right == nil
905         raise FTPProtoError, resp
906       end
907       numbers = resp[left + 1 .. right - 1].split(",")
908       if numbers[0] == "4"
909         if numbers.length != 9 || numbers[1] != "4" || numbers[2 + 4] != "2"
910           raise FTPProtoError, resp
911         end
912         host = numbers[2, 4].join(".")
913         port = (numbers[7].to_i << 8) + numbers[8].to_i
914       elsif numbers[0] == "6"
915         if numbers.length != 21 || numbers[1] != "16" || numbers[2 + 16] != "2"
916           raise FTPProtoError, resp
917         end
918         v6 = ["", "", "", "", "", "", "", ""]
919         for i in 0 .. 7
920           v6[i] = sprintf("%02x%02x", numbers[(i * 2) + 2].to_i,
921                           numbers[(i * 2) + 3].to_i)
922         end
923         host = v6[0, 8].join(":")
924         port = (numbers[19].to_i << 8) + numbers[20].to_i
925       end 
926       return host, port
927     end
928     private :parse228
929     
930     def parse229(resp)
931       if resp[0, 3] != "229"
932         raise FTPReplyError, resp
933       end
934       left = resp.index("(")
935       right = resp.index(")")
936       if left == nil or right == nil
937         raise FTPProtoError, resp
938       end
939       numbers = resp[left + 1 .. right - 1].split(resp[left + 1, 1])
940       if numbers.length != 4
941         raise FTPProtoError, resp
942       end
943       port = numbers[3].to_i
944       host = (@sock.peeraddr())[3]
945       return host, port
946     end
947     private :parse229
948     
949     def parse257(resp)
950       if resp[0, 3] != "257"
951         raise FTPReplyError, resp
952       end
953       if resp[3, 2] != ' "'
954         return ""
955       end
956       dirname = ""
957       i = 5
958       n = resp.length
959       while i < n
960         c = resp[i, 1]
961         i = i + 1
962         if c == '"'
963           if i > n or resp[i, 1] != '"'
964             break
965           end
966           i = i + 1
967         end
968         dirname = dirname + c
969       end
970       return dirname
971     end
972     private :parse257
973   end
978 # Documentation comments:
979 #  - sourced from pickaxe and nutshell, with improvements (hopefully)
980 #  - three methods should be private (search WRITEME)
981 #  - two methods need more information (search TODO)