This file is indexed.

/usr/lib/ruby/vendor_ruby/net/sftp/session.rb is in ruby-net-sftp 1:2.0.5-3.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
require 'net/ssh'
require 'net/sftp/constants'
require 'net/sftp/errors'
require 'net/sftp/protocol'
require 'net/sftp/request'
require 'net/sftp/operations/dir'
require 'net/sftp/operations/upload'
require 'net/sftp/operations/download'
require 'net/sftp/operations/file_factory'

module Net; module SFTP

  # The Session class encapsulates a single SFTP channel on a Net::SSH
  # connection. Instances of this class are what most applications will
  # interact with most, as it provides access to both low-level (mkdir,
  # rename, remove, symlink, etc.) and high-level (upload, download, etc.)
  # SFTP operations.
  #
  # Although Session makes it easy to do SFTP operations serially, you can
  # also set up multiple operations to be done in parallel, too, without
  # needing to resort to threading. You merely need to fire off the requests,
  # and then run the event loop until all of the requests have completed:
  #
  #   handle1 = sftp.open!("/path/to/file1")
  #   handle2 = sftp.open!("/path/to/file2")
  #
  #   r1 = sftp.read(handle1, 0, 1024)
  #   r2 = sftp.read(handle2, 0, 1024)
  #   sftp.loop { [r1, r2].any? { |r| r.pending? } }
  #
  #   puts "chunk #1: #{r1.response[:data]}"
  #   puts "chunk #2: #{r2.response[:data]}"
  #
  # By passing blocks to the operations, you can set up powerful state
  # machines, to fire off subsequent operations. In fact, the Net::SFTP::Operations::Upload
  # and Net::SFTP::Operations::Download classes set up such state machines, so that
  # multiple uploads and/or downloads can be running simultaneously.
  #
  # The convention with the names of the operations is as follows: if the method
  # name ends with an exclamation mark, like #read!, it will be synchronous
  # (e.g., it will block until the server responds). Methods without an
  # exclamation mark (e.g. #read) are asynchronous, and return before the
  # server has responded. You will need to make sure the SSH event loop is
  # run in order to process these requests. (See #loop.)
  class Session
    include Net::SSH::Loggable
    include Net::SFTP::Constants::PacketTypes

    # The highest protocol version supported by the Net::SFTP library.
    HIGHEST_PROTOCOL_VERSION_SUPPORTED = 6

    # A reference to the Net::SSH session object that powers this SFTP session.
    attr_reader :session

    # The Net::SSH::Connection::Channel object that the SFTP session is being
    # processed by.
    attr_reader :channel

    # The state of the SFTP connection. It will be :opening, :subsystem, :init,
    # :open, or :closed.
    attr_reader :state

    # The protocol instance being used by this SFTP session. Useful for
    # querying the protocol version in effect.
    attr_reader :protocol

    # The hash of pending requests. Any requests that have been sent and which
    # the server has not yet responded to will be represented here.
    attr_reader :pending_requests

    # Creates a new Net::SFTP instance atop the given Net::SSH connection.
    # This will return immediately, before the SFTP connection has been properly
    # initialized. Once the connection is ready, the given block will be called.
    # If you want to block until the connection has been initialized, try this:
    #
    #   sftp = Net::SFTP::Session.new(ssh)
    #   sftp.loop { sftp.opening? }
    def initialize(session, &block)
      @session    = session
      @input      = Net::SSH::Buffer.new
      self.logger = session.logger
      @state      = :closed

      connect(&block)
    end

    public # high-level SFTP operations

      # Initiates an upload from +local+ to +remote+, asynchronously. This
      # method will return a new Net::SFTP::Operations::Upload instance, and requires
      # the event loop to be run in order for the upload to progress. See
      # Net::SFTP::Operations::Upload for a full discussion of how this method can be
      # used.
      #
      #   uploader = sftp.upload("/local/path", "/remote/path")
      #   uploader.wait
      def upload(local, remote, options={}, &block)
        Operations::Upload.new(self, local, remote, options, &block)
      end

      # Identical to #upload, but blocks until the upload is complete.
      def upload!(local, remote, options={}, &block)
        upload(local, remote, options, &block).wait
      end

      # Initiates a download from +remote+ to +local+, asynchronously. This
      # method will return a new Net::SFTP::Operations::Download instance, and requires
      # that the event loop be run in order for the download to progress. See
      # Net::SFTP::Operations::Download for a full discussion of hos this method can be
      # used.
      #
      #   download = sftp.download("/remote/path", "/local/path")
      #   download.wait
      def download(remote, local, options={}, &block)
        Operations::Download.new(self, local, remote, options, &block)
      end

      # Identical to #download, but blocks until the download is complete.
      # If +local+ is omitted, downloads the file to an in-memory buffer
      # and returns the result as a string; otherwise, returns the
      # Net::SFTP::Operations::Download instance.
      def download!(remote, local=nil, options={}, &block)
        require 'stringio' unless defined?(StringIO)
        destination = local || StringIO.new
        result = download(remote, destination, options, &block).wait
        local ? result : destination.string
      end

      # Returns an Net::SFTP::Operations::FileFactory instance, which can be used to
      # mimic synchronous, IO-like file operations on a remote file via
      # SFTP.
      #
      #   sftp.file.open("/path/to/file") do |file|
      #     while line = file.gets
      #       puts line
      #     end
      #   end
      #
      # See Net::SFTP::Operations::FileFactory and Net::SFTP::Operations::File for more details.
      def file
        @file ||= Operations::FileFactory.new(self)
      end

      # Returns a Net::SFTP::Operations::Dir instance, which can be used to
      # conveniently iterate over and search directories on the remote server.
      #
      #  sftp.dir.glob("/base/path", "*/**/*.rb") do |entry|
      #    p entry.name
      #  end
      #
      # See Net::SFTP::Operations::Dir for a more detailed discussion of how
      # to use this.
      def dir
        @dir ||= Operations::Dir.new(self)
      end

    public # low-level SFTP operations

      # :call-seq:
      #   open(path, flags="r", options={}) -> request
      #   open(path, flags="r", options={}) { |response| ... } -> request
      #
      # Opens a file on the remote server. The +flags+ parameter determines
      # how the flag is open, and accepts the same format as IO#open (e.g.,
      # either a string like "r" or "w", or a combination of the IO constants).
      # The +options+ parameter is a hash of attributes to be associated
      # with the file, and varies greatly depending on the SFTP protocol
      # version in use, but some (like :permissions) are always available.
      #
      # Returns immediately with a Request object. If a block is given, it will
      # be invoked when the server responds, with a Response object as the only
      # parameter. The :handle property of the response is the handle of the
      # opened file, and may be passed to other methods (like #close, #read,
      # #write, and so forth).
      #
      #   sftp.open("/path/to/file") do |response|
      #     raise "fail!" unless response.ok?
      #     sftp.close(response[:handle])
      #   end
      #   sftp.loop
      def open(path, flags="r", options={}, &callback)
        request :open, path, flags, options, &callback
      end

      # Identical to #open, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful. Otherwise, it will
      # return the handle of the newly opened file.
      #
      #   handle = sftp.open!("/path/to/file")
      def open!(path, flags="r", options={}, &callback)
        wait_for(open(path, flags, options, &callback), :handle)
      end

      # :call-seq:
      #   close(handle) -> request
      #   close(handle) { |response| ... } -> request
      #
      # Closes an open handle, whether obtained via #open, or #opendir. Returns
      # immediately with a Request object. If a block is given, it will be
      # invoked when the server responds.
      #
      #   sftp.open("/path/to/file") do |response|
      #     raise "fail!" unless response.ok?
      #     sftp.close(response[:handle])
      #   end
      #   sftp.loop
      def close(handle, &callback)
        request :close, handle, &callback
      end

      # Identical to #close, but blocks until the server responds. It will
      # raise a StatusException if the request was unsuccessful. Otherwise,
      # it returns the Response object for this request.
      #
      #   sftp.close!(handle)
      def close!(handle, &callback)
        wait_for(close(handle, &callback))
      end

      # :call-seq:
      #   read(handle, offset, length) -> request
      #   read(handle, offset, length) { |response| ... } -> request
      #
      # Requests that +length+ bytes, starting at +offset+ bytes from the
      # beginning of the file, be read from the file identified by
      # +handle+. (The +handle+ should be a value obtained via the #open
      # method.)  Returns immediately with a Request object. If a block is
      # given, it will be invoked when the server responds.
      #
      # The :data property of the response will contain the requested data,
      # assuming the call was successful.
      #
      #   request = sftp.read(handle, 0, 1024) do |response|
      #     if response.eof?
      #       puts "end of file reached before reading any data"
      #     elsif !response.ok?
      #       puts "error (#{response})"
      #     else
      #       print(response[:data])
      #     end
      #   end
      #   request.wait
      #
      # To read an entire file will usually require multiple calls to #read,
      # unless you know in advance how large the file is.
      def read(handle, offset, length, &callback)
        request :read, handle, offset, length, &callback
      end

      # Identical to #read, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful. If the end of the file
      # was reached, +nil+ will be returned. Otherwise, it returns the data that
      # was read, as a String.
      #
      #   data = sftp.read!(handle, 0, 1024)
      def read!(handle, offset, length, &callback)
        wait_for(read(handle, offset, length, &callback), :data)
      end

      # :call-seq:
      #   write(handle, offset, data) -> request
      #   write(handle, offset, data) { |response| ... } -> request
      #
      # Requests that +data+ be written to the file identified by +handle+,
      # starting at +offset+ bytes from the start of the file. The file must
      # have been opened for writing via #open. Returns immediately with a
      # Request object. If a block is given, it will be invoked when the
      # server responds.
      #
      #   request = sftp.write(handle, 0, "hello, world!\n")
      #   request.wait
      def write(handle, offset, data, &callback)
        request :write, handle, offset, data, &callback
      end

      # Identical to #write, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful, or the end of the file
      # was reached. Otherwise, it returns the Response object for this request.
      #
      #   sftp.write!(handle, 0, "hello, world!\n")
      def write!(handle, offset, data, &callback)
        wait_for(write(handle, offset, data, &callback))
      end

      # :call-seq:
      #   lstat(path, flags=nil) -> request
      #   lstat(path, flags=nil) { |response| ... } -> request
      #
      # This method is identical to the #stat method, with the exception that
      # it will not follow symbolic links (thus allowing you to stat the
      # link itself, rather than what it refers to). The +flags+ parameter
      # is not used in SFTP protocol versions prior to 4, and will be ignored
      # in those versions of the protocol that do not use it. For those that
      # do, however, you may provide hints as to which file proprties you wish
      # to query (e.g., if all you want is permissions, you could pass the
      # Net::SFTP::Protocol::V04::Attributes::F_PERMISSIONS flag as the value
      # for the +flags+ parameter).
      #
      # The method returns immediately with a Request object. If a block is given,
      # it will be invoked when the server responds. The :attrs property of
      # the response will contain an Attributes instance appropriate for the
      # the protocol version (see Protocol::V01::Attributes, Protocol::V04::Attributes,
      # and Protocol::V06::Attributes).
      #
      #   request = sftp.lstat("/path/to/file") do |response|
      #     raise "fail!" unless response.ok?
      #     puts "permissions: %04o" % response[:attrs].permissions
      #   end
      #   request.wait
      def lstat(path, flags=nil, &callback)
        request :lstat, path, flags, &callback
      end

      # Identical to the #lstat method, but blocks until the server responds.
      # It will raise a StatusException if the request was unsuccessful.
      # Otherwise, it will return the attribute object describing the path.
      #
      #   puts sftp.lstat!("/path/to/file").permissions
      def lstat!(path, flags=nil, &callback)
        wait_for(lstat(path, flags, &callback), :attrs)
      end

      # The fstat method is identical to the #stat and #lstat methods, with
      # the exception that it takes a +handle+ as the first parameter, such
      # as would be obtained via the #open or #opendir methods. (See the #lstat
      # method for full documentation).
      def fstat(handle, flags=nil, &callback)
        request :fstat, handle, flags, &callback
      end

      # Identical to the #fstat method, but blocks until the server responds.
      # It will raise a StatusException if the request was unsuccessful.
      # Otherwise, it will return the attribute object describing the path.
      #
      #   puts sftp.fstat!(handle).permissions
      def fstat!(handle, flags=nil, &callback)
        wait_for(fstat(handle, flags, &callback), :attrs)
      end

      # :call-seq:
      #    setstat(path, attrs) -> request
      #    setstat(path, attrs) { |response| ... } -> request
      #
      # This method may be used to set file metadata (such as permissions, or
      # user/group information) on a remote file. The exact metadata that may
      # be tweaked is dependent on the SFTP protocol version in use, but in
      # general you may set at least the permissions, user, and group. (See
      # Protocol::V01::Attributes, Protocol::V04::Attributes, and Protocol::V06::Attributes
      # for the full lists of attributes that may be set for the different
      # protocols.)
      #
      # The +attrs+ parameter is a hash, where the keys are symbols identifying
      # the attributes to set.
      #
      # The method returns immediately with a Request object. If a block is given,
      # it will be invoked when the server responds.
      #
      #   request = sftp.setstat("/path/to/file", :permissions => 0644)
      #   request.wait
      #   puts "success: #{request.response.ok?}"
      def setstat(path, attrs, &callback)
        request :setstat, path, attrs, &callback
      end

      # Identical to the #setstat method, but blocks until the server responds.
      # It will raise a StatusException if the request was unsuccessful.
      # Otherwise, it will return the Response object for the request.
      #
      #   sftp.setstat!("/path/to/file", :permissions => 0644)
      def setstat!(path, attrs, &callback)
        wait_for(setstat(path, attrs, &callback))
      end

      # The fsetstat method is identical to the #setstat method, with the
      # exception that it takes a +handle+ as the first parameter, such as
      # would be obtained via the #open or #opendir methods. (See the
      # #setstat method for full documentation.)
      def fsetstat(handle, attrs, &callback)
        request :fsetstat, handle, attrs, &callback
      end

      # Identical to the #fsetstat method, but blocks until the server responds.
      # It will raise a StatusException if the request was unsuccessful.
      # Otherwise, it will return the Response object for the request.
      #
      #   sftp.fsetstat!(handle, :permissions => 0644)
      def fsetstat!(handle, attrs, &callback)
        wait_for(fsetstat(handle, attrs, &callback))
      end

      # :call-seq:
      #   opendir(path) -> request
      #   opendir(path) { |response| ... } -> request
      #
      # Attempts to open a directory on the remote host for reading. Once the
      # handle is obtained, directory entries may be retrieved using the
      # #readdir method. The method returns immediately with a Request object.
      # If a block is given, it will be invoked when the server responds.
      #
      #   sftp.opendir("/path/to/directory") do |response|
      #     raise "fail!" unless response.ok?
      #     sftp.close(response[:handle])
      #   end
      #   sftp.loop
      def opendir(path, &callback)
        request :opendir, path, &callback
      end

      # Identical to #opendir, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful. Otherwise, it will
      # return a handle to the given path.
      #
      #   handle = sftp.opendir!("/path/to/directory")
      def opendir!(path, &callback)
        wait_for(opendir(path, &callback), :handle)
      end

      # :call-seq:
      #   readdir(handle) -> request
      #   raeddir(handle) { |response| ... } -> request
      #
      # Reads a set of entries from the given directory handle (which must
      # have been obtained via #opendir). If the response is EOF, then there
      # are no more entries in the directory. Otherwise, the entries will be
      # in the :names property of the response:
      #
      #   loop do
      #     request = sftp.readdir(handle).wait
      #     break if request.response.eof?
      #     raise "fail!" unless request.response.ok?
      #     request.response[:names].each do |entry|
      #        puts entry.name
      #     end
      #   end
      #
      # See also Protocol::V01::Name and Protocol::V04::Name for the specific
      # properties of each individual entry (which vary based on the SFTP
      # protocol version in use).
      def readdir(handle, &callback)
        request :readdir, handle, &callback
      end

      # Identical to #readdir, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful. Otherwise, it will
      # return nil if there were no more names to read, or an array of name
      # entries.
      #
      #   while (entries = sftp.readdir!(handle)) do
      #     entries.each { |entry| puts(entry.name) }
      #   end
      def readdir!(handle, &callback)
        wait_for(readdir(handle, &callback), :names)
      end

      # :call-seq:
      #   remove(filename) -> request
      #   remove(filename) { |response| ... } -> request
      #
      # Attempts to remove the given file from the remote file system. Returns
      # immediately with a Request object. If a block is given, the block will
      # be invoked when the server responds, and will be passed a Response
      # object.
      #
      #   sftp.remove("/path/to/file").wait
      def remove(filename, &callback)
        request :remove, filename, &callback
      end

      # Identical to #remove, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful. Otherwise, it will
      # return the Response object for the request.
      #
      #   sftp.remove!("/path/to/file")
      def remove!(filename, &callback)
        wait_for(remove(filename, &callback))
      end

      # :call-seq:
      #   mkdir(path, attrs={}) -> request
      #   mkdir(path, attrs={}) { |response| ... } -> request
      #
      # Creates the named directory on the remote server. If an attribute hash
      # is given, it must map to the set of attributes supported by the version
      # of the SFTP protocol in use. (See Protocol::V01::Attributes,
      # Protocol::V04::Attributes, and Protocol::V06::Attributes.)
      #
      #   sftp.mkdir("/path/to/directory", :permissions => 0550).wait
      def mkdir(path, attrs={}, &callback)
        request :mkdir, path, attrs, &callback
      end

      # Identical to #mkdir, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful. Otherwise, it will
      # return the Response object for the request.
      #
      #   sftp.mkdir!("/path/to/directory", :permissions => 0550)
      def mkdir!(path, attrs={}, &callback)
        wait_for(mkdir(path, attrs, &callback))
      end

      # :call-seq:
      #   rmdir(path) -> request
      #   rmdir(path) { |response| ... } -> request
      #
      # Removes the named directory on the remote server. The directory must
      # be empty before it can be removed.
      #
      #   sftp.rmdir("/path/to/directory").wait
      def rmdir(path, &callback)
        request :rmdir, path, &callback
      end

      # Identical to #rmdir, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful. Otherwise, it will
      # return the Response object for the request.
      #
      #   sftp.rmdir!("/path/to/directory")
      def rmdir!(path, &callback)
        wait_for(rmdir(path, &callback))
      end

      # :call-seq:
      #   realpath(path) -> request
      #   realpath(path) { |response| ... } -> request
      #
      # Tries to canonicalize the given path, turning any given path into an
      # absolute path. This is primarily useful for converting a path with
      # ".." or "." segments into an identical path without those segments.
      # The answer will be in the response's :names attribute, as a
      # one-element array.
      #
      #   request = sftp.realpath("/path/../to/../directory").wait
      #   puts request[:names].first.name
      def realpath(path, &callback)
        request :realpath, path, &callback
      end

      # Identical to #realpath, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful. Otherwise, it will
      # return a name object identifying the path.
      #
      #   puts(sftp.realpath!("/path/../to/../directory"))
      def realpath!(path, &callback)
        wait_for(realpath(path, &callback), :names).first
      end

      # Identical to the #lstat method, except that it follows symlinks
      # (e.g., if you give it the path to a symlink, it will stat the target
      # of the symlink rather than the symlink itself). See the #lstat method
      # for full documentation.
      def stat(path, flags=nil, &callback)
        request :stat, path, flags, &callback
      end

      # Identical to #stat, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful. Otherwise, it will
      # return an attribute object for the named path.
      #
      #   attrs = sftp.stat!("/path/to/file")
      def stat!(path, flags=nil, &callback)
        wait_for(stat(path, flags, &callback), :attrs)
      end

      # :call-seq:
      #   rename(name, new_name, flags=nil) -> request
      #   rename(name, new_name, flags=nil) { |response| ... } -> request
      #
      # Renames the given file. This operation is only available in SFTP
      # protocol versions two and higher. The +flags+ parameter is ignored
      # in versions prior to 5. In versions 5 and higher, the +flags+
      # parameter can be used to specify how the rename should be performed
      # (atomically, etc.).
      #
      # The following flags are defined in protocol version 5:
      #
      # * 0x0001 - overwrite an existing file if the new name specifies a file
      #   that already exists.
      # * 0x0002 - perform the rewrite atomically.
      # * 0x0004 - allow the server to perform the rename as it prefers.
      def rename(name, new_name, flags=nil, &callback)
        request :rename, name, new_name, flags, &callback
      end

      # Identical to #rename, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful. Otherwise, it will
      # return the Response object for the request.
      #
      #   sftp.rename!("/path/to/old", "/path/to/new")
      def rename!(name, new_name, flags=nil, &callback)
        wait_for(rename(name, new_name, flags, &callback))
      end

      # :call-seq:
      #   readlink(path) -> request
      #   readlink(path) { |response| ... } -> request
      #
      # Queries the server for the target of the specified symbolic link.
      # This operation is only available in protocol versions 3 and higher.
      # The response to this request will include a names property, a one-element
      # array naming the target of the symlink.
      #
      #   request = sftp.readlink("/path/to/symlink").wait
      #   puts request.response[:names].first.name
      def readlink(path, &callback)
        request :readlink, path, &callback
      end

      # Identical to #readlink, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful. Otherwise, it will
      # return the Name object for the path that the symlink targets.
      #
      #   item = sftp.readlink!("/path/to/symlink")
      def readlink!(path, &callback)
        wait_for(readlink(path, &callback), :names).first
      end

      # :call-seq:
      #   symlink(path, target) -> request
      #   symlink(path, target) { |response| ... } -> request
      #
      # Attempts to create a symlink to +path+ at +target+. This operation
      # is only available in protocol versions 3, 4, and 5, but the Net::SFTP
      # library mimics the symlink behavior in protocol version 6 using the
      # #link method, so it is safe to use this method in protocol version 6.
      #
      #   sftp.symlink("/path/to/file", "/path/to/symlink").wait
      def symlink(path, target, &callback)
        request :symlink, path, target, &callback
      end

      # Identical to #symlink, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful. Otherwise, it will
      # return the Response object for the request.
      #
      #   sftp.symlink!("/path/to/file", "/path/to/symlink")
      def symlink!(path, target, &callback)
        wait_for(symlink(path, target, &callback))
      end

      # :call-seq:
      #   link(new_link_path, existing_path, symlink=true) -> request
      #   link(new_link_path, existing_path, symlink=true) { |response| ... } -> request
      #
      # Attempts to create a link, either hard or symbolic. This operation is
      # only available in SFTP protocol versions 6 and higher. If the +symlink+
      # paramter is true, a symbolic link will be created, otherwise a hard
      # link will be created. The link will be named +new_link_path+, and will
      # point to the path +existing_path+.
      #
      #   sftp.link("/path/to/symlink", "/path/to/file", true).wait
      #
      # Note that #link is only available for SFTP protocol 6 and higher. You
      # can use #symlink for protocols 3 and higher.
      def link(new_link_path, existing_path, symlink=true, &callback)
        request :link, new_link_path, existing_path, symlink, &callback
      end

      # Identical to #link, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful. Otherwise, it will
      # return the Response object for the request.
      #
      #   sftp.link!("/path/to/symlink", "/path/to/file", true)
      def link!(new_link_path, existing_path, symlink=true, &callback)
        wait_for(link(new_link_path, existing_path, symlink, &callback))
      end

      # :call-seq:
      #   block(handle, offset, length, mask) -> request
      #   block(handle, offset, length, mask) { |response| ... } -> request
      #
      # Creates a byte-range lock on the file specified by the given +handle+.
      # This operation is only available in SFTP protocol versions 6 and
      # higher. The lock may be either mandatory or advisory.
      #
      # The +handle+ parameter is a file handle, as obtained by the #open method.
      #
      # The +offset+ and +length+ parameters describe the location and size of
      # the byte range.
      #
      # The +mask+ describes how the lock should be defined, and consists of
      # some combination of the following bit masks:
      #
      # * 0x0040 - Read lock. The byte range may not be accessed for reading
      #   by via any other handle, though it may be written to.
      # * 0x0080 - Write lock. The byte range may not be written to via any
      #   other handle, though it may be read from.
      # * 0x0100 - Delete lock. No other handle may delete this file.
      # * 0x0200 - Advisory lock. The server need not honor the lock instruction.
      #
      # Once created, the lock may be removed via the #unblock method.
      def block(handle, offset, length, mask, &callback)
        request :block, handle, offset, length, mask, &callback
      end

      # Identical to #block, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful. Otherwise, it will
      # return the Response object for the request.
      def block!(handle, offset, length, mask, &callback)
        wait_for(block(handle, offset, length, mask, &callback))
      end

      # :call-seq:
      #   unblock(handle, offset, length) -> request
      #   unblock(handle, offset, length) { |response| ... } -> request
      #
      # Removes a previously created byte-range lock. This operation is only
      # available in protocol versions 6 and higher. The +offset+ and +length+
      # parameters must exactly match those that were given to #block when the
      # lock was acquired.
      def unblock(handle, offset, length, &callback)
        request :unblock, handle, offset, length, &callback
      end

      # Identical to #unblock, but blocks until the server responds. It will raise
      # a StatusException if the request was unsuccessful. Otherwise, it will
      # return the Response object for the request.
      def unblock!(handle, offset, length, &callback)
        wait_for(unblock(handle, offset, length, &callback))
      end

    public # miscellaneous methods

      # Closes the SFTP connection, but not the SSH connection. Blocks until the
      # session has terminated. Once the session has terminated, further operations
      # on this object will result in errors. You can reopen the SFTP session
      # via the #connect method.
      def close_channel
        return unless open?
        channel.close
        loop { !closed? }
      end

      # Returns true if the connection has been initialized.
      def open?
        state == :open
      end

      # Returns true if the connection has been closed.
      def closed?
        state == :closed
      end

      # Returns true if the connection is in the process of being initialized
      # (e.g., it is not closed, but is not yet fully open).
      def opening?
        !(open? || closed?)
      end

      # Attempts to establish an SFTP connection over the SSH session given when
      # this object was instantiated. If the object is already open, this will
      # simply execute the given block (if any), passing the SFTP session itself
      # as argument. If the session is currently being opened, this will add
      # the given block to the list of callbacks, to be executed when the session
      # is fully open.
      #
      # This method does not block, and will return immediately. If you pass a
      # block to it, that block will be invoked when the connection has been
      # fully established. Thus, you can do something like this:
      #
      #   sftp.connect do
      #     puts "open!"
      #   end
      #
      # If you just want to block until the connection is ready, see the #connect!
      # method.
      def connect(&block)
        case state
        when :open
          block.call(self) if block
        when :closed
          @state = :opening
          @channel = session.open_channel(&method(:when_channel_confirmed))
          @packet_length = nil
          @protocol = nil
          @on_ready = Array(block)
        else # opening
          @on_ready << block if block
        end

        self
      end

      # Same as the #connect method, but blocks until the SFTP connection has
      # been fully initialized.
      def connect!(&block)
        connect(&block)
        loop { opening? }
        self
      end

      alias :loop_forever :loop

      # Runs the SSH event loop while the given block returns true. This lets
      # you set up a state machine and then "fire it off". If you do not specify
      # a block, the event loop will run for as long as there are any pending
      # SFTP requests. This makes it easy to do thing like this:
      #
      #   sftp.remove("/path/to/file")
      #   sftp.loop
      def loop(&block)
        block ||= Proc.new { pending_requests.any? }
        session.loop(&block)
      end

      # Formats, constructs, and sends an SFTP packet of the given type and with
      # the given data. This does not block, but merely enqueues the packet for
      # sending and returns.
      #
      # You should probably use the operation methods, rather than building and
      # sending the packet directly. (See #open, #close, etc.)
      def send_packet(type, *args)
        data = Net::SSH::Buffer.from(*args)
        msg = Net::SSH::Buffer.from(:long, data.length+1, :byte, type, :raw, data)
        channel.send_data(msg.to_s)
      end

    private

      #--
      # "ruby -w" hates private attributes, so we have to do this longhand
      #++

      # The input buffer used to accumulate packet data
      def input; @input; end

      # Create and enqueue a new SFTP request of the given type, with the
      # given arguments. Returns a new Request instance that encapsulates the
      # request.
      def request(type, *args, &callback)
        request = Request.new(self, type, protocol.send(type, *args), &callback)
        info { "sending #{type} packet (#{request.id})" }
        pending_requests[request.id] = request
      end

      # Waits for the given request to complete. If the response is
      # EOF, nil is returned. If the response was not successful
      # (e.g., !response.ok?), a StatusException will be raised.
      # If +property+ is given, the corresponding property from the response
      # will be returned; otherwise, the response object itself will be
      # returned.
      def wait_for(request, property=nil)
        request.wait
        if request.response.eof?
          nil
        elsif !request.response.ok?
          raise StatusException.new(request.response)
        elsif property
          request.response[property.to_sym]
        else
          request.response
        end
      end

      # Called when the SSH channel is confirmed as "open" by the server.
      # This is one of the states of the SFTP state machine, and is followed
      # by the #when_subsystem_started state.
      def when_channel_confirmed(channel)
        debug { "requesting sftp subsystem" }
        @state = :subsystem
        channel.subsystem("sftp", &method(:when_subsystem_started))
      end

      # Called when the SSH server confirms that the SFTP subsystem was
      # successfully started. This sets up the appropriate callbacks on the
      # SSH channel and then starts the SFTP protocol version negotiation
      # process.
      def when_subsystem_started(channel, success)
        raise Net::SFTP::Exception, "could not start SFTP subsystem" unless success

        debug { "sftp subsystem successfully started" }
        @state = :init

        channel.on_data { |c,data| input.append(data) }
        channel.on_extended_data { |c,t,data| debug { data } }

        channel.on_close(&method(:when_channel_closed))
        channel.on_process(&method(:when_channel_polled))

        send_packet(FXP_INIT, :long, HIGHEST_PROTOCOL_VERSION_SUPPORTED)
      end

      # Called when the SSH server closes the underlying channel.
      def when_channel_closed(channel)
        debug { "sftp channel closed" }
        @channel = nil
        @state = :closed
      end

      # Called whenever Net::SSH polls the SFTP channel for pending activity.
      # This basically checks the input buffer to see if enough input has been
      # accumulated to handle. If there has, the packet is parsed and
      # dispatched, according to its type (see #do_version and #dispatch_request).
      def when_channel_polled(channel)
        while input.length > 0
          if @packet_length.nil?
            # make sure we've read enough data to tell how long the packet is
            return unless input.length >= 4
            @packet_length = input.read_long
          end

          return unless input.length >= @packet_length
          packet = Net::SFTP::Packet.new(input.read(@packet_length))
          input.consume!
          @packet_length = nil

          debug { "received sftp packet #{packet.type} len #{packet.length}" }

          if packet.type == FXP_VERSION
            do_version(packet)
          else
            dispatch_request(packet)
          end
        end
      end

      # Called to handle FXP_VERSION packets. This performs the SFTP protocol
      # version negotiation, instantiating the appropriate Protocol instance
      # and invoking the callback given to #connect, if any.
      def do_version(packet)
        debug { "negotiating sftp protocol version, mine is #{HIGHEST_PROTOCOL_VERSION_SUPPORTED}" }

        server_version = packet.read_long
        debug { "server reports sftp version #{server_version}" }

        negotiated_version = [server_version, HIGHEST_PROTOCOL_VERSION_SUPPORTED].min
        info { "negotiated version is #{negotiated_version}" }

        extensions = {}
        until packet.eof?
          name = packet.read_string
          data = packet.read_string
          extensions[name] = data
        end

        @protocol = Protocol.load(self, negotiated_version)
        @pending_requests = {}

        @state = :open
        @on_ready.each { |callback| callback.call(self) }
        @on_ready = nil
      end

      # Parses the packet, finds the associated Request instance, and tells
      # the Request instance to respond to the packet (see Request#respond_to).
      def dispatch_request(packet)
        id = packet.read_long
        request = pending_requests.delete(id) or raise Net::SFTP::Exception, "no such request `#{id}'"
        request.respond_to(packet)
      end
  end

end; end