>From 3658e89614cbe3b5b27f09271b7bc738a1c7ec38 Mon Sep 17 00:00:00 2001 From: "F. Jason Park" Date: Mon, 7 Nov 2022 05:13:59 -0800 Subject: [PATCH 0/6] *** NOT A PATCH *** *** BLURB HERE *** F. Jason Park (6): Teach thing-at-point to recognize bracketed IPv6 URLs Accommodate ircs:// URLs in url-irc and browse-url Refactor erc-select-read-args Default to TLS port when calling erc-tls from lisp Add optional server param to erc-networks--determine Improve new connections in erc-handle-irc-url doc/misc/erc.texi | 39 +++++ lisp/erc/erc-backend.el | 6 + lisp/erc/erc-compat.el | 15 ++ lisp/erc/erc-networks.el | 9 +- lisp/erc/erc.el | 224 +++++++++++++++++++-------- lisp/net/browse-url.el | 11 ++ lisp/thingatpt.el | 2 +- lisp/url/url-irc.el | 21 ++- test/lisp/erc/erc-networks-tests.el | 17 +++ test/lisp/erc/erc-tests.el | 225 ++++++++++++++++++++++++++++ test/lisp/net/browse-url-tests.el | 9 ++ test/lisp/thingatpt-tests.el | 3 + 12 files changed, 510 insertions(+), 71 deletions(-) Interdiff: diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi index 3db83197f9..d01eab1bbb 100644 --- a/doc/misc/erc.texi +++ b/doc/misc/erc.texi @@ -79,6 +79,7 @@ Top * Connecting:: Ways of connecting to an IRC server. * Sample Configuration:: An example configuration file. +* Integrations:: Integrations available for ERC. * Options:: Options that are available for ERC. @end detailmenu @@ -526,6 +527,7 @@ Advanced Usage @menu * Connecting:: Ways of connecting to an IRC server. * Sample Configuration:: An example configuration file. +* Integrations:: Integrations available for ERC. * Options:: Options that are available for ERC. @end menu @@ -990,6 +992,43 @@ Sample Configuration ;; (setq erc-kill-server-buffer-on-quit t) @end lisp +@node Integrations +@section Integrations +@cindex integrations + +@subheading URL +For anything to work, you'll want to set @code{url-irc-function} to +@code{url-irc-erc}. As a rule of thumb, libraries that rely directly +on @code{url-retrieve} should be good to go out the box from Emacs +29.1 onward. On older versions of Emacs, you may need to +@code{(require 'erc)} beforehand. @pxref{Retrieving URLs,,, url, URL}. + +For other apps and libraries, such as those relying on the +higher-level @code{browse-url}, you'll oftentimes be asked to specify +a pattern, sometimes paired with a function that accepts a string URL +as a first argument. For example, with EWW, you may need to tack +something like @code{"\\|\\`irc6?s?:"} onto the end of +@code{eww-use-browse-url}. But with @code{gnus-button-alist}, you'll +need a function as well: + +@lisp + '("\\birc6?s?://[][a-z0-9.,@@_:+%?&/#-]+" + 0 t erc-browse-url-handler 0) +@end lisp + +@defun erc-browse-url-handler url &rest args +An autoloaded convenience function for use in options like those +mentioned above. @var{url} must be a string. In Emacs 29 and above, +the function @code{browse-url-irc} can be used instead. +@end defun + +@noindent +Keep in mind that when fiddling with these options, it may be easier +(and more polite) to connect to a local server or a test network, like +@samp{ircs://testnet.ergo.chat/#test}, since these generally don't +require authentication. + + @node Options @section Options @cindex options diff --git a/lisp/erc/erc-compat.el b/lisp/erc/erc-compat.el index 03bd8f1352..683d19dfc7 100644 --- a/lisp/erc/erc-compat.el +++ b/lisp/erc/erc-compat.el @@ -168,6 +168,21 @@ erc-compat--with-memoization `(cl--generic-with-memoization ,table ,@forms)) (t `(progn ,@forms)))) +(declare-function browse-url-irc "browse-url" (url &rest _)) + +(defun erc-compat--browse-url-irc (string &rest _) + "Parse STRING and call `url-irc'." + (require 'url-irc) + (if (< emacs-major-version 29) + ;; `url-irc' binds this in Emacs 29+. + (let ((url-current-object (url-generic-parse-url string))) + (url-irc url-current-object)) + (browse-url-irc string))) + +(when (< emacs-major-version 29) + (add-to-list 'browse-url-default-handlers + '("\\`irc6?s?://" . erc-compat--browse-url-irc))) + (provide 'erc-compat) ;;; erc-compat.el ends here diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index db2029580a..3c9293e28a 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -1534,6 +1534,15 @@ erc-reuse-buffers (make-obsolete-variable 'erc-reuse-buffers "old behavior when t now permanent" "29.1") +(defcustom erc-legacy-port-names 'legacy + "Interpret \"irc\" and \"ircs\" using IANA service mappings. +When non-nil, this yields 194 and 994 instead of 6667 and 6697. +When set to `legacy', it also emits a warning saying that the +default will change to nil in the future." + :group 'erc + :package-version '(ERC . "5.4.1") ; FIXME increment on ELPA release + :type '(choice (const nil) (const legacy) (const t))) + (defun erc-normalize-port (port) "Normalize the port specification PORT to integer form. PORT may be an integer, a string or a symbol. If it is a string or a @@ -1557,8 +1566,14 @@ erc-normalize-port port-nr) ((string-equal port "ircu") 6667) ((string-equal port "ircs-u") 6697) - ((string-equal port "irc") - 194) + ((member port '("irc" "ircs")) + (when (eq erc-legacy-port-names 'legacy) + (lwarn 'ERC 'warning + (concat "`erc-legacy-port-names' will default to nil " + "in a future version of ERC."))) + (if (string= port "irc") + (if erc-legacy-port-names 194 6667) + (if erc-legacy-port-names 994 6697))) ((string-equal port "ircs") 994) ((string-equal port "ircd") @@ -2119,29 +2134,33 @@ erc--ensure-url (defun erc-select-read-args () "Prompt the user for values of nick, server, port, and password." (require 'url-parse) - (let ((input (read-string "IRC server: " - (erc-compute-server) - 'erc-server-history-list)) - server port nick passwd) - ;; For legacy reasons, also accept a URL without a scheme. - (let* ((url (url-generic-parse-url (erc--ensure-url input))) - (sp (and (string-suffix-p "s" (url-type url)) 'ircs-u))) - (setq server (url-host url) - port (or (url-portspec url) - (erc-string-to-port - (read-string "IRC port: " (erc-port-to-string - (erc-compute-port sp))))) - nick (or (url-user url) - (if (erc-already-logged-in server port nick) - (read-string (erc-format-message 'nick-in-use ?n nick) - nick 'erc-nick-history-list) - (read-string "Nickname: " (erc-compute-nick nick) - 'erc-nick-history-list))) - passwd (or (url-password url) - (if erc-prompt-for-password - (read-passwd "Server password: ") - (with-suppressed-warnings ((obsolete erc-password)) - erc-password))))) + (let* ((input (let ((d (erc-compute-server))) + (read-string (format "Server (default is %S): " d) + nil 'erc-server-history-list d))) + ;; For legacy reasons, also accept a URL without a scheme. + (url (url-generic-parse-url (erc--ensure-url input))) + (server (url-host url)) + (sp (and (or (string-suffix-p "s" (url-type url)) + (and (equal server erc-default-server) + (not (string-prefix-p "irc://" input)))) + 'ircs-u)) + (port (or (url-portspec url) + (erc-compute-port + (let ((d (erc-compute-port sp))) ; may be a string + (read-string (format "Port (default is %s): " d) + nil nil d))))) + ;; Trust the user not to connect twice accidentally. We + ;; can't use `erc-already-logged-in' to check for an existing + ;; connection without modifying it to consider USER and PASS. + (nick (or (url-user url) + (let ((d (erc-compute-nick))) + (read-string (format "Nickname (default is %S): " d) + nil 'erc-nick-history-list d)))) + (passwd (or (url-password url) + (if erc-prompt-for-password + (read-passwd "Server password (optional): ") + (with-suppressed-warnings ((obsolete erc-password)) + erc-password))))) (when (and passwd (string= "" passwd)) (setq passwd nil)) (list :server server :port port :nick nick :password passwd))) @@ -6395,10 +6414,7 @@ erc-compute-port - PORT (the argument passed to this function) - The `erc-port' option - The `erc-default-port' variable" - (cond ((numberp port) port) - (erc-port (erc-normalize-port erc-port)) - (port (erc-normalize-port port)) - (t erc-default-port))) + (erc-normalize-port (or port erc-port erc-default-port))) ;; time routines @@ -7168,21 +7184,47 @@ erc-get-parsed-vector-type ;; Teach url.el how to open irc:// URLs with ERC. ;; To activate, customize `url-irc-function' to `url-irc-erc'. -;; FIXME update comment above once the URL business is fully settled. -;; Also: the function `url-retrieve-internal' finds a "loader" by -;; looking for a library providing a feature named "url-", but -;; no such file currently exists for "ircs". +(defcustom erc-url-connect-function nil + "When non-nil, a function used to connect to an IRC URL. +Called with any number of keyword arguments recognized by `erc' +and `erc-tls'. The variable `url-current-object', if non-nil, +can be used to help determine whether to connect using TLS." + :group 'erc + :package-version '(ERC . "5.4.1") ; FIXME increment on release + :type '(choice (const nil) function)) + +(defun erc--url-default-connect-function (&rest plist) + (let* ((scheme (and url-current-object (url-type url-current-object))) + (ircsp (if scheme + (string-suffix-p "s" scheme) + (or (eql 6697 (plist-get plist :port)) + (yes-or-no-p "Connect using TLS? ")))) + (erc-server (plist-get plist :server)) + (erc-port (or (plist-get plist :port) + (and ircsp (erc-normalize-port 'ircs-u)) + erc-port)) + (erc-nick (or (plist-get plist :nick) erc-nick)) + (erc-password (plist-get plist :password)) + (args (erc-select-read-args))) + (unless ircsp + (setq ircsp (eql 6697 erc-port))) + (apply (if ircsp #'erc-tls #'erc) args))) + +;; The current spec, unlike the 2003 Butcher draft, doesn't explicitly +;; allow for an auth[:password]@ component (or trailing ,flags or +;; &options). +;; +;; https://www.iana.org/assignments/uri-schemes +;; https://datatracker.ietf.org/doc/html/draft-butcher-irc-url#section-6 ;;;###autoload -(defun erc-handle-irc-url (host port channel nick password - &optional connect-fn) +(defun erc-handle-irc-url (host port channel nick password) "Use ERC to IRC on HOST:PORT in CHANNEL. If ERC is already connected to HOST:PORT, simply /join CHANNEL. Otherwise, connect to HOST:PORT as NICK and /join CHANNEL. -Note that ERC no longer attempts to establish new connections -without human intervention, although opting in may eventually be -allowed." +Beginning with ERC 5.5, new connections require human intervention. +Customize `erc-url-connect-function' to override this." (when (eql port 0) (setq port nil)) (let* ((net (erc-networks--determine host)) (server-buffer @@ -7202,10 +7244,10 @@ erc-handle-irc-url port))))))))) key deferred) (unless server-buffer - (unless connect-fn - (user-error "Existing session for %s not found." host)) (setq deferred t - server-buffer (apply connect-fn :server host + server-buffer (apply (or erc-url-connect-function + #'erc--url-default-connect-function) + :server host `(,@(and port (list :port port)) ,@(and nick (list :nick nick)) ,@(and password `(:password ,password)))))) @@ -7227,75 +7269,13 @@ erc-handle-irc-url (with-current-buffer server-buffer (erc-cmd-JOIN channel key)))))) -;; XXX ERASE ME (possibly use as basis for new section in info doc) -;; -;; For now, as a demo, users must require erc and do something like: -;; -;; (add-to-list 'browse-url-default-handlers -;; '("\\`irc6?s?://" . erc--handle-ircs-url)) -;; -;; Libraries that optionally depend on browse-url, like eww, etc. need -;; an extra hand as well: -;; -;; (setq eww-use-browse-url -;; (concat eww-use-browse-url "\\|\\`irc6?s?:")) -;; -;; Those that don't use browse-url get the same handler: -;; -;; (add-to-list 'gnus-button-alist -;; '("\\birc6?s?://[][a-z0-9.,@_:+%?&/#-]+" -;; 0 t erc--handle-ircs-url 0)) -;; -;; Finally, insert something like "ircs://testnet.ergo.chat/#test" -;; where appropriate and perform a suitable action. - -;; The two variables below are contenders for exporting as user -;; options. The rationale for separate functions here instead of, -;; say, a single option granting ERC permission to connect -;; automatically is that ERC lacks a concept of configured server -;; profiles and thus has no idea what values to give for connection -;; parameters, like nick, user, etc. -;; -;; Also, the current spec was simplified from the 2003 Butcher draft -;; and doesn't explicitly allow for an auth[:password]@ component (or -;; trailing ,flags or &options, for that matter). Regardless, even -;; when provided, we shouldn't just connect and risk exposing -;; whatever's returned by `user-login-name', right? -;; -;; https://www.iana.org/assignments/uri-schemes -;; https://datatracker.ietf.org/doc/html/draft-butcher-irc-url#section-6 - -(defvar erc--url-irc-connect-function nil) -(defvar erc--url-ircs-connect-function nil) - -(defun erc--url-default-connect-function (ircs &rest plist) - (let ((erc-server (plist-get plist :server)) - (erc-port (or (plist-get plist :port) - (and ircs (erc-normalize-port 'ircs-u)) - erc-port)) - (erc-nick (or (plist-get plist :nick) erc-nick))) - (call-interactively (if ircs #'erc-tls #'erc)))) - (defvar url-irc-function) -;; FIXME rename this and autoload it -(defun erc--handle-ircs-url (&optional url &rest _) - (unless url - (setq url (pop command-line-args-left)) - (cl-assert url)) - (require 'url-parse) - (unless (url-p url) - (setq url (url-generic-parse-url url))) - (let* ((ircsp (string-suffix-p "s" (url-type url))) - (fn (or (if ircsp - erc--url-ircs-connect-function - erc--url-irc-connect-function) - (apply-partially #'erc--url-default-connect-function ircsp))) - (url-irc-function (lambda (&rest r) - (apply #'erc-handle-irc-url `(,@r ,fn))))) - ;; Amazingly, this does TRT for /&chan, /#chan, /##chan, /#&chan - (url-irc url))) - +;;;###autoload +(defun erc-browse-url-handler (url &rest _) + "Launch an ERC session when given an irc:// URL." + (let ((url-irc-function 'url-irc-erc)) + (erc-compat--browse-url-irc url))) (provide 'erc) diff --git a/lisp/net/browse-url.el b/lisp/net/browse-url.el index 1597f3651a..8d95c0667b 100644 --- a/lisp/net/browse-url.el +++ b/lisp/net/browse-url.el @@ -565,6 +565,7 @@ browse-url--non-html-file-url-p (defvar browse-url-default-handlers '(("\\`mailto:" . browse-url--mailto) ("\\`man:" . browse-url--man) + ("\\`irc6?s?://" . browse-url-irc) (browse-url--non-html-file-url-p . browse-url-emacs)) "Like `browse-url-handlers' but populated by Emacs and packages. @@ -1510,6 +1511,16 @@ browse-url-text-emacs (function-put 'browse-url-text-emacs 'browse-url-browser-kind 'internal) +;; --- irc --- + +;;;###autoload +(defun browse-url-irc (url &rest _) + "Call `url-irc' directly after parsing URL. +This function is a fit for options like `gnus-button-alist'." + (url-irc (url-generic-parse-url url))) + +(function-put 'browse-url-irc 'browse-url-browser-kind 'internal) + ;; --- mailto --- (autoload 'rfc6068-parse-mailto-url "rfc6068") diff --git a/lisp/url/url-irc.el b/lisp/url/url-irc.el index 9161f7d13e..0dd25b7f49 100644 --- a/lisp/url/url-irc.el +++ b/lisp/url/url-irc.el @@ -38,7 +38,12 @@ url-irc-function PORT - the port number of the IRC server to contact CHANNEL - What channel on the server to visit right away (can be nil) USER - What username to use -PASSWORD - What password to use" +PASSWORD - What password to use. + +The variable `url-current-object' is bound to the parsed `url' +struct, but its members may not match the positional args above, +which should take precedence. For example, `:portspec' may be +nil while PORT is 6667." :type '(choice (const :tag "rcirc" :value url-irc-rcirc) (const :tag "ERC" :value url-irc-erc) (const :tag "ZEN IRC" :value url-irc-zenirc) @@ -80,7 +85,8 @@ url-irc (port (url-port url)) (pass (url-password url)) (user (url-user url)) - (chan (url-filename url))) + (chan (url-filename url)) + (url-current-object url)) (if (url-target url) (setq chan (concat chan "#" (url-target url)))) (if (string-match "^/" chan) @@ -90,6 +96,17 @@ url-irc (funcall url-irc-function host port chan user pass) nil)) +;;;; ircs:// + +;; The function `url-scheme-get-property' tries and fails to load the +;; nonexistent url-ircs.el but falls back to using the following: + +;;;###autoload +(defconst url-ircs-default-port 6697 "Default port for IRCS connections.") + +;;;###autoload +(defalias 'url-ircs 'url-irc) + (provide 'url-irc) ;;; url-irc.el ends here diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el index 3ca36c0abb..e097090e5d 100644 --- a/test/lisp/erc/erc-tests.el +++ b/test/lisp/erc/erc-tests.el @@ -976,26 +976,25 @@ erc--server-connect-dumb-ipv6-regexp (ert-deftest erc-select-read-args () - (ert-info ("Default") + (ert-info ("Defaults to TLS") (should (equal (ert-simulate-keys "\r\r\r\r" (erc-select-read-args)) (list :server "irc.libera.chat" - :port 6667 + :port 6697 :nick (user-login-name) :password nil)))) - (ert-info ("Default TSL") - (should (equal (ert-simulate-keys "\r\r\r\r" - (let ((erc-default-port erc-default-port-tls)) - (erc-select-read-args))) + (ert-info ("Override default TLS") + (should (equal (ert-simulate-keys "irc://irc.libera.chat\r\r\r\r" + (erc-select-read-args)) (list :server "irc.libera.chat" - :port 6697 + :port 6667 :nick (user-login-name) :password nil)))) (ert-info ("Address includes port") (should (equal (ert-simulate-keys - "\C-a\C-klocalhost:6667\r\C-a\C-knick\r\r" + "localhost:6667\rnick\r\r" (erc-select-read-args)) (list :server "localhost" :port 6667 @@ -1003,7 +1002,7 @@ erc-select-read-args :password nil)))) (ert-info ("Address includes nick, password skipped via option") - (should (equal (ert-simulate-keys "\C-a\C-knick@localhost:6667\r" + (should (equal (ert-simulate-keys "nick@localhost:6667\r" (let (erc-prompt-for-password) (erc-select-read-args))) (list :server "localhost" @@ -1012,7 +1011,7 @@ erc-select-read-args :password nil)))) (ert-info ("Addresss includes nick and password") - (should (equal (ert-simulate-keys "\C-a\C-knick:sesame@localhost:6667\r" + (should (equal (ert-simulate-keys "nick:sesame@localhost:6667\r" (erc-select-read-args)) (list :server "localhost" :port 6667 @@ -1020,7 +1019,7 @@ erc-select-read-args :password "sesame")))) (ert-info ("IPv6 address plain") - (should (equal (ert-simulate-keys "\C-a\C-k::1\r\r\r\r" + (should (equal (ert-simulate-keys "::1\r\r\r\r" (erc-select-read-args)) (list :server "[::1]" :port 6667 @@ -1028,7 +1027,7 @@ erc-select-read-args :password nil)))) (ert-info ("IPv6 address with port") - (should (equal (ert-simulate-keys "\C-a\C-k[::1]:6667\r\r\r" + (should (equal (ert-simulate-keys "[::1]:6667\r\r\r" (erc-select-read-args)) (list :server "[::1]" :port 6667 @@ -1036,7 +1035,7 @@ erc-select-read-args :password nil)))) (ert-info ("IPv6 address includes nick") - (should (equal (ert-simulate-keys "\C-a\C-knick@[::1]:6667\r\r" + (should (equal (ert-simulate-keys "nick@[::1]:6667\r\r" (erc-select-read-args)) (list :server "[::1]" :port 6667 @@ -1109,14 +1108,14 @@ erc-tests--make-client-buf (current-buffer))) (ert-deftest erc-handle-irc-url () - (should-error (erc-handle-irc-url "irc.gnu.org" 6667 nil nil nil)) (let* (calls rvbuf erc-networks-alist erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook - (connect (lambda (&rest r) - (push r calls) - (if (functionp rvbuf) (funcall rvbuf) rvbuf)))) + (erc-url-connect-function + (lambda (&rest r) + (push r calls) + (if (functionp rvbuf) (funcall rvbuf) rvbuf)))) (cl-letf (((symbol-function 'erc-cmd-JOIN) (lambda (&rest r) (push r calls)))) @@ -1127,34 +1126,34 @@ erc-handle-irc-url (erc-tests--make-server-buf "baznet") (ert-info ("Unknown network") - (erc-handle-irc-url "irc.foonet.org" 6667 "#chan" nil nil connect) + (erc-handle-irc-url "irc.foonet.org" 6667 "#chan" nil nil) (should (equal '("#chan" nil) (pop calls))) (should-not calls)) (ert-info ("Unknown network, no port") - (erc-handle-irc-url "irc.foonet.org" nil "#chan" nil nil connect) + (erc-handle-irc-url "irc.foonet.org" nil "#chan" nil nil) (should (equal '("#chan" nil) (pop calls))) (should-not calls)) (ert-info ("Known network, no port") (setq erc-networks-alist '((foonet "irc.foonet.org"))) - (erc-handle-irc-url "irc.foonet.org" nil "#chan" nil nil connect) + (erc-handle-irc-url "irc.foonet.org" nil "#chan" nil nil) (should (equal '("#chan" nil) (pop calls))) (should-not calls)) (ert-info ("Known network, different port") - (erc-handle-irc-url "irc.foonet.org" 6697 "#chan" nil nil connect) + (erc-handle-irc-url "irc.foonet.org" 6697 "#chan" nil nil) (should (equal '("#chan" nil) (pop calls))) (should-not calls)) (ert-info ("Known network, existing chan with key") (erc-tests--make-client-buf "foonet" "#chan") - (erc-handle-irc-url "irc.foonet.org" nil "#chan?sec" nil nil connect) + (erc-handle-irc-url "irc.foonet.org" nil "#chan?sec" nil nil) (should (equal '("#chan" "sec") (pop calls))) (should-not calls)) (ert-info ("Unknown network, connect, no chan") - (erc-handle-irc-url "irc.gnu.org" nil nil nil nil connect) + (erc-handle-irc-url "irc.gnu.org" nil nil nil nil) (should (equal '(:server "irc.gnu.org") (pop calls))) (should-not calls)) @@ -1162,7 +1161,7 @@ erc-handle-irc-url (with-current-buffer "foonet" (should-not (local-variable-p 'erc-after-connect))) (setq rvbuf (lambda () (erc-tests--make-server-buf "gnu"))) - (erc-handle-irc-url "irc.gnu.org" nil "#spam" nil nil connect) + (erc-handle-irc-url "irc.gnu.org" nil "#spam" nil nil) (should (equal '(:server "irc.gnu.org") (pop calls))) (should-not calls) (with-current-buffer "gnu" diff --git a/test/lisp/net/browse-url-tests.el b/test/lisp/net/browse-url-tests.el index 1c993958b8..cf917802e0 100644 --- a/test/lisp/net/browse-url-tests.el +++ b/test/lisp/net/browse-url-tests.el @@ -56,6 +56,15 @@ browse-url-tests-select-handler-man 'browse-url--man)) (should-not (browse-url-select-handler "man:ls" 'external))) +(ert-deftest browse-url-tests-select-handler-irc () + (should (eq (browse-url-select-handler "irc://localhost" 'internal) + 'browse-url-irc)) + (should-not (browse-url-select-handler "irc://localhost" 'external)) + (should (eq (browse-url-select-handler "irc6://localhost") + 'browse-url-irc)) + (should (eq (browse-url-select-handler "ircs://tester@irc.gnu.org/#chan") + 'browse-url-irc))) + (ert-deftest browse-url-tests-select-handler-file () (should (eq (browse-url-select-handler "file://foo.txt") 'browse-url-emacs)) -- 2.38.1