Tcl Library Source Code

View Ticket
Login
Ticket UUID: d638a04e5c45fcd217842b755a3fbc7215a58f07
Title: [coroutine::util gets_safety] bugs
Type: Bug Version: 1.3
Submitter: falsifian Created on: 2024-09-12 21:11:58
Subsystem: coroutine Assigned To:
Priority: 5 Medium Severity: Minor
Status: Open Last Modified: 2024-12-02 01:05:06
Resolution: None Closed By: nobody
    Closed on:
Description:
A few problems with `[coroutine::util gets_safety]`. Tested with tcllib 1.3, but I don't see any changes in the source since then.

----

First problem:

I don't think it actually protects against too-long lines.
Here is a demonstration. Alternatively, see the example by LH on the [chan pending wiki page](https://wiki.tcl-lang.org/page/chan+pending) (though I haven't tried LH's example with `coroutine::util::gets_safety`).

1. Create a file with one line that's a million bytes long. For example,

```
echo 'for {set i 0} {$i < 1000000} {incr i} {puts -nonewline A};puts {}' | tclsh8.6 > long_line
```

2. Run the following tcl 8.6 script with that file on standard input:

```
package require coroutine 1.3
proc p {} {
    coroutine::gets_safety stdin 1 line
    puts "It should have failed, but it didn't."
}
coroutine::util create p
```

Output is "It should have failed, but it didn't."

----

Second problem:

It actually needs to be called as `coroutine::util::gets_safety` but the man page implies it's part of the ensemble command. If I trie `coroutine::util gets_safety` I see `unknown or ambiguous subcommand "gets_safety": must be after, await, create, exit, gets, global, puts, read, socket, update, or vwait`.

----

Other, more minor, problems:

- It would be nice to have an `-errorcode` to catch when a line is too long. For example, I'm currently writing an HTTP implementation and would like to send back a 400 Bad Request when the line is too long, instead of just crashing. But I don't want to wrap it in a catch-all `[catch]` since there might be other errors I haven't anticipated that shouldn't result in a 400 Bad Request.

- In the code, the word "During" hangs at the end of a comment. It looks like it was added by accident.

- To me the name `gets_safely` would make more sense, but maybe it's not worth changing at this point.
User Comments: falsifian added on 2024-12-02 01:05:06:

Replying to the 2024-09-13 message from anonymous. Sorry for the slow reply.

That doesn't seem to fix it. Result is the same as in my original report. It looks like [::chan pending input $chan] is returning 0, perhaps because nothing is pending before a read is attempted.

Besides that problem, it looks like that code will attempt to read the whole line into memory, which defeats the purpose, at least for me: I wanted to avoid letting whomever controls the input force my program to allocate an unbounded amount of memory.

Note: there's some discussion at https://wiki.tcl-lang.org/page/gets that might be relevant.


anonymous added on 2024-09-13 06:36:53:
Could the following variant be a possible solution?

---snip---
proc ::coroutine::util::gets_safety {chan limit varname {timeout 120000}} {
    # Process arguments.
    # Acceptable syntax:
    # * gets_safety CHAN LIMIT VARNAME ?TIMEOUT?

    # Loop until we have a complete line. Yield to the event loop
    # where necessary.
    set blocking [::chan configure $chan -blocking]
    upvar 1 $varname line
    try {
        while {1} {
            ::chan configure $chan -blocking 0

            if {[::chan pending input $chan] >= $limit} {
                try {
                    set result [::chan gets $chan line]
                    if {([string length $line] >= $limit) ||
                         [::chan blocked $chan]} {
                        return -code error \
                            {Too many notes, Mozart. Too many notes}
                    }
                } on error {result opts} {
                    return -code $result -options $opts
                }
                return $result
            }

            try {
                set result [::chan gets $chan line]
            } on error {result opts} {
                return -code $result -options $opts
            }

            if {[::chan blocked $chan]} {
                set timeoutevent \
                    [::after $timeout [list [info coroutine] timeout]]
                ::chan event $chan readable [list [info coroutine] readable]
                set event [yield]
                if {$event eq "timeout"} {
                    return -code error {Connection Timed Out}
                }
                ::after cancel $timeoutevent
                ::chan event $chan readable {}
            } else {
                return $result
            }
        }
    } finally {
        ::chan configure $chan -blocking $blocking
    }
}
---snip---

falsifian added on 2024-09-12 21:24:20:
Also:

The documentation says "Of limit is reached before the set first newline, an error is thrown."

But from the implementation, it looks like an error could be thrown even if the line is within the limit, if more data was sent after that first line.

I would prefer an implementation that successfully extracts the first line even when there's a huge amount of data following it (think HTTP request with a big file in the request body, using gets to parse the header lines). But, if that's not practical, the documentation should be corrected to match the implementation.

(Also "Of limit" should be "If limit".)