Subject: Re: bug#29259: tail does not seek to the end of block device

From: Pádraig Brady <[email-masked]>
Date: Mon, 13 Nov 2017 00:03:06 -0800

On 12/11/17 22:21, Pádraig Brady wrote:
> On 12/11/17 21:52, Paul Eggert wrote:
>> Why doesn't lseek work for this?
> 
> Good call, it probably would.
> Something like the following is more acceptable
> since it adds very little complexity:

Full patch attached with tests.

cheers,
Pádraig

>From 691b547c6ad68ee17cafe161b55138b962c3cd7a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <[email-masked]>
Date: Sun, 12 Nov 2017 23:06:08 -0800
Subject: [PATCH] tail: seek to the end of block devices

* src/tail.c (tail_bytes): Try lseek(..., SEEK_END) when
we can't determine the file size.
* tests/tail-2/end-of-device.sh: Add a new root only test.
* tests/local.mk: Reference the new test.
* NEWS: Mention the improvement.
Paul Eggert suggested using lseek() (rather than ioctl(BLKGETSIZE64)).
Fixes https://bugs.gnu.org/29259
---
 NEWS                          |  5 +++++
 src/tail.c                    | 27 +++++++++++++++---------
 tests/local.mk                |  1 +
 tests/tail-2/end-of-device.sh | 48 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 71 insertions(+), 10 deletions(-)
 create mode 100755 tests/tail-2/end-of-device.sh

diff --git a/NEWS b/NEWS
index 3e6704d..bc5d5be 100644
--- a/NEWS
+++ b/NEWS
@@ -31,6 +31,11 @@ GNU coreutils NEWS                                    -*- outline -*-
   timeout would have then waited for the time limit to expire.
   [bug introduced in coreutils-8.27]
 
+** Improvements
+
+  tail --bytes=NUM will efficiently seek to the end of block devices,
+  rather than reading from the start.
+
 ** Build-related
 
   Default man pages are now distributed which are used if perl is
diff --git a/src/tail.c b/src/tail.c
index 1c7418d..c6de7f1 100644
--- a/src/tail.c
+++ b/src/tail.c
@@ -1830,12 +1830,11 @@ tail_bytes (const char *pretty_filename, int fd, uintmax_t n_bytes,
 
   if (from_start)
     {
-      if ( ! presume_input_pipe
-           && S_ISREG (stats.st_mode) && n_bytes <= OFF_T_MAX)
-        {
-          xlseek (fd, n_bytes, SEEK_CUR, pretty_filename);
-          *read_pos += n_bytes;
-        }
+      if (! presume_input_pipe && n_bytes <= OFF_T_MAX
+          && ((S_ISREG (stats.st_mode)
+               && xlseek (fd, n_bytes, SEEK_CUR, pretty_filename) >= 0)
+              || lseek (fd, n_bytes, SEEK_CUR) != -1))
+        *read_pos += n_bytes;
       else
         {
           int t = start_bytes (pretty_filename, fd, n_bytes, read_pos);
@@ -1846,12 +1845,20 @@ tail_bytes (const char *pretty_filename, int fd, uintmax_t n_bytes,
     }
   else
     {
-      off_t end_pos = ((! presume_input_pipe && usable_st_size (&stats)
-                        && n_bytes <= OFF_T_MAX)
-                       ? stats.st_size : -1);
+      off_t end_pos = -1;
+      off_t current_pos = -1;
+
+      if (! presume_input_pipe && n_bytes <= OFF_T_MAX)
+        {
+          if (usable_st_size (&stats))
+            end_pos = stats.st_size;
+          else if ((current_pos = lseek (fd, -n_bytes, SEEK_END)) != -1)
+            end_pos = current_pos + n_bytes;
+        }
       if (end_pos <= ST_BLKSIZE (stats))
         return pipe_bytes (pretty_filename, fd, n_bytes, read_pos);
-      off_t current_pos = xlseek (fd, 0, SEEK_CUR, pretty_filename);
+      if (current_pos == -1)
+        current_pos = xlseek (fd, 0, SEEK_CUR, pretty_filename);
       if (current_pos < end_pos)
         {
           off_t bytes_remaining = end_pos - current_pos;
diff --git a/tests/local.mk b/tests/local.mk
index 74426f6..8ee7c50 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -135,6 +135,7 @@ all_root_tests =				\
   tests/rm/one-file-system.sh			\
   tests/rm/read-only.sh				\
   tests/tail-2/append-only.sh			\
+  tests/tail-2/end-of-device.sh			\
   tests/touch/now-owned-by-other.sh
 
 ALL_RECURSIVE_TARGETS += check-root
diff --git a/tests/tail-2/end-of-device.sh b/tests/tail-2/end-of-device.sh
new file mode 100755
index 0000000..9123c80
--- /dev/null
+++ b/tests/tail-2/end-of-device.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+# Ensure that tail seeks to the end of a device
+
+# Copyright (C) 2017 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ tail
+
+# need write access to local device
+# (even though we don't actually write anything)
+require_root_
+require_local_dir_
+
+get_device_size() {
+  BLOCKDEV=blockdev
+  $BLOCKDEV -V >/dev/null 2>&1 || BLOCKDEV=/sbin/blockdev
+  $BLOCKDEV --getsize64 "$1"
+}
+
+# Get path to device the current dir is on.
+# Note df can only get fs size, not device size.
+device=$(df --output=source . | tail -n1) || framework_failure_
+
+dev_size=$(get_device_size "$device") ||
+  skip_ "failed to determine size of $device"
+
+tail_offset=$(expr $dev_size - 1023) ||
+  skip_ "failed to determine tail offset"
+
+timeout 10 tail -c 1024 "$device" > end1 || fail=1
+timeout 10 tail -c +"$tail_offset" "$device" > end2 || fail=1
+test $(wc -c < end1) = 1024 || fail=1
+cmp end1 end2 || fail=1
+
+Exit $fail
-- 
2.9.3