Subject: Re: bug#11424: coreutils: split tests hang on /dev/zero on GNU/Hurd

From: Paul Eggert <[email-masked]>
Date: Tue, 08 May 2012 09:27:19 -0700

On 05/08/2012 01:39 AM, Jim Meyering wrote:
> I went ahead and pushed the less-invasive fix.

Hmm, I don't see this on Savannah; is this part
of the problem where usable_st_size got pushed?

> Your behavior-changing one is more than welcome, too.

I came up with a better idea, and propose this patch
instead.  The idea is to fall back on lseek if
st_size is not reliable.  This allows the programs
to work in more cases, including the case in question.
One test case needs to be removed because it assumes
a command must fail, where it now typically works.

>From bba6f199f621fb434c5df09a0b0278a081be87e2 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Tue, 8 May 2012 09:22:22 -0700
Subject: [PATCH] maint: handle file sizes more reliably

Problem reported by Samuel Thibault in <http://debbugs.gnu.org/11424>.
* NEWS: Document this.
* src/dd.c (skip):
* src/split.c (main):
* src/truncate.c (do_ftruncate, main):
On files where st_size is not portable, fall back on using lseek
with SEEK_END to determine the size.  Although strictly speaking
POSIX says the behavior is implementation-defined, in practice
if lseek returns a nonnegative value it's a reasonable one to
use for the file size.
* src/dd.c (dd_copy): It's OK to truncate shared memory objects.
* src/du.c (duinfo_add): Check for overflow.
(print_only_size): Report overflow.
(process_file): Ignore negative file sizes in the --apparent-size case.
* src/od.c (skip): Fix comment about st_size.
* src/stat.c (print_stat): Don't report negative sizes as huge
positive ones.
* src/system.h (usable_st_size): Symlinks have reliable st_size too.
* tests/misc/truncate-dir-fail: Don't assume that getting the size
of a dir is not allowed, as it's now allowed on many platforms,
e.g., GNU/Linux.
---
 NEWS                         |    3 ++
 src/dd.c                     |   17 +++++++++---
 src/du.c                     |   12 ++++++---
 src/od.c                     |    3 +-
 src/split.c                  |    7 +++++
 src/stat.c                   |    2 +-
 src/system.h                 |    3 +-
 src/truncate.c               |   56 ++++++++++++++++++++++++++++++-----------
 tests/misc/truncate-dir-fail |    3 --
 9 files changed, 76 insertions(+), 30 deletions(-)

diff --git a/NEWS b/NEWS
index 2dc6531..c911d52 100644
--- a/NEWS
+++ b/NEWS
@@ -21,6 +21,9 @@ GNU coreutils NEWS                                    -*- outline -*-
 
 ** New features
 
+  dd, split, and truncate now allow any seekable files in situations
+  where the file size is needed, instead of insisting on regular files.
+
   fmt now accepts the --goal=WIDTH (-g) option.
 
 ** Changes in behavior
diff --git a/src/dd.c b/src/dd.c
index 4626de2..75109bc 100644
--- a/src/dd.c
+++ b/src/dd.c
@@ -1542,9 +1542,18 @@ skip (int fdesc, char const *file, uintmax_t records, size_t blocksize,
       if (fdesc == STDIN_FILENO)
         {
            struct stat st;
+           off_t file_size;
            if (fstat (STDIN_FILENO, &st) != 0)
              error (EXIT_FAILURE, errno, _("cannot fstat %s"), quote (file));
-           if (S_ISREG (st.st_mode) && st.st_size < (input_offset + offset))
+           if (usable_st_size (&st))
+             file_size = st.st_size;
+           else
+             {
+               file_size = skip_via_lseek (file, fdesc, 0, SEEK_END);
+               if (skip_via_lseek (file, fdesc, offset, SEEK_CUR) < 0)
+                 error (EXIT_FAILURE, errno, _("%s: cannot skip"), quote (file));
+             }
+           if (0 <= file_size && file_size < input_offset + offset)
              {
                /* When skipping past EOF, return the number of _full_ blocks
                 * that are not skipped, and set offset to EOF, so the caller
@@ -2104,8 +2113,8 @@ dd_copy (void)
         }
     }
 
-  /* If the last write was converted to a seek, then for a regular file,
-     ftruncate to extend the size.  */
+  /* If the last write was converted to a seek, then for a regular file
+     or shared memory object, ftruncate to extend the size.  */
   if (final_op_was_seek)
     {
       struct stat stdout_stat;
@@ -2114,7 +2123,7 @@ dd_copy (void)
           error (0, errno, _("cannot fstat %s"), quote (output_file));
           return EXIT_FAILURE;
         }
-      if (S_ISREG (stdout_stat.st_mode))
+      if (S_ISREG (stdout_stat.st_mode) || S_TYPEISSHM (&stdout_stat))
         {
           off_t output_offset = lseek (STDOUT_FILENO, 0, SEEK_CUR);
           if (output_offset > stdout_stat.st_size)
diff --git a/src/du.c b/src/du.c
index 41c9535..7333941 100644
--- a/src/du.c
+++ b/src/du.c
@@ -99,7 +99,8 @@ duinfo_set (struct duinfo *a, uintmax_t size, struct timespec tmax)
 static inline void
 duinfo_add (struct duinfo *a, struct duinfo const *b)
 {
-  a->size += b->size;
+  uintmax_t sum = a->size + b->size;
+  a->size = a->size <= sum ? sum : UINTMAX_MAX;
   if (timespec_cmp (a->tmax, b->tmax) < 0)
     a->tmax = b->tmax;
 }
@@ -370,8 +371,11 @@ static void
 print_only_size (uintmax_t n_bytes)
 {
   char buf[LONGEST_HUMAN_READABLE + 1];
-  fputs (human_readable (n_bytes, buf, human_output_opts,
-                         1, output_block_size), stdout);
+  fputs ((n_bytes == UINTMAX_MAX
+          ? _("Infinity")
+          : human_readable (n_bytes, buf, human_output_opts,
+                            1, output_block_size)),
+         stdout);
 }
 
 /* Print size (and optionally time) indicated by *PDUI, followed by STRING.  */
@@ -495,7 +499,7 @@ process_file (FTS *fts, FTSENT *ent)
 
   duinfo_set (&dui,
               (apparent_size
-               ? sb->st_size
+               ? MAX (0, sb->st_size)
                : (uintmax_t) ST_NBLOCKS (*sb) * ST_NBLOCKSIZE),
               (time_type == time_mtime ? get_stat_mtime (sb)
                : time_type == time_atime ? get_stat_atime (sb)
diff --git a/src/od.c b/src/od.c
index 7593796..a25f965 100644
--- a/src/od.c
+++ b/src/od.c
@@ -983,8 +983,7 @@ skip (uintmax_t n_skip)
 
       if (fstat (fileno (in_stream), &file_stats) == 0)
         {
-          /* The st_size field is valid only for regular files
-             (and for symbolic links, which cannot occur here).
+          /* The st_size field is valid for regular files.
              If the number of bytes left to skip is larger than
              the size of the current file, we can decrement n_skip
              and go on to the next file.  Skip this optimization also
diff --git a/src/split.c b/src/split.c
index 99f6390..fe14d80 100644
--- a/src/split.c
+++ b/src/split.c
@@ -1344,6 +1344,13 @@ main (int argc, char **argv)
   if (split_type == type_chunk_bytes || split_type == type_chunk_lines)
     {
       off_t input_offset = lseek (STDIN_FILENO, 0, SEEK_CUR);
+      if (0 <= input_offset && ! usable_st_size (&stat_buf))
+        {
+          file_size = lseek (STDIN_FILENO, 0, SEEK_END);
+          input_offset = (file_size < 0
+                          ? file_size
+                          : lseek (STDIN_FILENO, input_offset, SEEK_SET));
+        }
       if (input_offset < 0)
         error (EXIT_FAILURE, 0, _("%s: cannot determine file size"),
                quote (infile));
diff --git a/src/stat.c b/src/stat.c
index b2e1030..d001cda 100644
--- a/src/stat.c
+++ b/src/stat.c
@@ -954,7 +954,7 @@ print_stat (char *pformat, size_t prefix_len, unsigned int m,
       out_uint_x (pformat, prefix_len, minor (statbuf->st_rdev));
       break;
     case 's':
-      out_uint (pformat, prefix_len, statbuf->st_size);
+      out_int (pformat, prefix_len, statbuf->st_size);
       break;
     case 'B':
       out_uint (pformat, prefix_len, ST_NBLOCKSIZE);
diff --git a/src/system.h b/src/system.h
index e3d3156..06f09cb 100644
--- a/src/system.h
+++ b/src/system.h
@@ -605,7 +605,8 @@ bad_cast (char const *s)
 static inline bool
 usable_st_size (struct stat const *sb)
 {
-  return S_ISREG (sb->st_mode) || S_TYPEISSHM (sb) || S_TYPEISTMO (sb);
+  return (S_ISREG (sb->st_mode) || S_ISLNK (sb->st_mode)
+          || S_TYPEISSHM (sb) || S_TYPEISTMO (sb));
 }
 
 void usage (int status) ATTRIBUTE_NORETURN;
diff --git a/src/truncate.c b/src/truncate.c
index 9b847d2..49d9732 100644
--- a/src/truncate.c
+++ b/src/truncate.c
@@ -157,23 +157,36 @@ do_ftruncate (int fd, char const *fname, off_t ssize, off_t rsize,
     }
   if (rel_mode)
     {
-      uintmax_t const fsize = rsize < 0 ? sb.st_size : rsize;
+      uintmax_t fsize;
 
-      if (rsize < 0) /* fstat used above to get size.  */
+      if (0 <= rsize)
+        fsize = rsize;
+      else
         {
-          if (!S_ISREG (sb.st_mode) && !S_TYPEISSHM (&sb))
+          off_t file_size;
+          if (usable_st_size (&sb))
             {
-              error (0, 0, _("cannot get the size of %s"), quote (fname));
-              return false;
+              file_size = sb.st_size;
+              if (file_size < 0)
+                {
+                  /* Sanity check.  Overflow is the only reason I can think
+                     this would ever go negative. */
+                  error (0, 0, _("%s has unusable, apparently negative size"),
+                         quote (fname));
+                  return false;
+                }
             }
-          if (sb.st_size < 0)
+          else
             {
-              /* Sanity check. Overflow is the only reason I can think
-                 this would ever go negative. */
-              error (0, 0, _("%s has unusable, apparently negative size"),
-                     quote (fname));
-              return false;
+              file_size = lseek (fd, 0, SEEK_END);
+              if (file_size < 0)
+                {
+                  error (0, errno, _("cannot get the size of %s"),
+                         quote (fname));
+                  return false;
+                }
             }
+          fsize = file_size;
         }
 
       if (rel_mode == rm_min)
@@ -348,15 +361,28 @@ main (int argc, char **argv)
     {
       /* FIXME: Maybe support getting size of block devices.  */
       struct stat sb;
+      off_t file_size = -1;
       if (stat (ref_file, &sb) != 0)
         error (EXIT_FAILURE, errno, _("cannot stat %s"), quote (ref_file));
-      if (!S_ISREG (sb.st_mode) && !S_TYPEISSHM (&sb))
-        error (EXIT_FAILURE, 0, _("cannot get the size of %s"),
+      if (usable_st_size (&sb))
+        file_size = sb.st_size;
+      else
+        {
+          int ref_fd = open (ref_file, O_RDONLY);
+          if (0 <= ref_fd)
+            {
+              off_t file_end = lseek (ref_fd, 0, SEEK_END);
+              if (0 <= file_end && close (ref_fd) == 0)
+                file_size = file_end;
+            }
+        }
+      if (file_size < 0)
+        error (EXIT_FAILURE, errno, _("cannot get the size of %s"),
                quote (ref_file));
       if (!got_size)
-        size = sb.st_size;
+        size = file_size;
       else
-        rsize = sb.st_size;
+        rsize = file_size;
     }
 
   oflags = O_WRONLY | (no_create ? 0 : O_CREAT) | O_NONBLOCK;
diff --git a/tests/misc/truncate-dir-fail b/tests/misc/truncate-dir-fail
index 1167352..54a3147 100755
--- a/tests/misc/truncate-dir-fail
+++ b/tests/misc/truncate-dir-fail
@@ -22,7 +22,4 @@ print_ver_ truncate
 # truncate on dir not allowed
 truncate -s+0 . && fail=1
 
-# getting the size of a dir is not allowed
-truncate -r. file && fail=1
-
 Exit $fail
-- 
1.7.6.5