homefs 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/homefs/homefs.rb +283 -37
  3. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7c2589a5398a08b340c286465ef325c6a5cd479d
4
- data.tar.gz: e38703f8191b106040c3d24820662264b46543ed
3
+ metadata.gz: e74057ef8ec81a7efa25cb238fa1d960ec7fa727
4
+ data.tar.gz: 6a9bff9e3760f6d4f35f5a368849e17d7caecb07
5
5
  SHA512:
6
- metadata.gz: 1b24049d47a6c5c35bc69305edb6eb9a6b4306b491187beafa492cdfef44f793c11222dc6fb7edb9a517f01a4ac6d3ab2d63baf2cc3767493098770ba6de6115
7
- data.tar.gz: 6a75b18765bd4f35f9a464e0b15bf38418814978df1b409d75b15db78280dda08f987f32a43d955ee83c3f9c69bdf713ed58268098c2272b3995321b907512d8
6
+ metadata.gz: 60a6183228c6436b89a30581a85e47211b9bcf1a56f6fe4e0c50d63b30929c1aa456266be3e2dfde148cec82dbbe062131f57be173d2875c149125e6f83f42cf
7
+ data.tar.gz: 0a58275485f48cafdc452161d78cdd030b57e9965b22c15608c443db60842386cbdfa4b5d164e0751387cb5f0d0752689af2a634db8e12bff09c34d94603b061
data/lib/homefs/homefs.rb CHANGED
@@ -1,25 +1,89 @@
1
1
  require 'rfusefs'
2
2
 
3
+ # An instance of HomeFS; this implements all FuseFS methods, and ultimately
4
+ # handles reading, writing, and inspection of files.
5
+ #
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.
12
+ # @author Dylan Frese
3
13
  class HomeFS
14
+
15
+ # 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
+ # calling user's home directory, that is, the user interacting with the
18
+ # filesystem through open(2), read(2), write(2), etc.
19
+ # @param [String] path the path, relative to $HOME, to use as the root of
20
+ # the file system.
4
21
  def initialize(path)
5
22
  @relpath = path
6
- @dirlinks = Hash.new
7
23
  read_passwd
24
+ read_group
8
25
  end
9
26
 
27
+ # Read or re-read /etc/passwd to update the internal table of home
28
+ # directories for each UID. This method does not try to guess as to which
29
+ # user IDs are owned by actual users of the system, and which are utility
30
+ # accounts such as 'lp' or 'pulse'.
31
+ # @return [void]
10
32
  def read_passwd
11
- # Here we map each line in /etc/passwd to an array,
33
+ # Try to read from getent
34
+ passwd = `getent passwd`
35
+ if $? != 0
36
+ passwd = File.read('/etc/passwd')
37
+ end
38
+
39
+ @homedirs = Hash.new
40
+ @usernames = Hash.new
41
+
42
+ # Here we map each line in passwd to an array,
12
43
  # which Hash interprets as a key, value pair.
13
- @homedirs = Hash[
14
- File.readlines('/etc/passwd').map do |line|
15
- next if line.strip.empty?
16
- values = line.split(':')
17
- # UID, home directory
18
- [Integer(values[2]), values[5]]
19
- end
20
- ]
44
+ passwd.each_line.map { |line|
45
+ next if line.strip.empty?
46
+ values = line.split(':')
47
+ # Username, UID, home directory
48
+ [values[0], Integer(values[2]), values[5]]
49
+ }.reject { |line|
50
+ line.nil?
51
+ }.each { |username, uid, home|
52
+ @homedirs[uid] = home
53
+ @usernames[username] = uid
54
+ }
55
+ end
56
+
57
+ # Read or re-read /etc/group to update the internal table of group
58
+ # group membership.
59
+ # @return [void]
60
+ def read_group
61
+ # Try to read from getent
62
+ group = `getent group`
63
+ if $? != 0
64
+ group = File.read('/etc/group')
65
+ end
66
+
67
+ @groups = Hash.new
68
+
69
+ group.each_line.map { |line|
70
+ next if line.strip.empty?
71
+ _name, _, gid, members = line.split(':')
72
+ members = members.strip.split(',').map {|member| p member; @usernames[member]}
73
+ [Integer(gid), members]
74
+ }.reject { |line|
75
+ line.nil?
76
+ }.each { |gid, members|
77
+ @groups[gid] = members
78
+ }
21
79
  end
22
80
 
81
+ # Return the path to the root of HomeFS relative to the underlying
82
+ # filesystem. If _path_ is specified, it is taken to be relative to the
83
+ # root of HomeFS.
84
+ # @return [String] path to the root of the HomeFS relative to the underlying
85
+ # filesystem, or if _path_ is specified, the path to that resource
86
+ # relative to the underlying filesystem.
23
87
  def homepath(path = nil)
24
88
  uid = FuseFS.reader_uid
25
89
  basepath = @homedirs[uid]
@@ -27,85 +91,184 @@ class HomeFS
27
91
  # in case.
28
92
  raise Errno::ENOENT if basepath == nil
29
93
  if path
30
- "#{basepath}/#{@relpath}/#{path}"
94
+ File.join(basepath, @relpath, path)
31
95
  else
32
- "#{basepath}/#{@relpath}"
96
+ File.join(basepath, @relpath)
33
97
  end
34
98
  end
35
99
 
100
+ # Check whether the given file has a mode that matches the given mask. If
101
+ # check_ids is true, the third-least significant digit of the mask is set to
102
+ # zero if the calling user is not the owner of the file, and the
103
+ # second-least significant digit of the mask is zeroed if the calling user's
104
+ # group is not the group of the file. This has the effect of only checking
105
+ # the owner's permissions if we're the owner, and only checking the group
106
+ # permissions if we're in that group.
107
+ #
108
+ # This method checks if _any_ of the set bits in the mask are set in the
109
+ # file's mode. It does not check if the mask matches the file's mode, nor
110
+ # does it check if _all_ set bits in the mask are set in the file's mode.
111
+ #
112
+ # The first three least significant digits of the mode, in octal, (that is,
113
+ # the three rightmost digits) control the read (4), write (2), and execute
114
+ # (1) permissions of the file. The next digit controls setuid (4), setgid
115
+ # (2), and the sticky bit (1) of the file.
116
+ #
117
+ # It is recommended to see
118
+ # man chmod
119
+ # to get a more detailed description about file modes.
120
+ #
121
+ # @example Test if we can write to a file
122
+ # mode_mask("/example", 0222, true)
123
+ # mode_mask("/example", 0222)
124
+ # @example Test if a file is executable by anyone
125
+ # mode_mask("/binary", 0111, false)
126
+ # @example Test if a directory has the sticky bit set
127
+ # mode_mask("/tmp", 01000, false)
128
+ #
129
+ # @param [String] file the path to the file to check
130
+ # @param [Integer] mask the mask against which to check the file's mode.
131
+ # 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.
135
+ # @return [Boolean] whether the mode of _file_ matches the given mask
36
136
  def mode_mask(file, mask, check_ids = true)
37
137
  stat = File.stat(file)
38
- # First digit: setuid (4), setgid (2), sticky bit (1)
39
- # Second, third, and fourth digits: user, group, and
40
- # world permissions for read (4), write (2), execute (1)
41
- # (see `man chmod` for more)
42
138
  fmode = stat.mode
43
139
  if check_ids
44
140
  fuid, fgid = stat.uid, stat.gid
45
- uid, gid = FuseFS.reader_uid, FuseFS.reader_gid
141
+ uid = FuseFS.reader_uid
46
142
  # Zero out the third digit (in octal).
47
143
  # We could use a constant here, but this works
48
144
  # for a mask of any length
49
145
  if uid != fuid
50
146
  mask &= ~(mask & 0700)
51
147
  end
52
- if gid != fgid
148
+ if !@groups[fgid].include?(uid)
53
149
  mask &= ~(mask & 0070)
54
150
  end
55
151
  end
56
152
  fmode & mask != 0
57
153
  end
58
154
 
155
+ # Test whether we can write to the given path. If no file exists at _path_,
156
+ # we test if the parent directory is either writable or has the sticky bit
157
+ # set.
158
+ # @param [String] path the path to test
159
+ # @return [Boolean] whether the given path is writable
160
+ def writable?(path)
161
+ return true if mode_mask(path, 0222)
162
+ if !File.exist?(path)
163
+ mode_mask(File.dirname(path), 01222)
164
+ else
165
+ false
166
+ end
167
+ end
168
+
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
173
+ # @return [void]
174
+ # @note This method should usually only be called by FUSE, and not called
175
+ # directly
59
176
  def rename(from_path, to_path)
60
177
  FileUtils.mv(homepath(from_path), homepath(to_path))
61
178
  end
62
179
 
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
184
+ # @return [void]
185
+ # @note This method should usually only be called by FUSE, and not called
186
+ # directly
63
187
  def touch(path, modtime)
64
188
  File.utime(modtime, modtime, homepath(path))
65
189
  end
66
190
 
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
194
+ # @return [void]
195
+ # @note This method should usually only be called by FUSE, and not called
196
+ # directly
67
197
  def rmdir(path)
68
198
  FileUtils.rmdir(homepath(path))
69
199
  end
70
200
 
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
71
206
  def can_rmdir?(path)
72
- File.writable?(homepath(path))
207
+ File.writable?(homepath(path)) &&
208
+ Dir.new(path).to_a.size == 2
73
209
  end
74
210
 
211
+ # Make a new directory at _path_. This is similar to mkdir(1).
212
+ # @param [String] path the path to the directory to make
213
+ # @return [void]
214
+ # @note This method should usually only be called by FUSE, and not called
215
+ # directly
75
216
  def mkdir(path)
76
217
  FileUtils.mkdir(homepath(path))
77
218
  end
78
219
 
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
79
225
  def can_mkdir?(path)
80
- writable?(File.dirname(homepath(path)))
226
+ writable?(homepath(path))
81
227
  end
82
228
 
229
+ # Delete the file given by _path_. This is similar to rm(1).
230
+ # @param [String] path the path to the file to delete
231
+ # @return [void]
232
+ # @note This method should usually only be called by FUSE, and not called
233
+ # directly
83
234
  def delete(path)
84
235
  FileUtils.rm(homepath(path))
85
236
  end
86
237
 
238
+ # Test if we can delete the file given by _path_.
239
+ # @param [String] path the path to the file to test
240
+ # @return [void]
241
+ # @note This method should usually only be called by FUSE, and not called
242
+ # directly
87
243
  def can_delete?(path)
88
244
  writable?(File.dirname(homepath(path)))
89
245
  end
90
246
 
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
250
+ # @return [void]
251
+ # @note This method should usually only be called by FUSE, and not called
252
+ # directly
91
253
  def write_to(path, str)
92
254
  File.open(homepath(path), "wb") {|file| file.write(str) }
93
255
  end
94
256
 
95
- def writable?(path)
96
- mode_mask(path, 0222)
97
- end
98
-
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
99
262
  def can_write?(path)
100
- hpath = homepath(path)
101
- if File.exist?(hpath)
102
- writable?(hpath)
103
- else
104
- # FIXME: We don't support sticky bits
105
- writable?(File.dirname(hpath))
106
- end
263
+ writable?(homepath(path))
107
264
  end
108
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
109
272
  def times(path)
110
273
  atime = File.atime(homepath(path))
111
274
  mtime = File.mtime(homepath(path))
@@ -113,32 +276,73 @@ class HomeFS
113
276
  [atime, mtime, ctime]
114
277
  end
115
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
116
284
  def executable?(path)
117
285
  mode_mask(homepath(path), 0111)
118
286
  end
119
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
120
293
  def read_file(path)
121
294
  File.open(homepath(path), "rb") do |file|
122
295
  file.read
123
296
  end
124
297
  end
125
298
 
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
126
305
  def contents(path)
127
306
  Dir.new(homepath(path)).to_a
128
307
  end
129
308
 
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
130
314
  def size(path)
131
315
  File.size(homepath(path))
132
316
  end
133
317
 
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
134
323
  def file?(path)
135
324
  File.file?(homepath(path))
136
325
  end
137
326
 
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
138
332
  def directory?(path)
139
333
  File.directory?(homepath(path))
140
334
  end
141
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
142
346
  def raw_open(path, mode, rfusefs = nil)
143
347
  mode = case mode
144
348
  when "rw" then File::RDWR | File::CREAT | File::BINARY
@@ -148,25 +352,67 @@ class HomeFS
148
352
  File.open(homepath(path), mode)
149
353
  end
150
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
151
367
  def raw_read(path, offset, size, raw = nil)
152
- raw ||= raw_open(path, "r")
153
- raw.seek(offset, :SET)
154
- raw.read(size)
368
+ file = raw || raw_open(path, "r")
369
+ file.seek(offset, :SET)
370
+ file.read(size)
371
+ ensure
372
+ file.close if raw.nil?
155
373
  end
156
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
380
+ # @return [void]
381
+ # @note This method should usually only be called by FUSE, and not called
382
+ # directly
157
383
  def raw_sync(path, datasync, raw = nil)
158
384
  return if raw.nil? # Should we sync anyway?
159
385
  raw.fdatasync
160
386
  end
161
387
 
388
+ # Close the file handle given by _raw_.
389
+ # @param path ignored
390
+ # @param [File] raw the file handle to close
391
+ # @return [void]
392
+ # @note This method should usually only be called by FUSE, and not called
393
+ # directly
162
394
  def raw_close(path, raw = nil)
163
395
  return if raw.nil? # ???
164
396
  raw.close
165
397
  end
166
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
408
+ # @return [void]
409
+ # @note This method should usually only be called by FUSE, and not called
410
+ # directly
167
411
  def raw_write(path, off, sz, buf, raw = nil)
168
- raw ||= File.open(path, "w")
169
- raw.seek(off, :SET)
170
- raw.write(buf[0...sz])
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?
171
417
  end
172
418
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: homefs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dylan Frese