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