From: Sergey Matveev <stargrave@stargrave.org>
Date: Wed, 15 Feb 2023 10:31:11 +0000 (+0300)
Subject: Netstrings are simpler than bencode
X-Git-Tag: v2.0.0
X-Git-Url: http://www.git.stargrave.org/?a=commitdiff_plain;h=39898ed396c86fe906bbcdf54c0f913ea44e44dc;p=paster.git

Netstrings are simpler than bencode
---

diff --git a/contrib/clean.sh b/contrib/clean.sh
index a9148a9..452c040 100755
--- a/contrib/clean.sh
+++ b/contrib/clean.sh
@@ -1,6 +1,6 @@
-#!/bin/sh
+#!/bin/sh -e
 
-find . -type f -mtime +1 \
+exec find . -type f -mtime +1 \
     -and -not -name index.html \
     -and -not -name "asciinema-player-*" \
     -delete
diff --git a/contrib/paster-tls-gnutls b/contrib/paster-tls-gnutls.sh
similarity index 85%
rename from contrib/paster-tls-gnutls
rename to contrib/paster-tls-gnutls.sh
index e091bfd..b916998 100755
--- a/contrib/paster-tls-gnutls
+++ b/contrib/paster-tls-gnutls.sh
@@ -1,2 +1,2 @@
-#!/bin/sh
+#!/bin/sh -e
 DST="gnutls-cli --logfile=/dev/null -p 2021 paster.example.com" paster $@
diff --git a/contrib/paster-tls-ucspi b/contrib/paster-tls-ucspi.sh
similarity index 90%
rename from contrib/paster-tls-ucspi
rename to contrib/paster-tls-ucspi.sh
index 0ee162a..fdf7a80 100755
--- a/contrib/paster-tls-ucspi
+++ b/contrib/paster-tls-ucspi.sh
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/sh -e
 HOST=paster.example.com
 DST=cat paster $@ |
 tcpclient -DHR -l 0 $HOST 2021 tlsc -name $HOST sh -c "cat >&7 ; cat <&6"
diff --git a/contrib/paster-ucspi b/contrib/paster-ucspi.sh
similarity index 87%
rename from contrib/paster-ucspi
rename to contrib/paster-ucspi.sh
index 8ab1070..cd5d12d 100755
--- a/contrib/paster-ucspi
+++ b/contrib/paster-ucspi.sh
@@ -1,3 +1,3 @@
-#!/bin/sh
+#!/bin/sh -e
 DST=cat paster $@ |
 tcpclient -DHR -l 0 paster.example.com 2020 sh -c "cat >&7 ; cat <&6"
diff --git a/contrib/paster b/contrib/paster.sh
similarity index 51%
rename from contrib/paster
rename to contrib/paster.sh
index 64f8c79..bac1926 100755
--- a/contrib/paster
+++ b/contrib/paster.sh
@@ -1,15 +1,16 @@
 #!/bin/sh -e
 DST=${DST:-nc paster.example.com 2020}
+ext="0:,"
 if [ -s "$1" ]; then
     src="$1"
     bn="${1##*/}"
     _ext="${bn##*.}"
-    [ "$bn" = "$_ext" ] || ext="1:e${#_ext}:$_ext"
+    [ "$bn" = "$_ext" ] || ext="${#_ext}:$_ext,"
 else
     src=`mktemp`
     trap "rm -f $src" HUP PIPE INT QUIT TERM EXIT
     cat > $src
-    [ $# -eq 0 ] || ext="1:e${#1}:$1"
+    [ $# -eq 0 ] || ext="${#1}:$1,"
 fi
-size=`perl -e 'print -s $ARGV[0]' $src`
-( echo -n "d${ext}1:v${size}:" ; cat $src ; echo -n e ) | $DST
+size=$(perl -e 'print -s $ARGV[0]' "$src")
+( echo -n "${ext}${size}:" ; cat "$src" ; echo -n , ) | $DST
diff --git a/contrib/paster.tcl b/contrib/paster.tcl
index 7ef8f0b..ad7c4ef 100755
--- a/contrib/paster.tcl
+++ b/contrib/paster.tcl
@@ -4,10 +4,11 @@ set Host paster.example.com
 set Port 2021
 set GnuTLS 1
 
+set ext ""
 set fn [lindex $argv 0]
 set size 0
 if {($argc > 0) && [file exists $fn]} {
-    set ext [file extension $fn]
+    set ext [string trimleft [file extension $fn] .]
     set fd [open $fn {RDONLY BINARY}]
     set size [file size $fn]
 } else {
@@ -23,19 +24,16 @@ if $GnuTLS {
     set sock [socket $Host $Port]
     chan configure $sock -encoding binary -translation binary
 }
-puts -nonewline $sock "d"
-if {[info exists ext]} {
-    set ext [string trimleft $ext .]
-    puts -nonewline $sock [string cat "1:e" [string length $ext] ":$ext"]
-}
-puts -nonewline $sock "1:v$size:"
+
+puts -nonewline $sock [string cat [string length $ext] ":$ext,"]
+puts -nonewline $sock "$size:"
 if {[info exists fd]} {
     fcopy $fd $sock
     close $fd
 } {
     puts -nonewline $sock $data
 }
-puts -nonewline $sock "e"
+puts -nonewline $sock ","
 flush $sock
 while {[gets $sock line] >= 0} { puts $line }
 close $sock
diff --git a/contrib/paster.zsh b/contrib/paster.zsh
index a4850d5..76f1464 100755
--- a/contrib/paster.zsh
+++ b/contrib/paster.zsh
@@ -3,16 +3,17 @@
 set -e
 
 DST=${DST:-paster.example.com 2020}
+ext="0:,"
 [[ -s "$1" ]] && {
     src="$1"
     bn=$src:t
     e=${bn##*.}
-    [[ $bn = $e ]] || ext="1:e${#e}:$e"
+    [[ $bn = $e ]] || ext="${#e}:$e,"
 } || {
     src=`mktemp`
     trap "rm -f $src" HUP PIPE INT QUIT TERM EXIT
     cat > $src
-    [[ $# -eq 0 ]] || ext="1:e${#1}:$1"
+    [[ $# -eq 0 ]] || ext="${#1}:$1,"
 }
 zmodload -F zsh/stat b:zstat
 size=`zstat +size $src`
@@ -20,6 +21,6 @@ size=`zstat +size $src`
 zmodload zsh/net/tcp
 ztcp ${=DST}
 fd=$REPLY
-( print -n "d${ext}1:v${size}:" ; cat $src ; print -n e ) >&$fd
+( print -n "${ext}${size}:" ; cat $src ; print -n , ) >&$fd
 cat <&$fd
 ztcp -c $fd
diff --git a/doc/features.texi b/doc/features.texi
index 56bcfa6..ffd0539 100644
--- a/doc/features.texi
+++ b/doc/features.texi
@@ -12,8 +12,8 @@
     with varying @code{Content-Types}. You can share images for example,
     not only plaintext
 
-@item No excessive HTTP protocol: just send
-    @url{https://en.wikipedia.org/wiki/Bencode, bencode}-ed dictionary
+@item No excessive HTTP protocol: just send two
+    @url{https://en.wikipedia.org/wiki/Netstring, netstring}-encoded strings
     with the data over the TCP
 
 @item Newline is appended for @file{.txt}/@file{.url} pastes, if it is missing
diff --git a/doc/install.texi b/doc/install.texi
index 8083cb3..8d62e6b 100644
--- a/doc/install.texi
+++ b/doc/install.texi
@@ -7,7 +7,7 @@
 Install paster itself:
 
 @example
-$ go get go.stargrave.org/paster
+$ go get go.stargrave.org/paster/v2
 @end example
 
 If you have got problems with your trust anchors, unwilling to
diff --git a/doc/protocol.texi b/doc/protocol.texi
index 491cc79..e1c0acc 100644
--- a/doc/protocol.texi
+++ b/doc/protocol.texi
@@ -1,16 +1,12 @@
 @node Protocol
 @unnumbered Protocol
 
-Protocol is very simple: @url{https://en.wikipedia.org/wiki/Bencode, bencode}d
-dictionary is sent over TCP.
-
-@itemize
-@item @code{v} key contains the data you want to paste
-@item optional @code{e} key, holding the desired filename extension,
-    without the leading dot, up to 9 characters long
-@end itemize
+Protocol is very simple: two
+@url{https://en.wikipedia.org/wiki/Netstring, netstring}s are sent over TCP.
+First one holds desired filename extension, without the leading dot, up to 9
+characters long. Second one is the paste data itself.
 
 @example
-"hello world" => d1:v11:hello worlde
-"http://example.com/" and "url" extension => d1:e3:url1:v18:http://example.come
+  "hello world", no explicit extension => 0:,11:hello world,
+"http://example.com/", "url" extension => 3:url,18:http://example.com,
 @end example
diff --git a/go.mod b/go.mod
index af8f1e5..4dbc7c1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,5 @@
-module go.stargrave.org/paster
+module go.stargrave.org/paster/v2
 
-go 1.16
+go 1.17
+
+require go.cypherpunks.ru/netstring/v2 v2.4.0
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..5044963
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,2 @@
+go.cypherpunks.ru/netstring/v2 v2.4.0 h1:qBOtHJj1hoCUpYkouuTurXl20R1IKnEkh+q7/J0TgZ4=
+go.cypherpunks.ru/netstring/v2 v2.4.0/go.mod h1:6YDx4gW414SmHdvSBMKbHaB2/7w9WZ04NQb7XIUV/pA=
diff --git a/main.go b/main.go
index 912408b..25e93d4 100644
--- a/main.go
+++ b/main.go
@@ -30,9 +30,12 @@ import (
 	"html/template"
 	"io"
 	"os"
-	"strconv"
+
+	"go.cypherpunks.ru/netstring/v2"
 )
 
+const MaxExtLen = 9
+
 var (
 	//go:embed asciicast.tmpl
 	ASCIICastHTMLTmplRaw string
@@ -74,7 +77,7 @@ func main() {
 	maxSize := flag.Uint64("max-size", 1<<20, "Maximal upload size")
 	asciicastPath := flag.String("asciicast-path", "", "Generate HTMLs for .cast asciicasts, specify \"asciinema-player-v2.6.1\"")
 	flag.Usage = func() {
-		fmt.Fprintf(os.Stderr, "Usage: paster [options] URL [...]\n")
+		fmt.Fprintf(os.Stderr, "Usage: paster [options] URL [URL ...]\n")
 		flag.PrintDefaults()
 	}
 	flag.Parse()
@@ -82,114 +85,81 @@ func main() {
 		flag.Usage()
 		os.Exit(1)
 	}
-	var fn string
-	r := bufio.NewReader(os.Stdin)
-	b, err := r.ReadByte()
+	r := netstring.NewReader(os.Stdin)
+	size, err := r.Next()
 	if err != nil {
 		fatal(err.Error())
 	}
-	if b != 'd' {
-		fatal("bad bencode: no dictionary start")
+	if size > MaxExtLen {
+		fatal("too long extension length")
 	}
-	buf := make([]byte, 21)
-	ext := ".txt"
-	var size uint64
-AnotherKey:
-	if _, err = io.ReadFull(r, buf[:3]); err != nil {
+	data, err := io.ReadAll(r)
+	if err != nil {
 		fatal(err.Error())
 	}
-	switch s := string(buf[:3]); s {
-	case "1:e":
-		if _, err = io.ReadFull(r, buf[:2]); err != nil {
-			fatal(err.Error())
-		}
-		if buf[1] != ':' {
-			fatal(`bad bencode: invalid "e" format`)
-		}
-		extLen, err := strconv.Atoi(string(buf[:1]))
-		if err != nil {
-			fatal(err.Error())
-		}
-		if _, err = io.ReadFull(r, buf[:extLen]); err != nil {
-			fatal(err.Error())
-		}
-		ext = "." + string(buf[:extLen])
-		goto AnotherKey
-	case "1:v":
-		n, err := r.Read(buf)
-		if err != nil {
-			fatal(err.Error())
-		}
-		i := bytes.IndexByte(buf[:n], ':')
-		if i == -1 {
-			fatal(`bad bencode: invalid "v" length`)
-		}
-		size, err = strconv.ParseUint(string(buf[:i]), 10, 64)
-		if err != nil {
-			fatal(err.Error())
-		}
-		if size == 0 {
-			fatal("empty paste")
-		}
-		if size > *maxSize {
-			fatal("too big")
-		}
-		buf = buf[i+1 : n]
-	default:
-		fatal("bad bencode: invalid key")
+	var ext string
+	if len(data) == 0 {
+		ext = ".txt"
+	} else {
+		ext = "." + string(data)
+	}
+	size, err = r.Next()
+	if err != nil {
+		fatal(err.Error())
+	}
+	if size == 0 {
+		fatal("empty paste")
+	}
+	if size > *maxSize {
+		fatal("too big")
 	}
 	rnd := make([]byte, 12)
 	if _, err = io.ReadFull(rand.Reader, rnd); err != nil {
 		fatal(err.Error())
 	}
-	fn = "." + base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(rnd) + ext
+	fn := "." + base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(rnd) + ext
 	fd, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE|os.O_EXCL, os.FileMode(0666))
 	if err != nil {
 		fatal(err.Error())
 	}
 	h := sha512.New()
 	bfd := bufio.NewWriter(fd)
-	mr := io.MultiReader(bytes.NewReader(buf), r)
 	mw := io.MultiWriter(bfd, h)
-	if _, err = io.CopyN(mw, mr, int64(size-1)); err != nil {
+	buf := make([]byte, 1)
+	_, err = io.CopyN(mw, r, int64(size-1))
+	if err != nil {
 		goto Failed
 	}
-	if len(buf) == 0 {
-		buf = append(buf, 0)
-	} else {
-		buf = buf[:1]
-	}
-	if _, err = mr.Read(buf); err != nil {
+	_, err = r.Read(buf)
+	if err != nil {
 		goto Failed
 	}
-	if _, err = mw.Write(buf); err != nil {
+	_, err = mw.Write(buf)
+	if err != nil {
 		goto Failed
 	}
 	if (ext == ".txt" || ext == ".url") && buf[0] != '\n' {
-		if err = bfd.WriteByte('\n'); err != nil {
+		err = bfd.WriteByte('\n')
+		if err != nil {
 			goto Failed
 		}
 	}
-	if _, err = mr.Read(buf); err != nil {
-		goto Failed
-	}
-	if buf[0] != 'e' {
-		os.Remove(fn)
-		fatal("bad bencode: no dictionary end")
-	}
-	if err = bfd.Flush(); err != nil {
+	err = bfd.Flush()
+	if err != nil {
 		goto Failed
 	}
-	if err = fd.Close(); err != nil {
+	err = fd.Close()
+	if err != nil {
 		goto Failed
 	}
-	if err = os.Rename(fn, fn[1:]); err != nil {
+	err = os.Rename(fn, fn[1:])
+	if err != nil {
 		goto Failed
 	}
 	for _, u := range flag.Args() {
 		fmt.Println(u + fn[1:])
 	}
-	fmt.Println("SHA512/2:", hex.EncodeToString(h.Sum(nil)[:512/2/8]))
+	fmt.Println("SHA512/2:", hex.EncodeToString(h.Sum(nil)[:sha512.Size/2]))
 	if ext == ".cast" && *asciicastPath != "" {
 		if err = asciicastHTML(*asciicastPath, fn[1:]); err != nil {
 			goto Failed