net-sftp 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,11 @@ module Net; module SFTP; module Operations
12
12
  # puts file.gets
13
13
  # file.close
14
14
  class File
15
+ # The largest amount of data sent in a single write request. Servers drop
16
+ # the connection on oversized packets (OpenSSH's limit is 256 KiB including
17
+ # headers), and the SFTP drafts only guarantee packets of about 34000 bytes.
18
+ MAX_WRITE_SIZE = 32 * 1024
19
+
15
20
  # A reference to the Net::SFTP::Session instance that drives this wrapper
16
21
  attr_reader :sftp
17
22
 
@@ -30,14 +35,14 @@ module Net; module SFTP; module Operations
30
35
  @pos = 0
31
36
  @real_pos = 0
32
37
  @real_eof = false
33
- @buffer = ""
38
+ @buffer = +""
34
39
  end
35
40
 
36
41
  # Repositions the file pointer to the given offset (relative to the
37
42
  # start of the file). This will also reset the EOF flag.
38
43
  def pos=(offset)
39
44
  @real_pos = @pos = offset
40
- @buffer = ""
45
+ @buffer = +""
41
46
  @real_eof = false
42
47
  end
43
48
 
@@ -63,15 +68,17 @@ module Net; module SFTP; module Operations
63
68
  #
64
69
  # This will advance the file pointer (#pos).
65
70
  def read(n=nil)
71
+ raise ArgumentError, "negative length #{n} given" if n && n < 0
72
+
66
73
  loop do
67
74
  break if n && @buffer.length >= n
68
75
  break unless fill
69
76
  end
70
77
 
71
78
  if n
72
- result, @buffer = @buffer[0,n], (@buffer[n..-1] || "")
79
+ result, @buffer = @buffer[0,n], (@buffer[n..-1] || +"")
73
80
  else
74
- result, @buffer = @buffer, ""
81
+ result, @buffer = @buffer, +""
75
82
  end
76
83
 
77
84
  @pos += result.length
@@ -92,6 +99,7 @@ module Net; module SFTP; module Operations
92
99
  sep_string = sep_or_limit
93
100
  lim = limit
94
101
  end
102
+ lim = Float::INFINITY if lim.nil? || lim < 0
95
103
 
96
104
  delim = if sep_string && sep_string.length == 0
97
105
  "#{$/}#{$/}"
@@ -99,22 +107,26 @@ module Net; module SFTP; module Operations
99
107
  sep_string
100
108
  end
101
109
 
110
+ search_from = 0
102
111
  loop do
103
- at = @buffer.index(delim) if delim
112
+ at = @buffer.index(delim, search_from) if delim
104
113
  if at
105
114
  offset = [at + delim.length, lim].min
106
115
  @pos += offset
107
116
  line, @buffer = @buffer[0,offset], @buffer[offset..-1]
108
117
  return line
109
- elsif lim < @buffer.length
118
+ elsif lim <= @buffer.length
110
119
  @pos += lim
111
120
  line, @buffer = @buffer[0,lim], @buffer[lim..-1]
112
121
  return line
113
- elsif !fill
114
- return nil if @buffer.empty?
115
- @pos += @buffer.length
116
- line, @buffer = @buffer, ""
117
- return line
122
+ else
123
+ search_from = [@buffer.length - delim.length + 1, 0].max if delim
124
+ unless fill
125
+ return nil if @buffer.empty?
126
+ @pos += @buffer.length
127
+ line, @buffer = @buffer, +""
128
+ return line
129
+ end
118
130
  end
119
131
  end
120
132
  end
@@ -128,13 +140,21 @@ module Net; module SFTP; module Operations
128
140
  end
129
141
 
130
142
  # Writes the given data to the stream, incrementing the file position and
131
- # returning the number of bytes written.
143
+ # returning the number of bytes written. Data larger than MAX_WRITE_SIZE
144
+ # is sent as several sequential write requests; if one fails, the
145
+ # position reflects the bytes written before it.
132
146
  def write(data)
133
147
  data = data.to_s
134
- sftp.write!(handle, @real_pos, data)
135
- @real_pos += data.bytes.length
136
- @pos = @real_pos
137
- data.bytes.length
148
+ size = data.bytesize
149
+ written = 0
150
+ loop do
151
+ chunk = data.byteslice(written, MAX_WRITE_SIZE)
152
+ sftp.write!(handle, @pos, chunk)
153
+ written += chunk.bytesize
154
+ self.pos = @pos + chunk.bytesize unless chunk.empty?
155
+ break if written >= size
156
+ end
157
+ size
138
158
  end
139
159
 
140
160
  # Writes each argument to the stream. If +$\+ is set, it will be written
@@ -155,16 +175,12 @@ module Net; module SFTP; module Operations
155
175
  end
156
176
 
157
177
  # Writes each argument to the stream, appending a newline to any item
158
- # that does not already end in a newline. Array arguments are flattened.
178
+ # that does not already end in a newline, like IO#puts. Array arguments
179
+ # are flattened, and with no arguments a single newline is written.
159
180
  def puts(*items)
160
- items.each do |item|
161
- if Array === item
162
- puts(*item)
163
- else
164
- write(item)
165
- write("\n") unless item[-1] == ?\n
166
- end
167
- end
181
+ items = [""] if items.empty?
182
+ recursion = {}.compare_by_identity
183
+ items.each { |item| write_puts_item(item, recursion) }
168
184
  nil
169
185
  end
170
186
 
@@ -178,12 +194,30 @@ module Net; module SFTP; module Operations
178
194
 
179
195
  private
180
196
 
197
+ def write_puts_item(item, recursion)
198
+ array = Array.try_convert(item)
199
+ if array
200
+ if recursion.key?(item)
201
+ item = "[...]"
202
+ else
203
+ recursion[item] = true
204
+ array.each { |child| write_puts_item(child, recursion) }
205
+ recursion.delete(item)
206
+ return
207
+ end
208
+ end
209
+
210
+ item = item.to_s
211
+ write(item) unless item.empty?
212
+ write("\n") unless item[-1] == ?\n
213
+ end
214
+
181
215
  # Fills the buffer. Returns +true+ if it succeeded, and +false+ if
182
216
  # EOF was encountered before any data was read.
183
217
  def fill
184
218
  data = sftp.read!(handle, @real_pos, 8192)
185
219
 
186
- if data.nil?
220
+ if data.nil? || data.empty?
187
221
  @real_eof = true
188
222
  return false
189
223
  else
@@ -1,3 +1,4 @@
1
+ require 'pathname'
1
2
  require 'net/ssh/loggable'
2
3
 
3
4
  module Net; module SFTP; module Operations
@@ -50,9 +51,20 @@ module Net; module SFTP; module Operations
50
51
  # higher might improve throughput. Reducing it will reduce throughput.
51
52
  # * <tt>:read_size</tt> - the maximum number of bytes to read at a time
52
53
  # from the source. Increasing this value might improve throughput. It
53
- # defaults to 32,000 bytes.
54
+ # defaults to 32,000 bytes. Each chunk is sent as one write request, so
55
+ # it must stay within the server's packet limit (OpenSSH drops the
56
+ # connection on packets over 256 KiB).
54
57
  # * <tt>:name</tt> - the filename to report to the progress monitor when
55
58
  # an IO object is given as +local+. This defaults to "<memory>".
59
+ # * <tt>:preserve</tt> - if true, the remote files and directories get the
60
+ # access and modification times of the local ones, and their permissions
61
+ # are set even on files that already exist remotely.
62
+ #
63
+ # Like OpenSSH's sftp client, new remote files and directories are created
64
+ # with the permissions of the local ones, without setuid, setgid and sticky
65
+ # bits and subject to the server's umask. Nothing is set when +local+ is an
66
+ # IO object. If the server refuses to set the attributes after the data has
67
+ # been written, a warning is logged and the upload still succeeds.
56
68
  #
57
69
  # == Progress Monitoring
58
70
  #
@@ -141,6 +153,9 @@ module Net; module SFTP; module Operations
141
153
  # This will return immediately, and requires that the SSH event loop be
142
154
  # run in order to effect the upload. (See #wait.)
143
155
  def initialize(sftp, local, remote, options={}, &progress) #:nodoc:
156
+ local = local.to_path if local.is_a?(Pathname)
157
+ remote = remote.to_path if remote.is_a?(Pathname)
158
+
144
159
  @sftp = sftp
145
160
  @local = local
146
161
  @remote = remote
@@ -148,11 +163,14 @@ module Net; module SFTP; module Operations
148
163
  @options = options
149
164
  @properties = options[:properties] || {}
150
165
  @active = 0
166
+ @directories = []
151
167
 
152
168
  self.logger = sftp.logger
153
169
 
154
170
  @uploads = []
155
171
  @recursive = local.respond_to?(:read) ? false : ::File.directory?(local)
172
+ requests
173
+ read_size
156
174
 
157
175
  if recursive?
158
176
  @stack = [entries_for(local)]
@@ -161,10 +179,10 @@ module Net; module SFTP; module Operations
161
179
 
162
180
  @active += 1
163
181
  if @options[:mkdir]
164
- sftp.mkdir(remote) do |response|
182
+ sftp.mkdir(remote, directory_attributes(local, remote)) do |response|
165
183
  @active -= 1
166
184
  raise StatusException.new(response, "mkdir `#{remote}'") unless response.ok?
167
- (options[:requests] || RECURSIVE_READERS).to_i.times do
185
+ requests.times do
168
186
  break unless process_next_entry
169
187
  end
170
188
  end
@@ -225,9 +243,28 @@ module Net; module SFTP; module Operations
225
243
  # The progress handler for this instance. Possibly nil.
226
244
  def progress; @progress; end
227
245
 
246
+ def requests
247
+ @requests ||= positive_option(:requests, recursive? ? RECURSIVE_READERS : SINGLE_FILE_READERS)
248
+ end
249
+
250
+ def read_size
251
+ @read_size ||= positive_option(:read_size, DEFAULT_READ_SIZE)
252
+ end
253
+
254
+ def positive_option(name, default)
255
+ value = (options[name] || default).to_i
256
+ raise ArgumentError, ":#{name} must be positive" unless value > 0
257
+
258
+ value
259
+ end
260
+
261
+ def preserve?
262
+ options[:preserve]
263
+ end
264
+
228
265
  # A simple struct for recording metadata about the file currently being
229
266
  # uploaded.
230
- LiveFile = Struct.new(:local, :remote, :io, :size, :handle)
267
+ LiveFile = Struct.new(:local, :remote, :io, :size, :handle, :attributes)
231
268
 
232
269
  # The default # of bytes to read from disk at a time.
233
270
  DEFAULT_READ_SIZE = 32_000
@@ -245,7 +282,11 @@ module Net; module SFTP; module Operations
245
282
  if @uploads.any?
246
283
  write_next_chunk(@uploads.first)
247
284
  elsif !active?
248
- update_progress(:finish)
285
+ if @directories.any?
286
+ set_directory_attributes
287
+ else
288
+ update_progress(:finish)
289
+ end
249
290
  end
250
291
  return false
251
292
  elsif @stack.last.empty?
@@ -265,7 +306,7 @@ module Net; module SFTP; module Operations
265
306
 
266
307
  @active += 1
267
308
  update_progress(:mkdir, rpath)
268
- request = sftp.mkdir(rpath, &method(:on_mkdir))
309
+ request = sftp.mkdir(rpath, directory_attributes(lpath, rpath), &method(:on_mkdir))
269
310
  request[:dir] = rpath
270
311
  else
271
312
  open_file(lpath, rpath)
@@ -294,10 +335,11 @@ module Net; module SFTP; module Operations
294
335
  size = file.size
295
336
  end
296
337
 
297
- metafile = LiveFile.new(name, remote, file, size)
338
+ attributes = local.respond_to?(:read) ? {} : attributes_for(file.stat)
339
+ metafile = LiveFile.new(name, remote, file, size, nil, attributes)
298
340
  update_progress(:open, metafile)
299
341
 
300
- request = sftp.open(remote, "w", &method(:on_open))
342
+ request = sftp.open(remote, "w", attributes, &method(:on_open))
301
343
  request[:file] = metafile
302
344
  end
303
345
 
@@ -326,7 +368,7 @@ module Net; module SFTP; module Operations
326
368
  write_next_chunk(file)
327
369
 
328
370
  if !recursive?
329
- (options[:requests] || SINGLE_FILE_READERS).to_i.times { write_next_chunk(file) }
371
+ (requests - 1).times { write_next_chunk(file) }
330
372
  end
331
373
  end
332
374
 
@@ -350,6 +392,49 @@ module Net; module SFTP; module Operations
350
392
  process_next_entry
351
393
  end
352
394
 
395
+ # setuid, setgid and sticky bits are never sent, matching OpenSSH.
396
+ def attributes_for(stat)
397
+ attributes = { :permissions => stat.mode & 0777 }
398
+ attributes.update(:atime => stat.atime.to_i, :mtime => stat.mtime.to_i) if preserve?
399
+ attributes
400
+ end
401
+
402
+ # The directory is created writable and searchable by its owner so its
403
+ # contents can be uploaded; its real attributes are set once everything
404
+ # else is done (see #set_directory_attributes).
405
+ def directory_attributes(local, remote)
406
+ stat = ::File.stat(local)
407
+ @directories << [remote, attributes_for(stat)]
408
+ { :permissions => (stat.mode & 0777) | 0300 }
409
+ end
410
+
411
+ # Deepest directories first; setting attributes on a directory doesn't
412
+ # change its parent's modification time.
413
+ def set_directory_attributes
414
+ @directories.reverse_each do |path, attributes|
415
+ @active += 1
416
+ sftp.setstat(path, attributes) do |response|
417
+ @active -= 1
418
+ warn_unless_ok(response, "setstat #{path}")
419
+ process_next_entry
420
+ end
421
+ end
422
+ @directories.clear
423
+ end
424
+
425
+ # Called when the +fsetstat+ for :preserve finishes.
426
+ def on_fsetstat(response)
427
+ @active -= 1
428
+ file = response.request[:file]
429
+ warn_unless_ok(response, "fsetstat #{file.remote}")
430
+ end
431
+
432
+ # Like OpenSSH's sftp client, failing to set attributes only warns; the
433
+ # data has already been transferred.
434
+ def warn_unless_ok(response, operation)
435
+ lwarn { "#{operation} failed: #{response}" } unless response.ok?
436
+ end
437
+
353
438
  # Attempts to send the next chunk from the given file (where +file+ is
354
439
  # a LiveFile instance).
355
440
  def write_next_chunk(file)
@@ -358,12 +443,18 @@ module Net; module SFTP; module Operations
358
443
  else
359
444
  @active += 1
360
445
  offset = file.io.pos
361
- data = file.io.read(options[:read_size] || DEFAULT_READ_SIZE)
446
+ data = file.io.read(read_size)
362
447
  if data.nil?
363
448
  update_progress(:close, file)
449
+ if preserve? && file.attributes.any?
450
+ @active += 1
451
+ request = sftp.fsetstat(file.handle, file.attributes, &method(:on_fsetstat))
452
+ request[:file] = file
453
+ end
364
454
  request = sftp.close(file.handle, &method(:on_close))
365
455
  request[:file] = file
366
- file.io.close
456
+ # An IO passed in by the caller is theirs to close.
457
+ file.io.close unless file.io.equal?(local)
367
458
  file.io = nil
368
459
  @uploads.delete(file)
369
460
  else
@@ -1,4 +1,5 @@
1
1
  require 'net/ssh/buffer'
2
+ require 'net/sftp/protocol/bounded_read'
2
3
 
3
4
  module Net; module SFTP; module Protocol; module V01
4
5
 
@@ -39,6 +40,8 @@ module Net; module SFTP; module Protocol; module V01
39
40
  T_BLOCK_DEVICE = 8
40
41
  T_FIFO = 9
41
42
 
43
+ extend Net::SFTP::Protocol::BoundedRead
44
+
42
45
  class <<self
43
46
  # Returns the array of attribute meta-data that defines the structure of
44
47
  # the attributes packet as described by this version of the protocol.
@@ -106,7 +109,8 @@ module Net; module SFTP; module Protocol; module V01
106
109
  # Parse the hash of extended data from the buffer.
107
110
  def parse_extended(buffer)
108
111
  extended = Hash.new
109
- buffer.read_long.times do
112
+ # Each pair is two strings, at least 8 bytes.
113
+ read_bounded_count!(buffer, 8, "extended attribute").times do
110
114
  extended[buffer.read_string] = buffer.read_string
111
115
  end
112
116
  extended
@@ -150,7 +154,7 @@ module Net; module SFTP; module Protocol; module V01
150
154
  # * :mtime:: the modification time of the file (integer, seconds since epoch)
151
155
  # * :extended:: a hash of name/value pairs identifying extended info
152
156
  def initialize(attributes={})
153
- @attributes = attributes
157
+ @attributes = attributes.dup
154
158
  end
155
159
 
156
160
  # Returns the user-id of the user that owns the file, or +nil+ if that
@@ -54,7 +54,8 @@ module Net; module SFTP; module Protocol; module V01
54
54
  def parse_name_packet(packet)
55
55
  names = []
56
56
 
57
- packet.read_long.times do
57
+ # Each entry is two strings and the attribute flags, at least 12 bytes.
58
+ read_bounded_count!(packet, 12, "name").times do
58
59
  filename = packet.read_string
59
60
  longname = packet.read_string
60
61
  attrs = attribute_factory.from_buffer(packet)
@@ -5,7 +5,7 @@ module Net; module SFTP; module Protocol; module V01
5
5
  # for use when displaying directory data, and has no specified format.
6
6
  class Name
7
7
  # The name of the item on the remote server.
8
- attr_reader :name
8
+ attr_accessor :name
9
9
 
10
10
  # The display-ready name of the item, possibly with other attributes.
11
11
  attr_reader :longname
@@ -18,6 +18,16 @@ module Net; module SFTP; module Protocol; module V03
18
18
  3
19
19
  end
20
20
 
21
+ # Version 3 adds a message and language tag to status responses.
22
+ def parse_status_packet(packet)
23
+ data = super
24
+ unless packet.eof?
25
+ data[:message] = packet.read_string
26
+ data[:language] = packet.read_string
27
+ end
28
+ data
29
+ end
30
+
21
31
  # Sends a FXP_READLINK packet to the server to request that the target of
22
32
  # the given symlink on the remote host (+path+) be returned.
23
33
  def readlink(path)
@@ -32,4 +42,4 @@ module Net; module SFTP; module Protocol; module V03
32
42
 
33
43
  end
34
44
 
35
- end; end; end; end
45
+ end; end; end; end
@@ -69,7 +69,8 @@ module Net; module SFTP; module Protocol; module V04
69
69
  def parse_acl(buffer)
70
70
  acl_buf = Net::SSH::Buffer.new(buffer.read_string)
71
71
  acl = []
72
- acl_buf.read_long.times do
72
+ # Each entry is three longs and a string, at least 16 bytes.
73
+ read_bounded_count!(acl_buf, 16, "ACL").times do
73
74
  acl << ACL.new(acl_buf.read_long, acl_buf.read_long, acl_buf.read_long, acl_buf.read_string)
74
75
  end
75
76
  acl
@@ -123,7 +124,7 @@ module Net; module SFTP; module Protocol; module V04
123
124
  # to T_REGULAR.
124
125
  def initialize(attributes={})
125
126
  super
126
- attributes[:type] ||= T_REGULAR
127
+ self.type ||= T_REGULAR
127
128
  end
128
129
 
129
130
  private
@@ -134,6 +135,15 @@ module Net; module SFTP; module Protocol; module V04
134
135
  # were given on instantiation
135
136
  owner
136
137
  group
138
+
139
+ # The subsecond flag applies to every timestamp present in the packet.
140
+ subseconds = self.class.elements.select { |_name, _type, flags| flags & F_SUBSECOND_TIMES != 0 }
141
+ if subseconds.any? { |name, _type, _flags| attributes[name] }
142
+ subseconds.each do |name, _type, _flags|
143
+ seconds = name.to_s.sub(/_nseconds\z/, "").to_sym
144
+ attributes[name] ||= 0 if attributes[seconds]
145
+ end
146
+ end
137
147
  end
138
148
 
139
149
  # Performs protocol-version-specific encoding of the access control
@@ -28,7 +28,8 @@ module Net; module SFTP; module Protocol; module V04
28
28
  def parse_name_packet(packet)
29
29
  names = []
30
30
 
31
- packet.read_long.times do
31
+ # Each entry is a string, the attribute flags and the type, at least 9 bytes.
32
+ read_bounded_count!(packet, 9, "name").times do
32
33
  filename = packet.read_string
33
34
  attrs = attribute_factory.from_buffer(packet)
34
35
  names << name_factory.new(filename, attrs)
@@ -9,7 +9,7 @@ module Net; module SFTP; module Protocol; module V04
9
9
  # a directory listing.
10
10
  class Name
11
11
  # The name of the item on the remote server.
12
- attr_reader :name
12
+ attr_accessor :name
13
13
 
14
14
  # Attributes instance describing this item.
15
15
  attr_reader :attributes
@@ -44,24 +44,33 @@ module Net; module SFTP; module Protocol; module V04
44
44
  "l"
45
45
  else
46
46
  "-"
47
- end
47
+ end.dup
48
48
 
49
- longname << (attributes.permissions & 0400 != 0 ? "r" : "-")
50
- longname << (attributes.permissions & 0200 != 0 ? "w" : "-")
51
- longname << (attributes.permissions & 0100 != 0 ? "x" : "-")
52
- longname << (attributes.permissions & 0040 != 0 ? "r" : "-")
53
- longname << (attributes.permissions & 0020 != 0 ? "w" : "-")
54
- longname << (attributes.permissions & 0010 != 0 ? "x" : "-")
55
- longname << (attributes.permissions & 0004 != 0 ? "r" : "-")
56
- longname << (attributes.permissions & 0002 != 0 ? "w" : "-")
57
- longname << (attributes.permissions & 0001 != 0 ? "x" : "-")
49
+ # Fields the server omitted are shown as "?", like GNU ls -l does for entries it cannot stat.
50
+ if (permissions = attributes.permissions)
51
+ longname << (permissions & 0400 != 0 ? "r" : "-")
52
+ longname << (permissions & 0200 != 0 ? "w" : "-")
53
+ longname << (permissions & 0100 != 0 ? "x" : "-")
54
+ longname << (permissions & 0040 != 0 ? "r" : "-")
55
+ longname << (permissions & 0020 != 0 ? "w" : "-")
56
+ longname << (permissions & 0010 != 0 ? "x" : "-")
57
+ longname << (permissions & 0004 != 0 ? "r" : "-")
58
+ longname << (permissions & 0002 != 0 ? "w" : "-")
59
+ longname << (permissions & 0001 != 0 ? "x" : "-")
60
+ else
61
+ longname << "?" * 9
62
+ end
58
63
 
59
- longname << (" %-8s %-8s %8d " % [attributes.owner, attributes.group, attributes.size])
64
+ longname << (" %-8s %-8s %8s " % [attributes.owner || "?", attributes.group || "?", attributes.size || "?"])
60
65
 
61
- longname << Time.at(attributes.mtime).strftime("%b %e %H:%M ")
66
+ longname << if attributes.mtime
67
+ Time.at(attributes.mtime).strftime("%b %e %H:%M ")
68
+ else
69
+ "%12s " % "?"
70
+ end
62
71
  longname << name
63
72
  end
64
73
  end
65
74
  end
66
75
 
67
- end; end; end; end
76
+ end; end; end; end
@@ -35,25 +35,28 @@ module Net; module SFTP; module Protocol; module V05
35
35
  def open(path, flags, options)
36
36
  flags = normalize_open_flags(flags)
37
37
 
38
- sftp_flags, desired_access = if flags & (IO::WRONLY | IO::RDWR) != 0
39
- open = if flags & (IO::CREAT | IO::EXCL) == (IO::CREAT | IO::EXCL)
38
+ sftp_flags = if flags & (IO::CREAT | IO::EXCL) == (IO::CREAT | IO::EXCL)
40
39
  FV5::CREATE_NEW
41
40
  elsif flags & (IO::CREAT | IO::TRUNC) == (IO::CREAT | IO::TRUNC)
42
41
  FV5::CREATE_TRUNCATE
43
42
  elsif flags & IO::CREAT == IO::CREAT
44
43
  FV5::OPEN_OR_CREATE
44
+ elsif flags & IO::TRUNC == IO::TRUNC
45
+ FV5::TRUNCATE_EXISTING
45
46
  else
46
47
  FV5::OPEN_EXISTING
47
48
  end
49
+
50
+ desired_access = if flags & (IO::WRONLY | IO::RDWR) != 0
48
51
  access = ACE::Mask::WRITE_DATA | ACE::Mask::WRITE_ATTRIBUTES
49
52
  access |= ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES if (flags & IO::RDWR) == IO::RDWR
50
53
  if flags & IO::APPEND == IO::APPEND
51
- open |= FV5::APPEND_DATA
54
+ sftp_flags |= FV5::APPEND_DATA
52
55
  access |= ACE::Mask::APPEND_DATA
53
56
  end
54
- [open, access]
57
+ access
55
58
  else
56
- [FV5::OPEN_EXISTING, ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES]
59
+ ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES
57
60
  end
58
61
 
59
62
  attributes = attribute_factory.new(options)
@@ -63,4 +66,4 @@ module Net; module SFTP; module Protocol; module V05
63
66
 
64
67
  end
65
68
 
66
- end; end; end; end
69
+ end; end; end; end
@@ -1,5 +1,6 @@
1
1
  require 'net/ssh/loggable'
2
2
  require 'net/sftp/constants'
3
+ require 'net/sftp/protocol/bounded_read'
3
4
 
4
5
  module Net; module SFTP; module Protocol
5
6
 
@@ -10,6 +11,7 @@ module Net; module SFTP; module Protocol
10
11
  include Net::SSH::Loggable
11
12
  include Net::SFTP::Constants
12
13
  include Net::SFTP::Constants::PacketTypes
14
+ include Net::SFTP::Protocol::BoundedRead
13
15
 
14
16
  # The SFTP session object that acts as client to this protocol instance
15
17
  attr_reader :session
@@ -19,6 +21,7 @@ module Net; module SFTP; module Protocol
19
21
  @session = session
20
22
  self.logger = session.logger
21
23
  @request_id_counter = -1
24
+ @request_ids_wrapped = false
22
25
  end
23
26
 
24
27
  # Attept to parse the given packet. If the packet is of an unsupported
@@ -41,10 +44,15 @@ module Net; module SFTP; module Protocol
41
44
  # A new request identifier will be allocated to this request, and will
42
45
  # be returned.
43
46
  def send_request(type, *args)
44
- @request_id_counter += 1
47
+ begin
48
+ previous_request_id = @request_id_counter
49
+ @request_id_counter = (@request_id_counter + 1) & 0xffffffff
50
+ @request_ids_wrapped = true if previous_request_id == 0xffffffff
51
+ end while @request_ids_wrapped && session.pending_requests.key?(@request_id_counter)
52
+
45
53
  session.send_packet(type, :long, @request_id_counter, *args)
46
54
  return @request_id_counter
47
55
  end
48
56
  end
49
57
 
50
- end; end; end
58
+ end; end; end
@@ -0,0 +1,25 @@
1
+ require 'net/sftp/errors'
2
+
3
+ module Net; module SFTP; module Protocol
4
+
5
+ # Several SFTP structures are a 32-bit item count followed by that many
6
+ # items. Net::SSH::Buffer returns nil instead of raising when a read runs
7
+ # past the end of the data, so a loop over an unchecked count from the
8
+ # server keeps allocating long after the packet is exhausted.
9
+ module BoundedRead
10
+ # Reads an item count from +buffer+ and raises Net::SFTP::Exception if it
11
+ # is missing, or larger than the remaining bytes could hold given that
12
+ # each item takes at least +min_item_size+ bytes.
13
+ def read_bounded_count!(buffer, min_item_size, what)
14
+ count = buffer.read_long
15
+ raise Net::SFTP::Exception, "truncated packet: missing #{what} count" if count.nil?
16
+
17
+ if count > buffer.available / min_item_size
18
+ raise Net::SFTP::Exception, "server sent #{count} #{what} entries in only #{buffer.available} bytes"
19
+ end
20
+
21
+ count
22
+ end
23
+ end
24
+
25
+ end; end; end