homefs 0.2.1 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8da5aed5c790cde44852ed7d17e9878f5ee8a786
4
- data.tar.gz: bd0a7440218649a6901b6f7530c41cc66502ab9f
3
+ metadata.gz: f11d42ee83c4b4ebf69e367d4ae58cc1a4927fac
4
+ data.tar.gz: c8bba0a2cad6c689a3865c910c7d172c14bb9933
5
5
  SHA512:
6
- metadata.gz: 2897cf7ee8057733b4c896303d38629feb86ef92486c4c27cb1aebd68465bef5fd339aa0cf985cf753d2560330df9b7d515f6b957b72efbe563d8aeaf994f435
7
- data.tar.gz: 2989acfca06ed15ba9a10796d8613e2e43fc8e7e2015f776ec3a903fedf22a34172ece57d0266efafcb94cfb66a3e76801054344f2c46ed35af5bbe871135cda
6
+ metadata.gz: d31980d7d5be1f62af06b45e14ae95bfc1001202eb674edb479921730c5fea3eb7c5908158b613fe7ff721edf418554cf1d29a8a7678d269c76ed6e30ab08156
7
+ data.tar.gz: 714090d6643662fe47fdc65987fd867a8da0368ef5cfa9534211ea314120ec5eadd53914ece6196e7d8e58f17c004bd13dbfb2426746fd1e5e98685a428426ae
data/bin/homefs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'homefs'
4
- require 'rb-inotify'
5
- require 'fileutils'
4
+
5
+ DEBUG = false
6
6
 
7
7
  if ARGV.size < 2
8
8
  warn "Usage:\n\t#{File.basename($0)} relative_directory mountpoint [options...]"
@@ -10,8 +10,10 @@ if ARGV.size < 2
10
10
  end
11
11
 
12
12
  # Return immediately
13
- if fork
14
- exit 0
13
+ unless DEBUG
14
+ if fork
15
+ exit 0
16
+ end
15
17
  end
16
18
 
17
19
  reldir = ARGV.shift
@@ -19,35 +21,47 @@ reldir = ARGV.shift
19
21
  fs = HomeFS.new(reldir)
20
22
 
21
23
  # Shut up
22
- $stdout = File.open("/dev/null", "w")
23
- $stderr = File.open("/dev/null", "w")
24
-
25
- pid = fork
26
- if pid
27
- # The child process will be the one actually handling the FuseFS,
28
- # so we want to exit as soon as it dies
29
- trap 'CLD' do
30
- exit 0
31
- end
24
+ unless DEBUG
25
+ $stdout = File.open("/dev/null", "w")
26
+ $stderr = File.open("/dev/null", "w")
27
+ end
32
28
 
33
- # Set a watch for /etc/passwd, and update our hash of UIDs => homedirs
34
- # when it changes
35
- notifier = INotify::Notifier.new
36
- notifier.watch('/etc/passwd', :modify) do
37
- # Send a signal to the child process; it will catch USR1 and update
38
- # the homedirs hash
39
- Process::kill('USR1', pid)
40
- end
41
- notifier.run
42
- else
43
- # Here we trap the signal to be sent to us by our parent
44
- # when /etc/passwd changes and update our hash accordingly
45
- trap 'USR1' do
46
- fs.read_passwd
29
+ if DEBUG
30
+ fs = HomeFS::Wrapper.new(fs)
31
+ end
32
+
33
+ begin
34
+ require 'rb-inotify'
35
+ pid = fork
36
+ if pid
37
+ # The child process will be the one actually handling the FuseFS,
38
+ # so we want to exit as soon as it dies
39
+ trap 'CLD' do
40
+ exit 0
41
+ end
42
+
43
+ # Set a watch for /etc/passwd, and update our hash of UIDs => homedirs
44
+ # when it changes
45
+ notifier = INotify::Notifier.new
46
+ notifier.watch('/etc/passwd', :modify) do
47
+ # Send a signal to the child process; it will catch USR1 and update
48
+ # the homedirs hash
49
+ Process::kill('USR1', pid)
50
+ end
51
+ notifier.run
52
+ else
53
+ # Here we trap the signal to be sent to us by our parent
54
+ # when /etc/passwd changes and update our hash accordingly
55
+ trap 'USR1' do
56
+ fs.read_passwd
57
+ end
47
58
  end
59
+ rescue LoadError
60
+ # We don't have rb-inotify, and so don't watch for changes to
61
+ # /etc/passwd
48
62
  end
49
63
 
50
64
  # We set allow_other, because this filesystem doesn't really make sense
51
65
  # without it (why direct different users to different places if only one
52
66
  # user can access the filesystem?).
53
- FuseFS.main(ARGV + ["-o", "allow_other"]) { fs }
67
+ RFuse.main(ARGV + ["-o", "allow_other"]) { fs }
@@ -0,0 +1,21 @@
1
+ class HomeFS
2
+ class Wrapper
3
+ def initialize(object)
4
+ methods = (object.class.instance_methods -
5
+ Object.instance_methods)
6
+ methods.each do |method|
7
+ self.define_singleton_method(method) do |*args|
8
+ warn "#{method} called with args #{args.inspect}"
9
+ begin
10
+ ret = object.send(method, *args)
11
+ warn "\tMethod returned #{ret.inspect}"
12
+ return ret
13
+ rescue Exception => e
14
+ warn e
15
+ warn e.backtrace
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
data/lib/homefs/homefs.rb CHANGED
@@ -1,24 +1,25 @@
1
- require 'rfusefs'
1
+ require 'rfuse'
2
2
 
3
3
  # An instance of HomeFS; this implements all FuseFS methods, and ultimately
4
4
  # handles reading, writing, and inspection of files.
5
5
  #
6
6
  # We rely on the context provided to us by FUSE to determine the calling user's
7
- # UID and GID. If you want to access an instance of this class directly, you may
8
- # set the UID and GID of the current thread like so
9
- # Thread.current[:fusefs_reader_uid] = uid
10
- # Thread.current[:fusefs_reader_gid] = gid
11
- # where 'uid' and 'gid' are the desired UID and GID you wish to emulate.
7
+ # UID and GID.
8
+ #
9
+ # Currently, HomeFS does not support POSIX locking (which has little effect
10
+ # in practice), and special files (those created by mknod/FIFOs). We do
11
+ # supported extended filesystem attributes, but only if you have the
12
+ # 'ffi-xattr' gem.
12
13
  # @author Dylan Frese
13
14
  class HomeFS
14
15
 
15
16
  # Creates a new instance of HomeFS. This instance should probably be passed
16
- # to FuseFS, as it does nothing on its own. The path is relative to the
17
+ # to RFuse, as it does nothing on its own. The path is relative to the
17
18
  # calling user's home directory, that is, the user interacting with the
18
19
  # filesystem through open(2), read(2), write(2), etc.
19
20
  # @param [String] path the path, relative to $HOME, to use as the root of
20
21
  # the file system.
21
- def initialize(path)
22
+ def initialize(path, options = Hash.new)
22
23
  @relpath = path
23
24
  read_passwd
24
25
  read_group
@@ -54,7 +55,7 @@ class HomeFS
54
55
  }
55
56
  end
56
57
 
57
- # Read or re-read /etc/group to update the internal table of group
58
+ # Read or re-read /etc/group to update the internal table of
58
59
  # group membership.
59
60
  # @return [void]
60
61
  def read_group
@@ -69,7 +70,7 @@ class HomeFS
69
70
  group.each_line.map { |line|
70
71
  next if line.strip.empty?
71
72
  _name, _, gid, members = line.split(':')
72
- members = members.strip.split(',').map {|member| p member; @usernames[member]}
73
+ members = members.strip.split(',').map {|member| @usernames[member]}
73
74
  [Integer(gid), members]
74
75
  }.reject { |line|
75
76
  line.nil?
@@ -81,11 +82,12 @@ class HomeFS
81
82
  # Return the path to the root of HomeFS relative to the underlying
82
83
  # filesystem. If _path_ is specified, it is taken to be relative to the
83
84
  # root of HomeFS.
85
+ # @param [Integer] uid the UID whose home directory to use as a base
86
+ # @param [String] path a relative path to a resource
84
87
  # @return [String] path to the root of the HomeFS relative to the underlying
85
88
  # filesystem, or if _path_ is specified, the path to that resource
86
89
  # relative to the underlying filesystem.
87
- def homepath(path = nil)
88
- uid = FuseFS.reader_uid
90
+ def homepath(uid, path = nil)
89
91
  basepath = @homedirs[uid]
90
92
  # basepath shouldn't ever be nil, but fail gracefully just
91
93
  # in case.
@@ -129,16 +131,15 @@ class HomeFS
129
131
  # @param [String] file the path to the file to check
130
132
  # @param [Integer] mask the mask against which to check the file's mode.
131
133
  # It is recommended you write this in octal (with a leading 0).
132
- # @param [Boolean] check_ids if true, only check user and group permissions
133
- # if the caller is, respectively, the owner of the file/in the file's
134
- # group.
134
+ # @param [Integer] uid if not nil, only check user and group permissions
135
+ # if the user it represents is, respectively, the owner of the
136
+ # file/in the file's group.
135
137
  # @return [Boolean] whether the mode of _file_ matches the given mask
136
- def mode_mask(file, mask, check_ids = true)
138
+ def mode_mask(file, mask, uid = nil)
137
139
  stat = File.stat(file)
138
140
  fmode = stat.mode
139
- if check_ids
141
+ if uid
140
142
  fuid, fgid = stat.uid, stat.gid
141
- uid = FuseFS.reader_uid
142
143
  # Zero out the third digit (in octal).
143
144
  # We could use a constant here, but this works
144
145
  # for a mask of any length
@@ -156,263 +157,640 @@ class HomeFS
156
157
  # we test if the parent directory is either writable or has the sticky bit
157
158
  # set.
158
159
  # @param [String] path the path to test
160
+ # @param [Integer] uid the UID to check the privilage of. If nil, this
161
+ # method returns whether the file is writable by anyone.
159
162
  # @return [Boolean] whether the given path is writable
160
- def writable?(path)
161
- return true if mode_mask(path, 0222)
163
+ def writable?(path, uid = nil)
162
164
  if !File.exist?(path)
163
- mode_mask(File.dirname(path), 01222)
165
+ mode_mask(File.dirname(path), 01222, uid)
164
166
  else
165
- false
167
+ mode_mask(path, 0222, uid)
166
168
  end
167
169
  end
168
170
 
169
- # Rename the file or directory at from_path to to_path. This is mostly
170
- # equivalent to /bin/mv.
171
- # @param [String] from_path the path of the file to be moved
172
- # @param [String] to_path the destination of the file
171
+ # Test whether the user described by _uid_ can read from the given
172
+ # path. If no file exists at the given path, an Errno::ENOENT is
173
+ # raised.
174
+ # @param [String] path the path to test
175
+ # @param [Integer] uid the UID to check the privilage of. If nil, this
176
+ # method returns whether the file is readable by anyone.
177
+ # @return [Boolean] whether the given path is readable
178
+ # @raise [Errno::ENOENT] if no file exists at _path_
179
+ def readable?(path, uid = nil)
180
+ mode_mask(path, 0444, uid)
181
+ end
182
+
183
+ # Test whether the user described by _uid_ can execute the given
184
+ # path. If no file exists at the given path, an Errno::ENOENT is
185
+ # raised.
186
+ # @param [String] path the path to test
187
+ # @param [Integer] uid the UID to check the privilage of. If nil, this
188
+ # method returns whether the file is executable by anyone.
189
+ # @return [Boolean] whether the given path is executable
190
+ # @raise [Errno::ENOENT] if no file exists at _path_
191
+ def executable?(path, uid = nil)
192
+ mode_mask(path, 0111, uid)
193
+ end
194
+
195
+ # This method raises an Errno::EACCES if the user given by _uid_ cannot
196
+ # write to the given path. The check is the same as in {#writable?}.
197
+ # @param [String] relpath the path to test, relative to the root of
198
+ # HomeFS (not the actual filesystem)
199
+ # @param [Integer] uid the UID to check the privilage of
173
200
  # @return [void]
174
- # @note This method should usually only be called by FUSE, and not called
175
- # directly
176
- def rename(from_path, to_path)
177
- FileUtils.mv(homepath(from_path), homepath(to_path))
201
+ # @raise [Errno::EACCES] if the path is not writable by the given user
202
+ def check_writable(uid, relpath)
203
+ hpath = homepath(uid, relpath)
204
+ unless writable?(hpath, uid)
205
+ fail Errno::EACCES, relpath
206
+ end
178
207
  end
179
208
 
180
- # Update the mtime and atime (last-modified and last-accessed time) of the
181
- # file at _path_ to the time given by modtime.
182
- # @param [String] path the path to the file to touch
183
- # @param [Time] modtime the time to update the file's times to
209
+ # This method raises an Errno::EACCES if the user given by _uid_ cannot
210
+ # read from the given path.
211
+ # @param [String] relpath the path to test, relative to the root of
212
+ # HomeFS (not the actual filesystem)
213
+ # @param [Integer] uid the UID to check the privilage of
184
214
  # @return [void]
185
- # @note This method should usually only be called by FUSE, and not called
186
- # directly
187
- def touch(path, modtime)
188
- File.utime(modtime, modtime, homepath(path))
215
+ # @raise [Errno::EACCES] if the path is not readable by the given user
216
+ # @raise [Errno::ENOENT] if no file or directory exists at the given
217
+ # path
218
+ def check_readable(uid, relpath)
219
+ hpath = homepath(uid, relpath)
220
+ unless readable?(hpath, uid)
221
+ fail Errno::EACCES, relpath
222
+ end
189
223
  end
190
224
 
191
- # Remove the directory at the given path. The directory must be empty. This
192
- # is mostly equivalent to rmdir(1).
193
- # @param [String] path the path to the directory to remove
225
+ # This method raises an Errno::EACCES if the user given by _uid_ cannot
226
+ # list the given directory, i.e., if the user does not have the execute
227
+ # and read permissions on the given directory. If the path given is not
228
+ # a directory, the dirname of the given path is tested.
229
+ # @param [String] relpath the path to test, relative to the root of
230
+ # HomeFS (not the actual filesystem)
231
+ # @param [Integer] uid the UID to check the privilage of
194
232
  # @return [void]
195
- # @note This method should usually only be called by FUSE, and not called
196
- # directly
197
- def rmdir(path)
198
- FileUtils.rmdir(homepath(path))
233
+ # @raise [Errno::EACCES] if the path is not readable by the given user
234
+ def check_listable(uid, relpath)
235
+ hpath = homepath(uid, relpath)
236
+ unless File.directory?(hpath)
237
+ hpath = File.dirname(hpath)
238
+ end
239
+ unless readable?(hpath) && executable?(hpath)
240
+ fail Errno::EACCES, relpath
241
+ end
199
242
  end
200
243
 
201
- # Test whether we can remove the directory given by _path_.
202
- # @param [String] path the path to the directory to test
203
- # @return [Boolean] true if the directory given can be removed.
204
- # @note This method should usually only be called by FUSE, and not called
205
- # directly
206
- def can_rmdir?(path)
207
- File.writable?(homepath(path)) &&
208
- Dir.new(path).to_a.size == 2
244
+ # This method raises an Errno::EACCES if the user given by _uid_ is not
245
+ # the owner of the file described by _relpath_
246
+ # @param [String] relpath the path to test, relative to the root of
247
+ # HomeFS (not the actual filesystem)
248
+ # @param [Integer] uid the UID to check ownership of
249
+ # @return [void]
250
+ # @raise [Errno::EACCES] if the path is not owned by the given user
251
+ # @raise [Errno::ENOENT] if no file or directory exists at the given
252
+ # path
253
+ def check_owner(uid, relpath)
254
+ hpath = File.dirname(homepath(uid, relpath))
255
+ unless File.stat(hpath).uid == uid
256
+ fail Errno::EACCES, relpath
257
+ end
209
258
  end
210
259
 
211
- # Make a new directory at _path_. This is similar to mkdir(1).
212
- # @param [String] path the path to the directory to make
260
+ # Check access permissions
261
+ # @overload access(context,path,mode)
262
+ # @abstract
263
+ # @param [Context] context
264
+ # @param [String] path
265
+ # @param [Integer] mode the permissions to check
213
266
  # @return [void]
214
- # @note This method should usually only be called by FUSE, and not called
215
- # directly
216
- def mkdir(path)
217
- FileUtils.mkdir(homepath(path))
267
+ # @raise [Errno::EACCESS] if the requested permission isn't available
268
+ # @note This method should usually only be called by FUSE, and not
269
+ # called directly
270
+ def access(context, path, mode)
271
+ uid = context.uid
272
+ unless mode_mask(homepath(uid, path), mode * 0111, uid)
273
+ raise Errno::EACCES
274
+ end
218
275
  end
219
276
 
220
- # Test if we can make a directory at the given path.
221
- # @param [String] path the path to test
222
- # @return [Boolean] true if we can make a directory at _path_.
223
- # @note This method should usually only be called by FUSE, and not called
224
- # directly
225
- def can_mkdir?(path)
226
- writable?(homepath(path))
277
+ # Change file permissions
278
+ # @overload chmod(context,path,mode)
279
+ # @abstract
280
+ # @param [Context] context
281
+ # @param [String] path
282
+ # @param [Integer] mode
283
+ # @return [void]
284
+ # @raise [Errno]
285
+ # @note This method should usually only be called by FUSE, and not
286
+ # called directly
287
+ def chmod(context, path, mode)
288
+ hpath = homepath(context.uid, path)
289
+ check_owner(context.uid, path)
290
+ File.chmod(mode, hpath)
227
291
  end
228
292
 
229
- # Delete the file given by _path_. This is similar to rm(1).
230
- # @param [String] path the path to the file to delete
293
+ # Change file ownership
294
+ # @overload chown(context,path,uid,gid)
295
+ # @abstract
296
+ # @param [Context] context
297
+ # @param [String] path
298
+ # @param [Integer] uid new user id
299
+ # @param [Integer] gid new group id
231
300
  # @return [void]
232
- # @note This method should usually only be called by FUSE, and not called
233
- # directly
234
- def delete(path)
235
- FileUtils.rm(homepath(path))
301
+ # @raise [Errno]
302
+ # @note This method should usually only be called by FUSE, and not
303
+ # called directly
304
+ def chown(context, path, uid, gid)
305
+ raise Errno::EACCES, path if context.uid != 0
306
+ hpath = homepath(context.uid, path)
307
+ File.chown(uid, gid, hpath)
236
308
  end
237
309
 
238
- # Test if we can delete the file given by _path_.
239
- # @param [String] path the path to the file to test
310
+ # Create and open a file
311
+ # @abstract
312
+ # @param [Context] context
313
+ # @param [String] path
314
+ # @param [Integer] mode the file permissions to create
315
+ # @param [Fileinfo] ffi - use the FileInfo#fh attribute to store a
316
+ # filehandle
240
317
  # @return [void]
241
- # @note This method should usually only be called by FUSE, and not called
242
- # directly
243
- def can_delete?(path)
244
- writable?(File.dirname(homepath(path)))
318
+ # @raise [Errno]
319
+ # If the file does not exist, first create it with the specified mode,
320
+ # and then open it.
321
+ # @note This method should usually only be called by FUSE, and not
322
+ # called directly
323
+ def create(context, path, mode, ffi)
324
+ check_writable(context.uid, path)
325
+ hpath = homepath(context.uid, path)
326
+
327
+ # It is important that we create the file and chown it before we
328
+ # set its mode to prevent a possible privilage escalation
329
+ # race-condition. To be more specific, if this filesystem is
330
+ # running as root (which it usually is), the file will be created
331
+ # owned by root. If the mode were set when the file is created,
332
+ # then an attacker could create a setuid file writable by anyone,
333
+ # and then, in the 'real' filesystem (not HomeFS), quickly write a
334
+ # small program that just executes /bin/bash. If the write
335
+ # completed before handle.chown was called, the attacker could have
336
+ # a setuid shell.
337
+ handle = File.new(hpath, File::CREAT | File::WRONLY, 0600)
338
+ handle.chown(context.uid, context.gid)
339
+ handle.chmod(mode)
340
+ ffi.fh = handle
245
341
  end
246
342
 
247
- # Write the given data to the given path.
248
- # @param [String] path the path to write to
249
- # @param [String] str a binary string of data to write
343
+ # Get attributes of an open file
344
+ # @overload fgetattr(context,path,ffi)
345
+ # @abstract
346
+ # @param [Context] context
347
+ # @param [String] path
348
+ # @param [Fileinfo] ffi
349
+ # @return [Stat] file attributes
350
+ # @raise [Errno]
351
+ # @note This method should usually only be called by FUSE, and not
352
+ # called directly
353
+ def fgetattr(context, path, ffi)
354
+ ffi.fh.lstat
355
+ end
356
+
357
+ # Possibly flush cached data
358
+ # @overload flush(context,path,ffi)
359
+ # @abstract
360
+ # @param [Context] context
361
+ # @param [String] path
362
+ # @param [FileInfo] ffi
250
363
  # @return [void]
251
- # @note This method should usually only be called by FUSE, and not called
252
- # directly
253
- def write_to(path, str)
254
- File.open(homepath(path), "wb") {|file| file.write(str) }
364
+ # @raise [Errno]
365
+ # BIG NOTE: This is not equivalent to fsync(). It's not a request to sync dirty data.
366
+ # Flush is called on each close() of a file descriptor. So if a
367
+ # filesystem wants to return write errors in close() and the file has
368
+ # cached dirty data, this is a good place to write back data and return
369
+ # any errors. Since many applications ignore close() errors this is not
370
+ # always useful.
371
+ #
372
+ # NOTE: The flush() method may be called more than once for each
373
+ # open(). This happens if more than one file descriptor refers to an
374
+ # opened file due to dup(), dup2() or fork() calls. It is not possible
375
+ # to determine if a flush is final, so each flush should be treated
376
+ # equally. Multiple write-flush sequences are relatively rare, so this
377
+ # shouldn't be a problem.
378
+ #
379
+ # Filesystems shouldn't assume that flush will always be called after
380
+ # some writes, or that if will be called at all.
381
+ # @note This method should usually only be called by FUSE, and not
382
+ # called directly
383
+ def flush(context, path, ffi) # We don't do any caching
255
384
  end
256
385
 
257
- # Test whether we can write out to the given path.
258
- # @param [String] path the path to test
259
- # @return [Boolean] whether we can write to the given path
260
- # @note This method should usually only be called by FUSE, and not called
261
- # directly
262
- def can_write?(path)
263
- writable?(homepath(path))
264
- end
265
-
266
- # Get the times (atime, mtime, ctime) for the file at the given path.
267
- # @param [String] path the path to the file to get the times for
268
- # @return [Array<Time>] an array of size three of the last-accessed time,
269
- # the last-modified time, and the creation time for the file
270
- # @note This method should usually only be called by FUSE, and not called
271
- # directly
272
- def times(path)
273
- atime = File.atime(homepath(path))
274
- mtime = File.mtime(homepath(path))
275
- ctime = File.ctime(homepath(path))
276
- [atime, mtime, ctime]
277
- end
278
-
279
- # Test whether the file given by _path_ is executable.
280
- # @param [String] path the path to the file to test
281
- # @return [Boolean] whether the file at the given path is executable.
282
- # @note This method should usually only be called by FUSE, and not called
283
- # directly
284
- def executable?(path)
285
- mode_mask(homepath(path), 0111)
286
- end
287
-
288
- # Read the contents of the file given by _path_.
289
- # @param [String] path the path of the file to read
290
- # @return [String] a binary string of the contents of the file
291
- # @note This method should usually only be called by FUSE, and not called
292
- # directly
293
- def read_file(path)
294
- File.open(homepath(path), "rb") do |file|
295
- file.read
386
+ # Synchronize file contents
387
+ # @overload fsync(context,path,datasync,ffi)
388
+ # @abstract
389
+ # @param [Context] context
390
+ # @param [String] path
391
+ # @param [Integer] datasync if non-zero, then only user data should be
392
+ # flushed, not the metadata
393
+ # @param [FileInfo] ffi
394
+ # @return [void]
395
+ # @raise [Errno]
396
+ # @note This method should usually only be called by FUSE, and not
397
+ # called directly
398
+ def fsync(context, path, datasync, ffi)
399
+ if datasync
400
+ ffi.fh.fdatasync
401
+ else
402
+ ffi.fh.fsync
296
403
  end
297
404
  end
298
405
 
299
- # List the contents of a directory. This includes '.' and '..' (the current
300
- # directory and its parent).
301
- # @param [String] path the path to the directory to list
302
- # @return [Array<String>] a list of file names of files in the directory
303
- # @note This method should usually only be called by FUSE, and not called
304
- # directly
305
- def contents(path)
306
- Dir.new(homepath(path)).to_a
406
+ # Change the size of an open file
407
+ # @overload ftruncate(context,path,size,ffi)
408
+ # @abstract
409
+ # @param [Context] context
410
+ # @param [String] path
411
+ # @param [Integer] size
412
+ # @param [Fileinfo] ffi
413
+ # @return [void]
414
+ # @raise [Errno]
415
+ # @note This method should usually only be called by FUSE, and not
416
+ # called directly
417
+ def ftruncate(context, path, size, ffi)
418
+ ffi.fh.truncate(size)
307
419
  end
308
420
 
309
- # Get the size of the file at _path_
310
- # @param [String] path the path to the file or directory to get the size of
311
- # @return [Integer] the size, in bytes, of the file
312
- # @note This method should usually only be called by FUSE, and not called
313
- # directly
314
- def size(path)
315
- File.size(homepath(path))
421
+ # Get file attributes.
422
+ # @overload getattr(context,path)
423
+ # @abstract
424
+ # @param [Context] context
425
+ # @param [String] path
426
+ # @return [Stat] or something that quacks like a stat, or nil if the path does not exist
427
+ # @raise [Errno]
428
+ # Similar to stat(). The 'st_dev' and 'st_blksize' fields are ignored.
429
+ # The 'st_ino' field is ignored except if the 'use_ino' mount option is
430
+ # given.
431
+ # @note This method should usually only be called by FUSE, and not
432
+ # called directly
433
+ def getattr(context, path)
434
+ check_listable(context.uid, path)
435
+ File.lstat(homepath(context.uid, path))
316
436
  end
317
437
 
318
- # Test whether _path_ is the name of a file (i.e., not a directory)
319
- # @param [String] path the path to test
320
- # @return [Boolean] whether _path_ represents a file
321
- # @note This method should usually only be called by FUSE, and not called
322
- # directly
323
- def file?(path)
324
- File.file?(homepath(path))
438
+ # Called when filesystem is initialised
439
+ # @overload init(info)
440
+ # @abstract
441
+ # @param [Context] context
442
+ # @param [Struct] info connection information
443
+ # @return [void]
444
+ # @note This method should usually only be called by FUSE, and not
445
+ # called directly
446
+ def init(context, info)
325
447
  end
326
448
 
327
- # Test whether _path_ is the name of a directory
328
- # @param [String] path the path to test
329
- # @return [Boolean] whether _path_ represents a directory
330
- # @note This method should usually only be called by FUSE, and not called
331
- # directly
332
- def directory?(path)
333
- File.directory?(homepath(path))
334
- end
335
-
336
- # Open a file handle to the file given by _path_, with the mode given by
337
- # _mode_. Right now, the supported modes are "rw", "w", and "r", for
338
- # read/write, write, and read. If the mode is write or read/write and no
339
- # file exists at _path_, on will be created.
340
- # @param [String] path the path to the file to open
341
- # @param [String] mode the mode to open the file with
342
- # @param rfusefs ignored
343
- # @return [File] a file handle of the file given by _path_
344
- # @note This method should usually only be called by FUSE, and not called
345
- # directly
346
- def raw_open(path, mode, rfusefs = nil)
347
- mode = case mode
348
- when "rw" then File::RDWR | File::CREAT | File::BINARY
349
- when "r" then File::RDONLY | File::BINARY
350
- when "w" then File::WRONLY | File::CREAT | File::BINARY
351
- end
352
- File.open(homepath(path), mode)
353
- end
354
-
355
- # Read _size_ bytes, starting at _offset_, from the file handle given by _raw_
356
- # (which is returned by a call to _raw_open_). If raw is nil and a path is
357
- # given, a file will be opened at _path_, and closed after the data is read.
358
- # Otherwise, _path_ is ignored.
359
- # @param [String] path the path to the file
360
- # @param [Integer] offset the offset, in bytes, from the start of the file
361
- # to start reading from.
362
- # @param [Integer] size the amount of bytes to read
363
- # @param [File] raw the file handle to read from
364
- # @return [String] a binary string of the requested data
365
- # @note This method should usually only be called by FUSE, and not called
366
- # directly
367
- def raw_read(path, offset, size, raw = nil)
368
- file = raw || raw_open(path, "r")
369
- file.seek(offset, :SET)
370
- file.read(size)
371
- ensure
372
- file.close if raw.nil?
373
- end
374
-
375
- # Sync writes to the underlying filesystem. If _raw_ is nil, no operation is
376
- # performed.
377
- # @param path ignored
378
- # @param datasync ignored
379
- # @param [File] raw the file handle to sync
449
+ # Create a hard link to file
450
+ # @overload link(context,from,to)
451
+ # @abstract
452
+ # @param [Context] context
453
+ # @param [String] from
454
+ # @param [String] to
380
455
  # @return [void]
381
- # @note This method should usually only be called by FUSE, and not called
382
- # directly
383
- def raw_sync(path, datasync, raw = nil)
384
- return if raw.nil? # Should we sync anyway?
385
- raw.fdatasync
456
+ # @raise [Errno]
457
+ # @note This method should usually only be called by FUSE, and not
458
+ # called directly
459
+ def link(context, from, to)
460
+ hfrom = homepath(context.uid, from)
461
+ check_writable(context.uid, from)
462
+ File.link(hfrom, to)
463
+ File.chown(context.uid, hfrom) if context.uid == 0
386
464
  end
387
465
 
388
- # Close the file handle given by _raw_.
389
- # @param path ignored
390
- # @param [File] raw the file handle to close
466
+ # (see RFuse::Fuse#mkdir)
467
+ # @note This method should usually only be called by FUSE, and not
468
+ # called directly
469
+ def mkdir(context, path, mode)
470
+ check_writable(context.uid, path)
471
+ hpath = homepath(context.uid, path)
472
+ Dir.mkdir(hpath, mode)
473
+ File.chown(context.uid, context.gid, hpath) if context.uid == 0
474
+ end
475
+
476
+ # File open operation
477
+ # @overload open(context,path,ffi)
478
+ # @abstract
479
+ # @param [Context] context
480
+ # @param [String] path
481
+ # @param [FileInfo] ffi
482
+ # file open flags etc.
483
+ # The fh attribute may be used to store an arbitrary filehandle object
484
+ # which will be passed to all subsequent operations on this file
485
+ # @raise [Errno::ENOPERM] if user is not permitted to open the file
486
+ # @raise [Errno] for other errors
391
487
  # @return [void]
392
- # @note This method should usually only be called by FUSE, and not called
393
- # directly
394
- def raw_close(path, raw = nil)
395
- return if raw.nil? # ???
396
- raw.close
397
- end
398
-
399
- # Write _sz_ bytes from _buf_ to the file handle _raw_, starting at _off_.
400
- # If no file handle is given, one will be opened at the given path, and
401
- # closed after the write is complete.
402
- # @param [String] path the path of the file
403
- # @param [Integer] off the offset, in bytes, to starting writing to the
404
- # file at
405
- # @param [Integer] sz the amount of bytes to read from buf
406
- # @param [String] buf a binary string containing the data to be written
407
- # @param [File] raw the file handle to write to
488
+ # @note This method should usually only be called by FUSE, and not
489
+ # called directly
490
+ def open(context, path, ffi)
491
+ if ffi.flags & File::RDWR != 0
492
+ mask = 0666
493
+ elsif ffi.flags & File::WRONLY != 0
494
+ mask = 0222
495
+ else
496
+ mask = 0444
497
+ end
498
+ hpath = homepath(context.uid, path)
499
+ unless mode_mask(hpath, mask, context.uid)
500
+ fail Errno::EACCES, path
501
+ end
502
+
503
+ # We pass the flags straight into File.open because the constants
504
+ # in RFuse::Fcntl and File::Constants have the same values, because
505
+ # they both have the same values as the open syscall. If this were
506
+ # not the case, we'd have the map the values of RFuse::Fcntl to
507
+ # their equivalents in File::Constants
508
+ ffi.fh = File.open(hpath, ffi.flags)
509
+ end
510
+
511
+ # Open directory
512
+ # @overload opendir(context,path,name)
513
+ # @abstract
514
+ # @param [Context] context
515
+ # @param [String] path
516
+ # @param [FileInfo] ffi
408
517
  # @return [void]
409
- # @note This method should usually only be called by FUSE, and not called
410
- # directly
411
- def raw_write(path, off, sz, buf, raw = nil)
412
- file = raw || File.open(path, "w")
413
- file.seek(off, :SET)
414
- file.write(buf[0...sz])
415
- ensure
416
- file.close if raw.nil?
518
+ # @raise [Errno]
519
+ # Unless the 'default_permissions' mount option is given, this method
520
+ # should check if opendir is permitted for this directory. Optionally
521
+ # opendir may also return an arbitrary filehandle in the fuse_file_info
522
+ # structure, which will be available to {#readdir}, {#fsyncdir},
523
+ # {#releasedir}.
524
+ # @note This method should usually only be called by FUSE, and not
525
+ # called directly
526
+ def opendir(context, path, ffi)
527
+ check_listable(context.uid, path)
528
+ ffi.fh = Dir.new(homepath(context.uid, path))
529
+ end
530
+
531
+ # Read data from an open file
532
+ # @overload read(context,path,size,offset,ffi)
533
+ # @abstract
534
+ # @param [Context] context
535
+ # @param [String] path
536
+ # @param [Integer] size
537
+ # @param [Integer] offset
538
+ # @param [FileInfo] ffi
539
+ # @return [String] should be exactly the number of bytes requested, or
540
+ # empty string on EOF
541
+ # @raise [Errno]
542
+ # @note This method should usually only be called by FUSE, and not
543
+ # called directly
544
+ def read(context, path, size, offset, ffi)
545
+ if offset < 0
546
+ ffi.fh.seek(offset, :END)
547
+ else
548
+ ffi.fh.seek(offset, :SET)
549
+ end
550
+ ffi.fh.read(size)
551
+ end
552
+
553
+ # (see RFuse::Fuse#readdir)
554
+ # @note This method should usually only be called by FUSE, and not
555
+ # called directly
556
+ def readdir(context, path, filler, offset, ffi)
557
+ filler.push(".", nil, 0)
558
+ filler.push("..", nil, 0)
559
+ ffi.fh.pos = offset
560
+ ffi.fh.each do |filename|
561
+ next if filename == '.' || filename == '..'
562
+ filler.push(filename, nil, 0)
563
+ end
417
564
  end
565
+
566
+ # Resolve target of symbolic link
567
+ # @overload readlink(context,path,size)
568
+ # @abstract
569
+ # @param [Context] context
570
+ # @param [String] path
571
+ # @param [Integer] size if the resolved path is greater than this size
572
+ # it should be truncated
573
+ # @return [String] the resolved link path
574
+ # @raise [Errno]
575
+ # @note This method should usually only be called by FUSE, and not
576
+ # called directly
577
+ def readlink(context, path, size)
578
+ # Is it okay that we return the 'real' path here?
579
+ File.readlink(homepath(context.uid, path))[0 ... size]
580
+ end
581
+
582
+ # Release an open file
583
+ # @overload release(context,path,ffi)
584
+ # @abstract
585
+ # @param [Context] context
586
+ # @param [String] path
587
+ # @param [FileInfo] ffi
588
+ # @return [void]
589
+ # Release is called when there are no more references to an open file:
590
+ # all file descriptors are closed and all memory mappings are unmapped.
591
+ #
592
+ # For every {#open} call there will be exactly one {#release} call with
593
+ # the same flags and file descriptor. It is possible to have a file
594
+ # opened more than once, in which case only the last release will mean,
595
+ # that no more reads/writes will happen on the file.
596
+ # @note This method should usually only be called by FUSE, and not
597
+ # called directly
598
+ def release(context, path, ffi)
599
+ ffi.fh.close
600
+ end
601
+
602
+ # (see RFuse::Fuse#rename)
603
+ # @note This method should usually only be called by FUSE, and not
604
+ # called directly
605
+ def rename(context, from, to)
606
+ hfrom = homepath(context.uid, from)
607
+ hto = homepath(context.uid, to)
608
+ check_readable(context.uid, from)
609
+ check_writable(File.dirname(from), context.uid)
610
+ check_writable(File.dirname(to), context.uid)
611
+ FileUtils.mv(hfrom, hto, :force => true)
612
+ end
613
+
614
+ # Create a symbolic link
615
+ # @overload symlink(context,to,from)
616
+ # @abstract
617
+ # @param [Context] context
618
+ # @param [String] to
619
+ # @param [String] from
620
+ # @return [void]
621
+ # @raise [Errno]
622
+ # Create a symbolic link named "from" which, when evaluated, will lead
623
+ # to "to".
624
+ # @note This method should usually only be called by FUSE, and not
625
+ # called directly
626
+ def symlink(context, to, from)
627
+ check_writable(context.uid, from)
628
+ hfrom = homepath(context.uid, from)
629
+ File.symlink(to, hfrom)
630
+ begin
631
+ File.lchown(context.uid, context.gid, hfrom)
632
+ rescue
633
+ end
634
+ end
635
+
636
+ # Change the size of a file
637
+ # @overload truncate(context,path,offset)
638
+ # @abstract
639
+ # @param [Context] context
640
+ # @param [String] path
641
+ # @param [Integer] offset
642
+ # @return [void]
643
+ # @raise [Errno]
644
+ # @note This method should usually only be called by FUSE, and not
645
+ # called directly
646
+ def truncate(context, path, offset)
647
+ check_writable(context.uid, path)
648
+ File.truncate(homepath(context.uid, path), offset)
649
+ end
650
+
651
+ # Remove a file
652
+ # @overload unlink(context,path)
653
+ # @abstract
654
+ # @param [Context] context
655
+ # @param [String] path
656
+ # @return [void]
657
+ # @raise [Errno]
658
+ # @note This method should usually only be called by FUSE, and not
659
+ # called directly
660
+ def unlink(context, path)
661
+ check_writable(context.uid, File.dirname(path))
662
+ File.unlink(homepath(context.uid, path))
663
+ end
664
+
665
+ # Change access/modification times of a file
666
+ # @overload utimens(context,path,actime,modtime)
667
+ # @abstract
668
+ # @param [Context] context
669
+ # @param [String] path
670
+ # @param [Integer] actime access time in nanoseconds
671
+ # @param [Integer] modtime modification time in nanoseconds
672
+ # @return [void]
673
+ # @raise [Errno]
674
+ # @note This method should usually only be called by FUSE, and not
675
+ # called directly
676
+ def utimens(context, path, actime, modtime)
677
+ check_writable(context.uid, path)
678
+ File.utime(actime, modtime, homepath(context.uid, path))
679
+ end
680
+
681
+ # Write data to an open file
682
+ # @overload write(context,path,data,offset,ffi)
683
+ # @abstract
684
+ # @param [Context] context
685
+ # @param [String] path
686
+ # @param [String] data
687
+ # @param [Integer] offset
688
+ # @param [FileInfo] ffi
689
+ # @return [Integer] exactly the number of bytes requested except on
690
+ # error
691
+ # @raise [Errno]
692
+ # @note This method should usually only be called by FUSE, and not
693
+ # called directly
694
+ def write(context, path, data, offset, ffi)
695
+ if offset < 0
696
+ ffi.fh.seek(offset, :END)
697
+ else
698
+ ffi.fh.seek(offset, :SET)
699
+ end
700
+ ffi.fh.write(data)
701
+ end
702
+
703
+ begin
704
+
705
+ require 'ffi-xattr'
706
+
707
+ # Get extended attribute
708
+ # @overload getxattr(context,path,name)
709
+ # @abstract
710
+ # @param [Context] context
711
+ # @param [String] path
712
+ # @param [String] name
713
+ # @return [String] attribute value
714
+ # @raise [Errno] Errno::ENOATTR if attribute does not exist
715
+ # @note This method should usually only be called by FUSE, and not
716
+ # called directly
717
+ # @note This method is only defined if the gem 'ffi-xattr' is
718
+ # available
719
+ def getxattr(context, path, name)
720
+ check_readable(context.uid, path)
721
+ hpath = homepath(context.uid, path)
722
+ check_nodata Xattr::Lib.get(hpath, false, name)
723
+ end
724
+
725
+ # Set extended attributes
726
+ # @overload setxattr(context,path,name,data,flags)
727
+ # @abstract
728
+ # @param [Context] context
729
+ # @param [String] path
730
+ # @param [String] name
731
+ # @param [String] data
732
+ # @param [Integer] flags
733
+ # @return [void]
734
+ # @raise [Errno]
735
+ # @note This method should usually only be called by FUSE, and not
736
+ # called directly
737
+ # @note This method is only defined if the gem 'ffi-xattr' is
738
+ # available
739
+ def setxattr(context, path, name, data, flags)
740
+ check_writable(context.uid, path)
741
+ hpath = homepath(context.uid, path)
742
+ Xattr::Lib.set(hpath, false, name, data)
743
+ end
744
+
745
+ # List extended attributes
746
+ # @overload listxattr(context,path)
747
+ # @abstract
748
+ # @param [Context] context
749
+ # @param [String] path
750
+ # @return [Array<String>] list of attribute names
751
+ # @raise [Errno]
752
+ # @note This method should usually only be called by FUSE, and not
753
+ # called directly
754
+ # @note This method is only defined if the gem 'ffi-xattr' is
755
+ # available
756
+ def listxattr(context, path)
757
+ check_readable(context.uid, path)
758
+ hpath = homepath(context.uid, path)
759
+ check_nodata Xattr::Lib.list(hpath, false)
760
+ end
761
+
762
+ # Remove extended attribute
763
+ # @overload removexattr(context,path,name)
764
+ # @abstract
765
+ # @param [Context] context
766
+ # @param [String] path
767
+ # @param [String] name attribute to remove
768
+ # @return [void]
769
+ # @raise [Errno]
770
+ # @note This method should usually only be called by FUSE, and not
771
+ # called directly
772
+ # @note This method is only defined if the gem 'ffi-xattr' is
773
+ # available
774
+ def removexattr(context, path, name)
775
+ check_writable(context.uid, path)
776
+ hpath = homepath(context.uid, path)
777
+ Xattr::Lib.remove(hpath, false, name)
778
+ end
779
+
780
+ # Raise Errno::ENODATA if _value_ is nil. Otherwise, return _value_.
781
+ # @param [Object] value the value to check
782
+ # @return [Object] the given value
783
+ # @raise [Errno::ENODATA] if value is nil
784
+ def check_nodata(value)
785
+ if value.nil?
786
+ fail Errno::ENODATA
787
+ end
788
+ value
789
+ end
790
+ private :check_nodata
791
+
792
+ rescue LoadError # require 'ffi-xattr'
793
+ # We don't have the library, so we don't define those methods
794
+ end
795
+
418
796
  end
data/lib/homefs.rb CHANGED
@@ -1 +1,2 @@
1
1
  require 'homefs/homefs'
2
+ require 'homefs/debug'
metadata CHANGED
@@ -1,31 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: homefs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dylan Frese
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-10-26 00:00:00.000000000 Z
11
+ date: 2015-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rfusefs
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: rb-inotify
14
+ name: rfuse
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - ">="
@@ -39,9 +25,9 @@ dependencies:
39
25
  - !ruby/object:Gem::Version
40
26
  version: '0'
41
27
  description: |-
42
- FuseFS currently written in Ruby that directs filesystem
43
- calls to a directory relative to the calling user's home
44
- directory.
28
+ FUSE filesystem currently written in Ruby that directs
29
+ filesystem calls to a directory relative to the calling
30
+ user's home directory.
45
31
  email: dmfrese@gmail.com
46
32
  executables:
47
33
  - homefs
@@ -50,6 +36,7 @@ extra_rdoc_files: []
50
36
  files:
51
37
  - bin/homefs
52
38
  - lib/homefs.rb
39
+ - lib/homefs/debug.rb
53
40
  - lib/homefs/homefs.rb
54
41
  homepage:
55
42
  licenses: