Skip to content

Commit 3c22d4a

Browse files
committed
[GR-45621] Add Pathname#lutime method
PullRequest: truffleruby/4159
2 parents 577c1dd + bbd1e2f commit 3c22d4a

File tree

9 files changed

+159
-104
lines changed

9 files changed

+159
-104
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Compatibility:
4646
* Handle either positional or keywords arguments by default in `Struct.new` (#3039, @rwstauner).
4747
* Promote `Set` class to core library (#3039, @andrykonchin).
4848
* Support `connect_timeout` keyword argument to `TCPSocket.{new,open}` (#3421, @manefz, @patricklinpl, @nirvdrum, @rwstauner).
49+
* Add `File.lutime` and `Pathname#lutime` methods (#3039, @andrykonchin).
4950

5051
Performance:
5152

lib/truffle/pathname.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,10 @@ def truncate(length) File.truncate(@path, length) end
896896
# See <tt>File.utime</tt>. Update the access and modification times.
897897
def utime(atime, mtime) File.utime(atime, mtime, @path) end
898898

899+
# See <tt>File.lutime</tt>. Update the access and modification times.
900+
# Same as Pathname#utime, but does not follow symbolic links.
901+
def lutime(atime, mtime) File.lutime(atime, mtime, @path) end
902+
899903
# See <tt>File.basename</tt>. Returns the last component of the path.
900904
def basename(*args) self.class.new(File.basename(@path, *args)) end
901905

spec/ruby/core/file/lutime_spec.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
require_relative '../../spec_helper'
2+
require_relative 'shared/update_time'
3+
4+
describe "File.lutime" do
5+
it_behaves_like :update_time, :lutime
6+
end
27

38
describe "File.lutime" do
49
platform_is_not :windows do
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
describe :update_time, shared: true do
2+
before :all do
3+
@time_is_float = platform_is :windows
4+
end
5+
6+
before :each do
7+
@atime = Time.now
8+
@mtime = Time.now
9+
@file1 = tmp("specs_file_utime1")
10+
@file2 = tmp("specs_file_utime2")
11+
touch @file1
12+
touch @file2
13+
end
14+
15+
after :each do
16+
rm_r @file1, @file2
17+
end
18+
19+
it "sets the access and modification time of each file" do
20+
File.send(@method, @atime, @mtime, @file1, @file2)
21+
22+
if @time_is_float
23+
File.atime(@file1).should be_close(@atime, 0.0001)
24+
File.mtime(@file1).should be_close(@mtime, 0.0001)
25+
File.atime(@file2).should be_close(@atime, 0.0001)
26+
File.mtime(@file2).should be_close(@mtime, 0.0001)
27+
else
28+
File.atime(@file1).to_i.should be_close(@atime.to_i, TIME_TOLERANCE)
29+
File.mtime(@file1).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE)
30+
File.atime(@file2).to_i.should be_close(@atime.to_i, TIME_TOLERANCE)
31+
File.mtime(@file2).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE)
32+
end
33+
end
34+
35+
it "uses the current times if two nil values are passed" do
36+
tn = Time.now
37+
File.send(@method, nil, nil, @file1, @file2)
38+
39+
if @time_is_float
40+
File.atime(@file1).should be_close(tn, 0.050)
41+
File.mtime(@file1).should be_close(tn, 0.050)
42+
File.atime(@file2).should be_close(tn, 0.050)
43+
File.mtime(@file2).should be_close(tn, 0.050)
44+
else
45+
File.atime(@file1).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE)
46+
File.mtime(@file1).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE)
47+
File.atime(@file2).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE)
48+
File.mtime(@file2).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE)
49+
end
50+
end
51+
52+
it "accepts an object that has a #to_path method" do
53+
File.send(@method, @atime, @mtime, mock_to_path(@file1), mock_to_path(@file2))
54+
end
55+
56+
it "accepts numeric atime and mtime arguments" do
57+
if @time_is_float
58+
File.send(@method, @atime.to_f, @mtime.to_f, @file1, @file2)
59+
60+
File.atime(@file1).should be_close(@atime, 0.0001)
61+
File.mtime(@file1).should be_close(@mtime, 0.0001)
62+
File.atime(@file2).should be_close(@atime, 0.0001)
63+
File.mtime(@file2).should be_close(@mtime, 0.0001)
64+
else
65+
File.send(@method, @atime.to_i, @mtime.to_i, @file1, @file2)
66+
67+
File.atime(@file1).to_i.should be_close(@atime.to_i, TIME_TOLERANCE)
68+
File.mtime(@file1).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE)
69+
File.atime(@file2).to_i.should be_close(@atime.to_i, TIME_TOLERANCE)
70+
File.mtime(@file2).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE)
71+
end
72+
end
73+
74+
it "may set nanosecond precision" do
75+
t = Time.utc(2007, 11, 1, 15, 25, 0, 123456.789r)
76+
File.send(@method, t, t, @file1)
77+
78+
File.atime(@file1).nsec.should.between?(0, 123500000)
79+
File.mtime(@file1).nsec.should.between?(0, 123500000)
80+
end
81+
82+
it "returns the number of filenames in the arguments" do
83+
File.send(@method, @atime.to_f, @mtime.to_f, @file1, @file2).should == 2
84+
end
85+
86+
platform_is :linux do
87+
platform_is wordsize: 64 do
88+
it "allows Time instances in the far future to set mtime and atime (but some filesystems limit it up to 2446-05-10 or 2038-01-19 or 2486-07-02)" do
89+
# https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout#Inode_Timestamps
90+
# "Therefore, timestamps should not overflow until May 2446."
91+
# https://lwn.net/Articles/804382/
92+
# "On-disk timestamps hitting the y2038 limit..."
93+
# The problem seems to be being improved, but currently it actually fails on XFS on RHEL8
94+
# https://rubyci.org/logs/rubyci.s3.amazonaws.com/rhel8/ruby-master/log/20201112T123004Z.fail.html.gz
95+
# Amazon Linux 2023 returns 2486-07-02 in this example
96+
# http://rubyci.s3.amazonaws.com/amazon2023/ruby-master/log/20230322T063004Z.fail.html.gz
97+
time = Time.at(1<<44)
98+
File.send(@method, time, time, @file1)
99+
100+
[559444, 2486, 2446, 2038].should.include? File.atime(@file1).year
101+
[559444, 2486, 2446, 2038].should.include? File.mtime(@file1).year
102+
end
103+
end
104+
end
105+
end

spec/ruby/core/file/utime_spec.rb

Lines changed: 2 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,6 @@
11
require_relative '../../spec_helper'
2+
require_relative 'shared/update_time'
23

34
describe "File.utime" do
4-
5-
before :all do
6-
@time_is_float = platform_is :windows
7-
end
8-
9-
before :each do
10-
@atime = Time.now
11-
@mtime = Time.now
12-
@file1 = tmp("specs_file_utime1")
13-
@file2 = tmp("specs_file_utime2")
14-
touch @file1
15-
touch @file2
16-
end
17-
18-
after :each do
19-
rm_r @file1, @file2
20-
end
21-
22-
it "sets the access and modification time of each file" do
23-
File.utime(@atime, @mtime, @file1, @file2)
24-
if @time_is_float
25-
File.atime(@file1).should be_close(@atime, 0.0001)
26-
File.mtime(@file1).should be_close(@mtime, 0.0001)
27-
File.atime(@file2).should be_close(@atime, 0.0001)
28-
File.mtime(@file2).should be_close(@mtime, 0.0001)
29-
else
30-
File.atime(@file1).to_i.should be_close(@atime.to_i, TIME_TOLERANCE)
31-
File.mtime(@file1).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE)
32-
File.atime(@file2).to_i.should be_close(@atime.to_i, TIME_TOLERANCE)
33-
File.mtime(@file2).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE)
34-
end
35-
end
36-
37-
it "uses the current times if two nil values are passed" do
38-
tn = Time.now
39-
File.utime(nil, nil, @file1, @file2)
40-
if @time_is_float
41-
File.atime(@file1).should be_close(tn, 0.050)
42-
File.mtime(@file1).should be_close(tn, 0.050)
43-
File.atime(@file2).should be_close(tn, 0.050)
44-
File.mtime(@file2).should be_close(tn, 0.050)
45-
else
46-
File.atime(@file1).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE)
47-
File.mtime(@file1).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE)
48-
File.atime(@file2).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE)
49-
File.mtime(@file2).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE)
50-
end
51-
end
52-
53-
it "accepts an object that has a #to_path method" do
54-
File.utime(@atime, @mtime, mock_to_path(@file1), mock_to_path(@file2))
55-
end
56-
57-
it "accepts numeric atime and mtime arguments" do
58-
if @time_is_float
59-
File.utime(@atime.to_f, @mtime.to_f, @file1, @file2)
60-
File.atime(@file1).should be_close(@atime, 0.0001)
61-
File.mtime(@file1).should be_close(@mtime, 0.0001)
62-
File.atime(@file2).should be_close(@atime, 0.0001)
63-
File.mtime(@file2).should be_close(@mtime, 0.0001)
64-
else
65-
File.utime(@atime.to_i, @mtime.to_i, @file1, @file2)
66-
File.atime(@file1).to_i.should be_close(@atime.to_i, TIME_TOLERANCE)
67-
File.mtime(@file1).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE)
68-
File.atime(@file2).to_i.should be_close(@atime.to_i, TIME_TOLERANCE)
69-
File.mtime(@file2).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE)
70-
end
71-
end
72-
73-
it "may set nanosecond precision" do
74-
t = Time.utc(2007, 11, 1, 15, 25, 0, 123456.789r)
75-
File.utime(t, t, @file1)
76-
File.atime(@file1).nsec.should.between?(0, 123500000)
77-
File.mtime(@file1).nsec.should.between?(0, 123500000)
78-
end
79-
80-
it "returns the number of filenames in the arguments" do
81-
File.utime(@atime.to_f, @mtime.to_f, @file1, @file2).should == 2
82-
end
83-
84-
platform_is :linux do
85-
platform_is wordsize: 64 do
86-
it "allows Time instances in the far future to set mtime and atime (but some filesystems limit it up to 2446-05-10 or 2038-01-19 or 2486-07-02)" do
87-
# https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout#Inode_Timestamps
88-
# "Therefore, timestamps should not overflow until May 2446."
89-
# https://lwn.net/Articles/804382/
90-
# "On-disk timestamps hitting the y2038 limit..."
91-
# The problem seems to be being improved, but currently it actually fails on XFS on RHEL8
92-
# https://rubyci.org/logs/rubyci.s3.amazonaws.com/rhel8/ruby-master/log/20201112T123004Z.fail.html.gz
93-
# Amazon Linux 2023 returns 2486-07-02 in this example
94-
# http://rubyci.s3.amazonaws.com/amazon2023/ruby-master/log/20230322T063004Z.fail.html.gz
95-
time = Time.at(1<<44)
96-
File.utime(time, time, @file1)
97-
[559444, 2486, 2446, 2038].should.include? File.atime(@file1).year
98-
[559444, 2486, 2446, 2038].should.include? File.mtime(@file1).year
99-
end
100-
end
101-
end
5+
it_behaves_like :update_time, :utime
1026
end

spec/tags/core/file/lutime_tags.txt

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/main/c/truffleposix/truffleposix.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ int truffleposix_poll_single_fd(int fd, int events, int timeout_ms) {
9999
return poll(&fds, 1, timeout_ms) >= 0 ? fds.revents : -1;
100100
}
101101

102+
int truffleposix_lutimes(const char *filename, long atime_sec, int atime_nsec,
103+
long mtime_sec, int mtime_nsec) {
104+
struct timespec timespecs[2];
105+
106+
timespecs[0].tv_sec = atime_sec;
107+
timespecs[0].tv_nsec = atime_nsec;
108+
timespecs[1].tv_sec = mtime_sec;
109+
timespecs[1].tv_nsec = mtime_nsec;
110+
111+
return utimensat(AT_FDCWD, filename, timespecs, AT_SYMLINK_NOFOLLOW);
112+
}
113+
102114
int truffleposix_utimes(const char *filename, long atime_sec, int atime_nsec,
103115
long mtime_sec, int mtime_nsec) {
104116
struct timespec timespecs[2];

src/main/ruby/truffleruby/core/file.rb

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,31 @@ def self.lstat(path)
790790
Stat.lstat path
791791
end
792792

793+
##
794+
# Sets the access and modification times of each named file to the first
795+
# two arguments. If a file is a symlink, this method acts upon the link
796+
# itself as opposed to its referent; for the inverse behavior, see
797+
# File.utime. Returns the number of file names in the argument list.
798+
def self.lutime(atime, mtime, *paths)
799+
if !atime || !mtime
800+
now = Time.now
801+
atime ||= now
802+
mtime ||= now
803+
end
804+
805+
atime = Time.at(atime) unless Primitive.is_a?(atime, Time)
806+
mtime = Time.at(mtime) unless Primitive.is_a?(mtime, Time)
807+
808+
paths.each do |path|
809+
path = Truffle::Type.coerce_to_path(path)
810+
n = POSIX.truffleposix_lutimes(path, atime.to_i, atime.nsec,
811+
mtime.to_i, mtime.nsec)
812+
Errno.handle unless n == 0
813+
end
814+
815+
paths.size
816+
end
817+
793818
##
794819
# Returns the modification time for the named file as a Time object.
795820
#
@@ -1062,10 +1087,10 @@ def self.unlink(*paths)
10621087
end
10631088

10641089
##
1065-
# Sets the access and modification times of each named
1066-
# file to the first two arguments. Returns the number
1067-
# of file names in the argument list.
1068-
# #=> Integer
1090+
# Sets the access and modification times of each named file to the first
1091+
# two arguments. If a file is a symlink, this method acts upon its
1092+
# referent rather than the link itself; for the inverse behavior see
1093+
# File.lutime. Returns the number of file names in the argument list.
10691094
def self.utime(atime, mtime, *paths)
10701095
if !atime || !mtime
10711096
now = Time.now

src/main/ruby/truffleruby/core/posix.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ def self.attach_function_eagerly(native_name, argument_types, return_type,
204204
attach_function :lseek, [:int, :off_t, :int], :off_t
205205
attach_function :truffleposix_lstat, [:string, :pointer], :int, LIBTRUFFLEPOSIX
206206
attach_function :truffleposix_lstat_mode, [:string], :mode_t, LIBTRUFFLEPOSIX
207+
attach_function :truffleposix_lutimes, [:string, :long, :int, :long, :int], :int, LIBTRUFFLEPOSIX
207208
attach_function :truffleposix_major, [:dev_t], :uint, LIBTRUFFLEPOSIX
208209
attach_function :truffleposix_minor, [:dev_t], :uint, LIBTRUFFLEPOSIX
209210
attach_function :mkdir, [:string, :mode_t], :int

0 commit comments

Comments
 (0)