File Coverage

blib/lib/DateTime/TimeZone/Local/Unix.pm
Criterion Covered Total %
statement 79 117 67.5
branch 21 56 37.5
condition 9 36 25.0
subroutine 20 25 80.0
pod 2 4 50.0
total 131 238 55.0


line stmt bran cond sub pod time code
1             package DateTime::TimeZone::Local::Unix;
2              
3 2     2   35427 use strict;
  2         5  
  2         79  
4 2     2   12 use warnings;
  2         4  
  2         103  
5 2     2   9 use namespace::autoclean;
  2         4  
  2         16  
6              
7             our $VERSION = '2.67';
8              
9 2     2   188 use Cwd 3;
  2         31  
  2         152  
10 2     2   9 use Try::Tiny;
  2         3  
  2         77  
11              
12 2     2   7 use parent 'DateTime::TimeZone::Local';
  2         3  
  2         12  
13              
14             sub Methods {
15 284     284 1 1144 return qw(
16             FromEnv
17             FromEtcTimezone
18             FromEtcLocaltime
19             FromEtcTIMEZONE
20             FromEtcSysconfigClock
21             FromEtcDefaultInit
22             );
23             }
24              
25 288     288 1 702 sub EnvVars { return 'TZ' }
26              
27             ## no critic (Variables::ProhibitPackageVars)
28             our $EtcDir = '/etc';
29             ## use critic
30              
31             sub _EtcFile {
32 12     12   26 shift;
33 12         261 return File::Spec->catfile( $EtcDir, @_ );
34             }
35              
36             sub FromEtcLocaltime {
37 4     4   3617 my $class = shift;
38              
39 4         17 my $lt_file = $class->_EtcFile('localtime');
40 4 50 33     232 return unless -r $lt_file && -s _;
41              
42 4         12 my $real_name;
43 4 50       79 if ( -l $lt_file ) {
44              
45             # The _Readlink sub exists so the test suite can mock it.
46 4         25 $real_name = $class->_Readlink($lt_file);
47             }
48              
49 4   66     56 $real_name ||= $class->_FindMatchingZoneinfoFile($lt_file);
50              
51 4 50       19 if ( defined $real_name ) {
52 4         83 my ( undef, $dirs, $file ) = File::Spec->splitpath($real_name);
53              
54 4 50       32 my @parts = grep { defined && length } File::Spec->splitdir($dirs),
  25         96  
55             $file;
56              
57 4         18 foreach my $x ( reverse 0 .. $#parts ) {
58 11 100       58 my $name = (
59             $x < $#parts
60             ? join '/', @parts[ $x .. $#parts ]
61             : $parts[$x]
62             );
63              
64             my $tz = try {
65             ## no critic (Variables::RequireInitializationForLocalVars)
66 11     11   336 local $SIG{__DIE__};
67 11         60 DateTime::TimeZone->new( name => $name );
68 11         71 };
69              
70 11 100       277 return $tz if $tz;
71             }
72             }
73             }
74              
75             sub _Readlink {
76 2     2   8034 my $link = $_[1];
77              
78             # Using abs_path will resolve multiple levels of link indirection,
79             # whereas readlink just follows the link to the next target.
80 2         297 return Cwd::abs_path($link);
81             }
82              
83             ## no critic (Variables::ProhibitPackageVars)
84             our $ZoneinfoDir = '/usr/share/zoneinfo';
85             ## use critic
86              
87             # for systems where /etc/localtime is a copy of a zoneinfo file
88             sub _FindMatchingZoneinfoFile {
89 0     0   0 shift;
90 0         0 my $file_to_match = shift;
91              
92             # For some reason, under at least macOS 10.13 High Sierra,
93             # /usr/share/zoneinfo is a link to a link to a directory. And no, I didn't
94             # stutter. This is fine, and it passes the -d below. But File::Find does
95             # not understand a link to be a directory, so rather than incur the
96             # overhead of telling File::Find::find() to follow symbolic links, we just
97             # resolve it here.
98 0         0 my $zone_info_dir = $ZoneinfoDir;
99 0         0 $zone_info_dir = readlink $zone_info_dir while -l $zone_info_dir;
100              
101 0 0       0 return unless -d $zone_info_dir;
102              
103 0         0 require File::Basename;
104 0         0 require File::Compare;
105 0         0 require File::Find;
106              
107 0         0 my $size = -s $file_to_match;
108              
109 0         0 my $real_name;
110             try {
111             ## no critic (Variables::RequireInitializationForLocalVars)
112 0     0   0 local $SIG{__DIE__};
113 0         0 local $_;
114              
115             File::Find::find(
116             {
117             wanted => sub {
118 0 0 0     0 if (
      0        
      0        
      0        
      0        
119             !defined $real_name
120             && -f $_
121             && !-l $_
122             && $size == -s _
123              
124             # This fixes RT 24026 - apparently such a
125             # file exists on FreeBSD and it can cause a
126             # false positive
127             && File::Basename::basename($_) ne 'posixrules'
128             && File::Compare::compare( $_, $file_to_match ) == 0
129             ) {
130 0         0 $real_name = $_;
131              
132             # File::Find has no mechanism for bailing in the
133             # middle of a find.
134 0         0 die { found => 1 };
135             }
136             },
137 0         0 no_chdir => 1,
138             },
139             $zone_info_dir,
140             );
141             }
142             catch {
143 0 0 0 0   0 die $_ unless ref $_ && $_->{found};
144 0         0 };
145              
146 0         0 return $real_name;
147             }
148              
149             sub FromEtcTimezone {
150 3     3   1890 my $class = shift;
151              
152 3         15 my $tz_file = $class->_EtcFile('timezone');
153 3 100 66     125 return unless -f $tz_file && -r _;
154              
155 2 50       88 open my $fh, '<', $tz_file
156             or die "Cannot read $tz_file: $!";
157 2         7 my $name = do { local $/ = undef; <$fh> };
  2         15  
  2         68  
158 2 50       38 close $fh or die $!;
159              
160 2         21 $name =~ s/^\s+|\s+$//g;
161              
162 2 50       25 return unless $class->_IsValidName($name);
163              
164             return try {
165             ## no critic (Variables::RequireInitializationForLocalVars)
166 2     2   73 local $SIG{__DIE__};
167 2         20 DateTime::TimeZone->new( name => $name );
168 2         22 };
169             }
170              
171             sub FromEtcTIMEZONE {
172 1     1   1935 my $class = shift;
173              
174 1         7 my $tz_file = $class->_EtcFile('TIMEZONE');
175 1 50 33     42 return unless -f $tz_file && -r _;
176              
177             ## no critic (InputOutput::RequireBriefOpen)
178 0 0       0 open my $fh, '<', $tz_file
179             or die "Cannot read $tz_file: $!";
180              
181 0         0 my $name;
182 0         0 while ( defined( $name = <$fh> ) ) {
183 0 0       0 if ( $name =~ /\A\s*TZ\s*=\s*(\S+)/ ) {
184 0         0 $name = $1;
185 0         0 last;
186             }
187             }
188              
189 0 0       0 close $fh or die $!;
190              
191 0 0       0 return unless $class->_IsValidName($name);
192              
193             return try {
194             ## no critic (Variables::RequireInitializationForLocalVars)
195 0     0   0 local $SIG{__DIE__};
196 0         0 DateTime::TimeZone->new( name => $name );
197 0         0 };
198             }
199              
200             # RedHat uses this
201             sub FromEtcSysconfigClock {
202 2     2 0 2162 my $class = shift;
203              
204 2         9 my $clock_file = $class->_EtcFile('sysconfig/clock');
205 2 100 66     122 return unless -r $clock_file && -f _;
206              
207 1         9 my $name = $class->_ReadEtcSysconfigClock($clock_file);
208              
209 1 50       18 return unless $class->_IsValidName($name);
210              
211             return try {
212             ## no critic (Variables::RequireInitializationForLocalVars)
213 1     1   33 local $SIG{__DIE__};
214 1         10 DateTime::TimeZone->new( name => $name );
215 1         24 };
216             }
217              
218             # this is a separate function so that it can be overridden in the test suite
219             sub _ReadEtcSysconfigClock {
220 0     0   0 shift;
221 0         0 my $clock_file = shift;
222              
223 0 0       0 open my $fh, '<', $clock_file
224             or die "Cannot read $clock_file: $!";
225              
226             ## no critic (Variables::RequireInitializationForLocalVars)
227 0         0 local $_;
228 0         0 while (<$fh>) {
229 0 0       0 return $1 if /^(?:TIME)?ZONE="([^"]+)"/;
230             }
231              
232 0 0       0 close $fh or die $!;
233             }
234              
235             sub FromEtcDefaultInit {
236 2     2 0 3715 my $class = shift;
237              
238 2         11 my $init_file = $class->_EtcFile('default/init');
239 2 50 33     60 return unless -r $init_file && -f _;
240              
241 2         12 my $name = $class->_ReadEtcDefaultInit($init_file);
242              
243 2 50       24 return unless $class->_IsValidName($name);
244              
245             return try {
246             ## no critic (Variables::RequireInitializationForLocalVars)
247 2     2   73 local $SIG{__DIE__};
248 2         21 DateTime::TimeZone->new( name => $name );
249 2         17 };
250             }
251              
252             # this is a separate function so that it can be overridden in the test
253             # suite
254             sub _ReadEtcDefaultInit {
255 1     1   3 shift;
256 1         3 my $init_file = shift;
257              
258 1 50       45 open my $fh, '<', $init_file
259             or die "Cannot read $init_file: $!";
260              
261             ## no critic (Variables::RequireInitializationForLocalVars)
262 1         4 local $_;
263 1         30 while (<$fh>) {
264 1 50       31 return $1 if /^TZ=(.+)/;
265             }
266              
267 0 0         close $fh or die $!;
268             }
269              
270             1;
271              
272             # ABSTRACT: Determine the local system's time zone on Unix
273              
274             __END__
275              
276             =pod
277              
278             =encoding UTF-8
279              
280             =head1 NAME
281              
282             DateTime::TimeZone::Local::Unix - Determine the local system's time zone on Unix
283              
284             =head1 VERSION
285              
286             version 2.67
287              
288             =head1 SYNOPSIS
289              
290             my $tz = DateTime::TimeZone->new( name => 'local' );
291              
292             my $tz = DateTime::TimeZone::Local->TimeZone();
293              
294             =head1 DESCRIPTION
295              
296             This module provides methods for determining the local time zone on a Unix
297             platform.
298              
299             =head1 HOW THE TIME ZONE IS DETERMINED
300              
301             This class tries the following methods of determining the local time zone, in
302             the order listed here:
303              
304             =over 4
305              
306             =item * $ENV{TZ}
307              
308             It checks C<< $ENV{TZ} >> for a valid time zone name.
309              
310             =item * F</etc/timezone>
311              
312             If this file exists, it is read and its contents are used as a time zone name.
313              
314             Note that this file may be out of date on many systems, as modern distros may
315             not do a good job of updating this file. If you find that this file is not
316             being updated, you may want to consider deleting it so that one of the
317             following methods can be used.
318              
319             =item * F</etc/localtime>
320              
321             If this file is a symlink to an Olson database time zone file (usually in
322             F</usr/share/zoneinfo>) then it uses the target file's path name to determine
323             the time zone name. For example, if the path is
324             F</usr/share/zoneinfo/America/Chicago>, the time zone is "America/Chicago".
325              
326             Some systems just copy the relevant file to F</etc/localtime> instead of making
327             a symlink. In this case, we look in F</usr/share/zoneinfo> for a file that has
328             the same size and content as F</etc/localtime> to determine the local time
329             zone.
330              
331             =item * F</etc/TIMEZONE>
332              
333             If this file exists, it is opened and we look for a line starting like "TZ =
334             ...". If this is found, it should indicate a time zone name.
335              
336             =item * F</etc/sysconfig/clock>
337              
338             If this file exists, it is opened and we look for a line starting like
339             "TIMEZONE = ..." or "ZONE = ...". If this is found, it should indicate a time
340             zone name.
341              
342             =item * F</etc/default/init>
343              
344             If this file exists, it is opened and we look for a line starting like
345             "TZ=...". If this is found, it should indicate a time zone name.
346              
347             =back
348              
349             B<Note:> Some systems such as virtual machine boxes may lack any of these
350             files. You can confirm that this is case by running:
351              
352             $ ls -l /etc/localtime /etc/timezone /etc/TIMEZONE \
353             /etc/sysconfig/clock /etc/default/init
354              
355             If this is the case, then when checking for timezone handling you are likely to
356             get an exception:
357              
358             $ perl -wle 'use DateTime; DateTime->now( time_zone => "local" )'
359             Cannot determine local time zone
360              
361             In that case, you should consult your system F<man> pages for details on how to
362             address that problem. In one such case reported to us, a FreeBSD virtual
363             machine had been built without any of these files. The user was able to run the
364             FreeBSD F<tzsetup> utility. That installed F</etc/localtime>, after which the
365             above timezone diagnostic ran silently, I<i.e.>, without throwing an exception.
366              
367             =head1 SUPPORT
368              
369             Bugs may be submitted at L<https://github.com/houseabsolute/DateTime-TimeZone/issues>.
370              
371             =head1 SOURCE
372              
373             The source code repository for DateTime-TimeZone can be found at L<https://github.com/houseabsolute/DateTime-TimeZone>.
374              
375             =head1 AUTHOR
376              
377             Dave Rolsky <autarch@urth.org>
378              
379             =head1 COPYRIGHT AND LICENSE
380              
381             This software is copyright (c) 2026 by Dave Rolsky.
382              
383             This is free software; you can redistribute it and/or modify it under
384             the same terms as the Perl 5 programming language system itself.
385              
386             The full text of the license can be found in the
387             F<LICENSE> file included with this distribution.
388              
389             =cut