├── LICENSE.txt ├── README.md ├── package.lisp ├── tests.lisp ├── tests ├── empty-file ├── one-byte-file └── two-byte-file ├── trivial-file-size.asd └── trivial-file-size.lisp /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Paul M. Rodriguez 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # trivial-file-size 2 | 3 | This library exports a single function, `file-size-in-octets`. It 4 | returns the size of a file in bytes, using system calls when possible. 5 | 6 | The canonical way to determine the size of a file in bytes, using 7 | Common Lisp, is to open the file with an element type of 8 | `(unsigned-byte 8)` and then calculate the length of the stream. This 9 | is less than ideal. In most cases it would be better to get the size 10 | of the file from its metadata, using a system call. 11 | 12 | This is a problem I have run into several times in several different 13 | projects. I want it *solved*, once and for all. 14 | 15 | At the moment, getting the file size from metadata is supported for 16 | the following Lisps: 17 | 18 | - SBCL 19 | - CMUCL 20 | - Clozure CL 21 | - CLISP 22 | - Allegro CL 23 | - ABCL 24 | - GCL 25 | - LispWorks 26 | - ECL (on Unix only) 27 | 28 | For other Lisps and platforms, we fall back to opening the file and 29 | calling `file-length` on the stream. 30 | 31 | This library is as much a call to arms as it is a resource. If you 32 | know how to stat a file on *your* Common Lisp implementation, on 33 | *your* platform, please make a pull request -- or just open an issue 34 | to point me to the right documentation, and I'll do all the work. 35 | -------------------------------------------------------------------------------- /package.lisp: -------------------------------------------------------------------------------- 1 | ;;;; package.lisp 2 | 3 | (defpackage #:trivial-file-size 4 | (:use #:cl) 5 | (:import-from #:uiop 6 | #:ensure-pathname 7 | #:native-namestring) 8 | (:export #:file-size-in-octets)) 9 | -------------------------------------------------------------------------------- /tests.lisp: -------------------------------------------------------------------------------- 1 | (defpackage :trivial-file-size/tests 2 | (:use :cl :fiveam :trivial-file-size) 3 | (:export :run-file-size-tests)) 4 | (in-package :trivial-file-size/tests) 5 | 6 | (def-suite trivial-file-size) 7 | (in-suite trivial-file-size) 8 | 9 | (defun run-file-size-tests () 10 | (fiveam:run 'trivial-file-size)) 11 | 12 | (defun resolve-test-file (file) 13 | (asdf:system-relative-pathname 14 | "trivial-file-size/tests" 15 | (concatenate 'string "tests/" 16 | file))) 17 | 18 | (test empty-file 19 | (let ((file (resolve-test-file "empty-file"))) 20 | (is (zerop (file-size-in-octets file))))) 21 | 22 | (test one-byte-file 23 | (let ((file (resolve-test-file "one-byte-file"))) 24 | (is (= 1 (file-size-in-octets file))))) 25 | 26 | (test two-byte-file 27 | (let ((file (resolve-test-file "two-byte-file"))) 28 | (is (= 2 (file-size-in-octets file))))) 29 | 30 | (test no-such-file 31 | (let ((file (resolve-test-file "no-such-file"))) 32 | (let ((size (file-size-in-octets file))) 33 | (is #+abcl (= size 0) 34 | #-abcl (null size))))) 35 | -------------------------------------------------------------------------------- /tests/empty-file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruricolist/trivial-file-size/60fd1bf8c573c733a775580cb96abdaaa7413ba8/tests/empty-file -------------------------------------------------------------------------------- /tests/one-byte-file: -------------------------------------------------------------------------------- 1 | x -------------------------------------------------------------------------------- /tests/two-byte-file: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /trivial-file-size.asd: -------------------------------------------------------------------------------- 1 | ;;;; trivial-file-size.asd 2 | 3 | (defsystem "trivial-file-size" 4 | :description "Stat a file's size." 5 | :author "Paul M. Rodriguez " 6 | :license "MIT" 7 | :serial t 8 | :in-order-to ((test-op (test-op "trivial-file-size/tests"))) 9 | :depends-on ((:feature :sbcl (:require :sb-posix)) 10 | (:feature :allegro (:require "osi")) 11 | (:feature :cmucl (:require :unix)) 12 | (:feature :clisp (:require "syscalls")) 13 | "uiop") 14 | :components ((:file "package") 15 | (:file "trivial-file-size"))) 16 | 17 | (defsystem "trivial-file-size/tests" 18 | :description "Tests for trivial-file-size." 19 | :author "Paul M. Rodriguez " 20 | :license "MIT" 21 | :serial t 22 | :perform (test-op (o c) 23 | (symbol-call :trivial-file-size/tests 24 | '#:run-file-size-tests)) 25 | :depends-on ("fiveam" "trivial-file-size") 26 | :components ((:file "tests"))) 27 | -------------------------------------------------------------------------------- /trivial-file-size.lisp: -------------------------------------------------------------------------------- 1 | ;;;; trivial-file-size.lisp 2 | 3 | (in-package #:trivial-file-size) 4 | 5 | ;;; "trivial-file-size" goes here. Hacks and glory await! 6 | 7 | (deftype file-size () 8 | #+sbcl '(values (or null (integer 0 *)) &optional) 9 | #-sbcl '(or null (integer 0 *))) 10 | 11 | (defun file-size-from-stream (file) 12 | (with-open-file (in file 13 | :direction :input 14 | :element-type '(unsigned-byte 8)) 15 | (file-length in))) 16 | 17 | (declaim 18 | (ftype 19 | (function ((or string pathname)) 20 | file-size) 21 | file-size-in-octets)) 22 | 23 | (defun file-size-in-octets (file) 24 | "Return the size of FILE in octets. 25 | Whenever possible, get the size from the file's metadata. 26 | 27 | Some platforms (e.g. ABCL) may return 0 when the file does not exist." 28 | (multiple-value-bind (path namestring) 29 | (etypecase file 30 | (string (values (ensure-pathname file :want-pathname t) 31 | file)) 32 | (pathname (values file 33 | (native-namestring file)))) 34 | (declare (ignorable path namestring)) 35 | (handler-case 36 | (progn 37 | #+sbcl (sb-posix:stat-size (sb-posix:stat path)) 38 | #+cmucl (nth-value 8 (unix:unix-stat namestring)) 39 | #+ccl (ccl:file-data-size path) 40 | #+clisp (os:file-stat-size (os:file-stat path)) 41 | #+allegro (excl.osi:stat-size (excl.osi:stat path)) 42 | #+gcl (nth 1 (sys:stat namestring)) 43 | 44 | ;; According to trivial-features LispWorks pushes :unix to 45 | ;; `*features*`. 46 | #+(and lispworks unix) 47 | (sys:file-stat-size (sys:get-file-stat namestring)) 48 | 49 | #+(and lispworks (not unix) (not (or lispworks4 lispworks5 lispworks6 lispworks7))) 50 | (block nil 51 | (hcl:fast-directory-files 52 | file 53 | #'(lambda (f handle) 54 | (declare (ignore f)) 55 | (return (hcl:fdf-handle-size handle))))) 56 | #+(and lispworks (not unix) (or lispworks4 lispworks5 lispworks6 lispworks7)) 57 | (block nil 58 | (hcl:fast-directory-files 59 | file 60 | #'(lambda (f handle) 61 | (when (string= f (file-namestring file)) 62 | (return (hcl:fdf-handle-size handle)))))) 63 | 64 | #+abcl (stat/abcl namestring) 65 | 66 | #+(and ecl unix) 67 | (stat/ecl path) 68 | #+clasp (nth-value 0 (ext:stat namestring)) 69 | 70 | #-(or sbcl cmucl ccl clasp clisp allegro abcl gcl 71 | lispworks 72 | (and ecl unix)) 73 | (file-size-from-stream file)) 74 | (error () 75 | nil)))) 76 | 77 | #+abcl 78 | (defun stat/abcl (namestring) 79 | (let* ((class (java:jclass "java.io.File")) 80 | (method (java:jmethod class "length")) 81 | (file (java:jnew class namestring))) 82 | (java:jcall method file))) 83 | 84 | #+(and ecl unix) 85 | (defun stat/ecl (file) 86 | ;; Adapted from the ECL implementation of `file-write-date'. 87 | (ffi:clines "#include ") 88 | (ffi:clines "#include ") 89 | (ffi:c-inline (file) (:object) :object "{ 90 | cl_object file_size, filename = si_coerce_to_filename(#0); 91 | struct stat filestatus; 92 | int output; 93 | 94 | ecl_disable_interrupts(); 95 | output = stat((char*)filename->base_string.self, &filestatus); 96 | ecl_enable_interrupts(); 97 | 98 | if (output < 0) { 99 | file_size = ECL_NIL; 100 | } else { 101 | file_size = ecl_make_integer(filestatus.st_size); 102 | } 103 | @(return) = file_size; 104 | }")) 105 | --------------------------------------------------------------------------------