1
Fork 0
mirror of https://git.savannah.gnu.org/git/guile.git synced 2025-04-30 11:50:28 +02:00
guile/module/rnrs/io/ports.scm
Andy Wingo 075599e5b0 Implement R6RS custom textual ports
* module/ice-9/textual-ports.scm (custom-textual-port-read+flush-input):
(custom-textual-port-write):
(custom-textual-port-seek):
(custom-textual-port-close):
(custom-textual-port-random-access?):
(make-custom-textual-input-port):
(make-custom-textual-output-port):
(make-custom-textual-input/output-port): New procedures.
* doc/ref/api-io.texi (Ports): Update docs.
* doc/ref/r6rs.texi (rnrs io ports): Mention custom textual port
interfaces.
* module/rnrs/io/ports.scm: Re-export custom textual port interfaces
from (ice-9 textual-ports).
* test-suite/tests/r6rs-ports.test: Add minimal tests for textual ports.
2023-06-08 10:21:02 +02:00

539 lines
18 KiB
Scheme
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;;; ports.scm --- R6RS port API -*- coding: utf-8 -*-
;;;; Copyright (C) 2009-2011, 2013, 2019, 2023 Free Software Foundation, Inc.
;;;;
;;;; This library is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU Lesser General Public
;;;; License as published by the Free Software Foundation; either
;;;; version 3 of the License, or (at your option) any later version.
;;;;
;;;; This library 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
;;;; Lesser General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU Lesser General Public
;;;; License along with this library; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
;;; Author: Ludovic Courtès <ludo@gnu.org>
;;; Commentary:
;;;
;;; The I/O port API of the R6RS is provided by this module. In many areas
;;; it complements or refines Guile's own historical port API. For instance,
;;; it allows for binary I/O with bytevectors.
;;;
;;; Code:
(library (rnrs io ports (6))
(export eof-object eof-object?
;; auxiliary types
file-options buffer-mode buffer-mode?
eol-style native-eol-style error-handling-mode
make-transcoder transcoder-codec transcoder-eol-style
transcoder-error-handling-mode native-transcoder
latin-1-codec utf-8-codec utf-16-codec
;; transcoding bytevectors
bytevector->string string->bytevector
;; input & output ports
port? input-port? output-port?
port-eof?
port-transcoder binary-port? textual-port? transcoded-port
port-position set-port-position!
port-has-port-position? port-has-set-port-position!?
call-with-port close-port
;; input ports
open-bytevector-input-port
open-string-input-port
open-file-input-port
make-custom-binary-input-port
make-custom-textual-input-port
;; binary input
get-u8 lookahead-u8
get-bytevector-n get-bytevector-n!
get-bytevector-some get-bytevector-all
;; output ports
open-bytevector-output-port
open-string-output-port
open-file-output-port
make-custom-binary-output-port
call-with-bytevector-output-port
call-with-string-output-port
make-custom-textual-output-port
output-port-buffer-mode
flush-output-port
;; input/output ports
open-file-input/output-port
make-custom-binary-input/output-port
make-custom-textual-input/output-port
;; binary output
put-u8 put-bytevector
;; textual input
get-char get-datum get-line get-string-all get-string-n get-string-n!
lookahead-char
;; textual output
put-char put-datum put-string
;; standard ports
standard-input-port standard-output-port standard-error-port
current-input-port current-output-port current-error-port
;; condition types
&i/o i/o-error? make-i/o-error
&i/o-read i/o-read-error? make-i/o-read-error
&i/o-write i/o-write-error? make-i/o-write-error
&i/o-invalid-position i/o-invalid-position-error?
make-i/o-invalid-position-error
&i/o-filename i/o-filename-error? make-i/o-filename-error
i/o-error-filename
&i/o-file-protection i/o-file-protection-error?
make-i/o-file-protection-error
&i/o-file-is-read-only i/o-file-is-read-only-error?
make-i/o-file-is-read-only-error
&i/o-file-already-exists i/o-file-already-exists-error?
make-i/o-file-already-exists-error
&i/o-file-does-not-exist i/o-file-does-not-exist-error?
make-i/o-file-does-not-exist-error
&i/o-port i/o-port-error? make-i/o-port-error
i/o-error-port
&i/o-decoding i/o-decoding-error?
make-i/o-decoding-error
&i/o-encoding i/o-encoding-error?
make-i/o-encoding-error i/o-encoding-error-char)
(import (ice-9 binary-ports)
(only (ice-9 textual-ports)
make-custom-textual-input-port
make-custom-textual-output-port
make-custom-textual-input/output-port)
(only (rnrs base) assertion-violation)
(only (ice-9 ports internal)
port-write-buffer port-buffer-bytevector port-line-buffered?)
(only (rnrs bytevectors) bytevector-length)
(prefix (ice-9 iconv) iconv:)
(rnrs enums)
(rnrs records syntactic)
(rnrs exceptions)
(rnrs conditions)
(rnrs files) ;for the condition types
(srfi srfi-8)
(ice-9 rdelim)
(except (guile) raise display)
(prefix (only (guile) display)
guile:))
;;;
;;; Auxiliary types
;;;
(define-enumeration file-option
(no-create no-fail no-truncate)
file-options)
(define-enumeration buffer-mode
(none line block)
buffer-modes)
(define (buffer-mode? symbol)
(enum-set-member? symbol (enum-set-universe (buffer-modes))))
(define-enumeration eol-style
(lf cr crlf nel crnel ls none)
eol-styles)
(define (native-eol-style)
(eol-style none))
(define-enumeration error-handling-mode
(ignore raise replace)
error-handling-modes)
(define-record-type (transcoder %make-transcoder transcoder?)
(fields codec eol-style error-handling-mode))
(define* (make-transcoder codec
#:optional
(eol-style (native-eol-style))
(handling-mode (error-handling-mode replace)))
(%make-transcoder codec eol-style handling-mode))
(define (native-transcoder)
(make-transcoder (or (fluid-ref %default-port-encoding)
(latin-1-codec))))
(define (latin-1-codec)
"ISO-8859-1")
(define (utf-8-codec)
"UTF-8")
(define (utf-16-codec)
"UTF-16")
;;;
;;; Transcoding bytevectors
;;;
(define (string->bytevector str transcoder)
"Encode @var{str} using @var{transcoder}, returning a bytevector."
(iconv:string->bytevector
str
(transcoder-codec transcoder)
(case (transcoder-error-handling-mode transcoder)
((raise) 'error)
((replace) 'substitute)
(else (error "unsupported error handling mode"
(transcoder-error-handling-mode transcoder))))))
(define (bytevector->string bv transcoder)
"Decode @var{bv} using @var{transcoder}, returning a string."
(iconv:bytevector->string
bv
(transcoder-codec transcoder)
(case (transcoder-error-handling-mode transcoder)
((raise) 'error)
((replace) 'substitute)
(else (error "unsupported error handling mode"
(transcoder-error-handling-mode transcoder))))))
;;;
;;; Internal helpers
;;;
(define (with-i/o-filename-conditions filename thunk)
(with-throw-handler 'system-error
thunk
(lambda args
(let ((errno (system-error-errno args)))
(let ((construct-condition
(cond ((= errno EACCES)
make-i/o-file-protection-error)
((= errno EEXIST)
make-i/o-file-already-exists-error)
((= errno ENOENT)
make-i/o-file-does-not-exist-error)
((= errno EROFS)
make-i/o-file-is-read-only-error)
(else
make-i/o-filename-error))))
(raise (construct-condition filename)))))))
(define (with-i/o-port-error port make-primary-condition thunk)
(with-throw-handler 'system-error
thunk
(lambda args
(let ((errno (system-error-errno args)))
(if (memv errno (list EIO EFBIG ENOSPC EPIPE))
(raise (condition (make-primary-condition)
(make-i/o-port-error port)))
(apply throw args))))))
(define-syntax with-textual-output-conditions
(syntax-rules ()
((_ port body0 body ...)
(with-i/o-port-error port make-i/o-write-error
(lambda () (with-i/o-encoding-error body0 body ...))))))
(define-syntax with-textual-input-conditions
(syntax-rules ()
((_ port body0 body ...)
(with-i/o-port-error port make-i/o-read-error
(lambda () (with-i/o-decoding-error body0 body ...))))))
;;;
;;; Input and output ports.
;;;
(define (port-transcoder port)
"Return the transcoder object associated with @var{port}, or @code{#f}
if the port has no transcoder."
(and (textual-port? port)
;; All textual ports have transcoders.
(make-transcoder
(port-encoding port)
(native-eol-style)
(case (port-conversion-strategy port)
((error) 'raise)
((substitute) 'replace)
(else
(assertion-violation 'port-transcoder
"unsupported error handling mode"))))))
(define (binary-port? port)
"Return @code{#t} if @var{port} appears to be a binary port, else
return @code{#f}. Note that Guile does not currently distinguish
between binary and textual ports, so this predicate is not a reliable
indicator of whether the port was created as a binary port. Currently,
it returns @code{#t} if and only if the port encoding is ``ISO-8859-1'',
because Guile uses this encoding when creating a binary port."
(equal? (port-encoding port) "ISO-8859-1"))
(define (textual-port? port)
"Return @code{#t} if @var{port} appears to be a textual port, else
return @code{#f}. Note that Guile does not currently distinguish
between binary and textual ports, so this predicate is not a reliable
indicator of whether the port was created as a textual port. Currently,
it always returns @code{#t}, because all ports can be used for textual
I/O in Guile."
#t)
(define (port-eof? port)
(eof-object? (if (binary-port? port)
(lookahead-u8 port)
(lookahead-char port))))
(define (transcoded-port port transcoder)
"Return a new textual port based on @var{port}, using
@var{transcoder} to encode and decode data written to or
read from its underlying binary port @var{port}."
;; Hackily get at %make-transcoded-port.
(let ((result ((@@ (ice-9 binary-ports) %make-transcoded-port) port)))
(set-port-encoding! result (transcoder-codec transcoder))
(case (transcoder-error-handling-mode transcoder)
((raise)
(set-port-conversion-strategy! result 'error))
((replace)
(set-port-conversion-strategy! result 'substitute))
(else
(error "unsupported error handling mode"
(transcoder-error-handling-mode transcoder))))
result))
(define (port-position port)
"Return the offset (an integer) indicating where the next octet will be
read from/written to in @var{port}."
;; FIXME: We should raise an `&assertion' error when not supported.
(seek port 0 SEEK_CUR))
(define (set-port-position! port offset)
"Set the position where the next octet will be read from/written to
@var{port}."
;; FIXME: We should raise an `&assertion' error when not supported.
(seek port offset SEEK_SET))
(define (port-has-port-position? port)
"Return @code{#t} is @var{port} supports @code{port-position}."
(and (false-if-exception (port-position port)) #t))
(define (port-has-set-port-position!? port)
"Return @code{#t} is @var{port} supports @code{set-port-position!}."
(and (false-if-exception (set-port-position! port (port-position port)))
#t))
(define* (call-with-bytevector-output-port proc #:optional (transcoder #f))
(receive (port extract) (open-bytevector-output-port transcoder)
(call-with-port port proc)
(extract)))
(define (open-string-input-port str)
"Open an input port that will read from @var{str}."
(open-input-string str))
(define (r6rs-open filename mode buffer-mode transcoder)
(let ((port (with-i/o-filename-conditions filename
(lambda ()
(with-fluids ((%default-port-encoding #f))
(open filename mode))))))
(setvbuf port buffer-mode)
(when transcoder
(set-port-encoding! port (transcoder-codec transcoder)))
port))
(define (file-options->mode file-options base-mode)
(logior base-mode
(if (enum-set-member? 'no-create file-options)
0
O_CREAT)
(if (enum-set-member? 'no-truncate file-options)
0
O_TRUNC)
(if (enum-set-member? 'no-fail file-options)
0
O_EXCL)))
(define* (open-file-input-port filename
#:optional
(file-options (file-options))
(buffer-mode (buffer-mode block))
transcoder)
"Return an input port for reading from @var{filename}."
(r6rs-open filename O_RDONLY buffer-mode transcoder))
(define* (open-file-input/output-port filename
#:optional
(file-options (file-options))
(buffer-mode (buffer-mode block))
transcoder)
"Return a port for reading from and writing to @var{filename}."
(r6rs-open filename
(file-options->mode file-options O_RDWR)
buffer-mode
transcoder))
(define (open-string-output-port)
"Return two values: an output port that will collect characters written to it
as a string, and a thunk to retrieve the characters associated with that port."
(let ((port (open-output-string)))
(values port
(lambda ()
(let ((s (get-output-string port)))
(seek port 0 SEEK_SET)
(truncate-file port 0)
s)))))
(define* (open-file-output-port filename
#:optional
(file-options (file-options))
(buffer-mode (buffer-mode block))
maybe-transcoder)
"Return an output port for writing to @var{filename}."
(r6rs-open filename
(file-options->mode file-options O_WRONLY)
buffer-mode
maybe-transcoder))
(define (call-with-string-output-port proc)
"Call @var{proc}, passing it a string output port. When @var{proc} returns,
return the characters accumulated in that port."
(let ((port (open-output-string)))
(proc port)
(get-output-string port)))
(define (output-port-buffer-mode port)
"Return @code{none} if @var{port} is unbuffered, @code{line} if it is
line buffered, or @code{block} otherwise."
(let ((buffering (bytevector-length
(port-buffer-bytevector (port-write-buffer port)))))
(cond
((= buffering 1) 'none)
((port-line-buffered? port) 'line)
(else 'block))))
(define (flush-output-port port)
(force-output port))
;;;
;;; Textual output.
;;;
(define-condition-type &i/o-encoding &i/o-port
make-i/o-encoding-error i/o-encoding-error?
(char i/o-encoding-error-char))
(define-syntax with-i/o-encoding-error
(syntax-rules ()
"Convert Guile throws to `encoding-error' to `&i/o-encoding'."
((_ body ...)
;; XXX: This is heavyweight for small functions like `put-char'.
(with-throw-handler 'encoding-error
(lambda ()
(begin body ...))
(lambda (key subr message errno port chr)
(raise (make-i/o-encoding-error port chr)))))))
(define (put-char port char)
(with-textual-output-conditions port (write-char char port)))
(define (put-datum port datum)
(with-textual-output-conditions port (write datum port)))
(define* (put-string port s #:optional start count)
(with-textual-output-conditions port
(cond ((not (string? s))
(assertion-violation 'put-string "expected string" s))
((and start count)
(display (substring/shared s start (+ start count)) port))
(start
(display (substring/shared s start (string-length s)) port))
(else
(display s port)))))
;; Defined here to be able to make use of `with-i/o-encoding-error', but
;; not exported from here, but from `(rnrs io simple)'.
(define* (display object #:optional (port (current-output-port)))
(with-textual-output-conditions port (guile:display object port)))
;;;
;;; Textual input.
;;;
(define-condition-type &i/o-decoding &i/o-port
make-i/o-decoding-error i/o-decoding-error?)
(define-syntax with-i/o-decoding-error
(syntax-rules ()
"Convert Guile throws to `decoding-error' to `&i/o-decoding'."
((_ body ...)
;; XXX: This is heavyweight for small functions like `get-char' and
;; `lookahead-char'.
(with-throw-handler 'decoding-error
(lambda ()
(begin body ...))
(lambda (key subr message errno port)
(raise (make-i/o-decoding-error port)))))))
(define (get-char port)
(with-textual-input-conditions port (read-char port)))
(define (get-datum port)
(with-textual-input-conditions port (read port)))
(define (get-line port)
(with-textual-input-conditions port (read-line port 'trim)))
(define (get-string-all port)
(with-textual-input-conditions port (read-string port)))
(define (get-string-n port count)
"Read up to @var{count} characters from @var{port}.
If no characters could be read before encountering the end of file,
return the end-of-file object, otherwise return a string containing
the characters read."
(let* ((s (make-string count))
(rv (get-string-n! port s 0 count)))
(cond ((eof-object? rv) rv)
((= rv count) s)
(else (substring/shared s 0 rv)))))
(define (lookahead-char port)
(with-textual-input-conditions port (peek-char port)))
;;;
;;; Standard ports.
;;;
(define (standard-input-port)
(with-fluids ((%default-port-encoding #f))
(dup->inport 0)))
(define (standard-output-port)
(with-fluids ((%default-port-encoding #f))
(dup->outport 1)))
(define (standard-error-port)
(with-fluids ((%default-port-encoding #f))
(dup->outport 2)))
)
;;; ports.scm ends here