#How does a reader decide on size of its byte slice? (trying to inject text after EOF)

135 messages ยท Page 1 of 1 (latest)

pine mesa
#

What I'm trying to do is a bit complex and specific maybe but any general info could help.

I want to inject javascript at the end of html files. I'm wrapping the reader used in a http fileserver. Code works but only for some pages seemingly at random. Sometimes it hits EOF and the byte slice is the exact length for appending my javascript, but sometimes len(b)=1 which makes no sense to me.

My reader:

func (i injectedFile) Read(b []byte) (n int, err error) {
    fi, _ := i.Stat()

    n, err = i.file.Read(b)

    if err == io.EOF && path.Ext(fi.Name()) == ".html" {
        for i, v := range JS {
            b[i] = v
        }
        n += len(JS)
    }

    return n, err
}

I've wrapped the file info size to account for increased size:

func (ifi injectedFileInfo) Size() int64 {
    if path.Ext(ifi.Name()) == ".html" {
        return ifi.fileInfo.Size() + int64(len(JS))
    } else {
        return ifi.fileInfo.Size()
    }
}
#

A successfull read data:

file size (with javascript) 1015
javascript size 251
reader byte slice size 512
bytes read 512
error <nil>

file size (with javascript) 1015
javascript size 251
reader byte slice size 503
bytes read 252
error <nil>

file size (with javascript) 1015
javascript size 251
reader byte slice size 251
bytes read 0
error EOF
-->HERE I INJECT JAVASCRIPT AND HAVE PERFECT SIZE TO DO SO

An unsuccessfull one:

file size (with javascript) 762
javascript size 251
reader byte slice size 512
bytes read 511
error <nil>

file size (with javascript) 762
javascript size 251
reader byte slice size 1
bytes read 0
error EOF
-->ERROR HERE BECAUSE b DOES NOT FIT JS
#

So in the first example we see read is called three times and works as expected, but in the second it was called twice but it left out one byte reading the file and didn't provide enough space for the JS.

rustic hearth
#

How does a reader decide on size of its byte slice?
it doesn't, it reads data into the slice it was given, the caller decides what the (maximum) size is

#

if I give you a slice with len() = 50, you can read up to 50 bytes

pine mesa
hollow tree
#

you can inject the response with middleware

#
func incertMiddleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w, r) {
    next.ServeHTTP(w,r)
    if path.Ext(r.URL.Path()) == ".html" {
      w.Write([]byte(JS))
    }
  })
}

fs:=http.FileServer(http.Dir("dirname"))
mux.Handle("/", incertMiddleware(fs))
#

something like that

pine mesa
#

OK cool I'll take a look at that

#

I've spent so much time working on this reader that it would suck to throw it away though but yeah it is what it is, cool if there is a simpler solution ๐Ÿ˜†

rustic hearth
#

what are you actually trying to do?

pine mesa
# rustic hearth what are you actually trying to do?

A normal static local webserver on a directory. But then it injects the html pages with some custom javascript that can refresh pages using websockets that are triggered by file changes using a file watcher. It's for convenient development of static websites so when you update the css for example the browser refreshes automatically to reflect that.

rustic hearth
#

seems like performance/efficiency is not a huge deal, is that correct?

pine mesa
#

I suspect I'm really close to the solution (since it works perfectly for some pages) but could be wrong

rustic hearth
#

then I think the most sensible solution is to implement http.FileSystem.
In your Open method you can read the file into memory, do the replacement, and then serve that. Something along the lines of (writing it on the fly, untested): ```go
type replaceFs struct{}

func (replaceFs) Open(name string) (http.File, error) {
return newReplaceFile(name)
}

type replaceFile struct {
contents *bytes.Reader
*os.File // implements most of the http.File interface, but the file size will be wrong you need to deal with that
}

func (r *replaceFile) Read(d []byte) (int, error) { return r.contents.Read(d) }

func newReplaceFile(name string) (*replaceFile, error) {
f, err := os.Open(name)
if err != nil {
return nil, err
}

data, err := io.ReadAll(f)
if err != nil {
    return nil, err
}

replaced := bytes.Replace(data, needle, replacement, 1)

return &replaceFile{
    File:   f,
    contents: bytes.NewReader(replaced),
}, nil

}

#

that should be about right

pine mesa
#

Thank you I will take a look at this

#

In my solution I am using filesystem but I'm basically overwriting methods (using wrapping of structs) all the way down to fs.FileInfo and file http.File:

type injectedFile struct {
    file http.File
}

func (i injectedFile) Seek(offset int64, whence int) (int64, error) {
    return i.file.Seek(offset, whence)
}

func (i injectedFile) Readdir(count int) ([]fs.FileInfo, error) {
    return i.file.Readdir(count)
}

...

type injectedFileInfo struct {
    fileInfo fs.FileInfo
}

func (ifi injectedFileInfo) Name() string {
    return ifi.fileInfo.Name()
}

func (ifi injectedFileInfo) Size() int64 {
    if path.Ext(ifi.Name()) == ".html" {
        return ifi.fileInfo.Size() + int64(len(JS))
    } else {
        return ifi.fileInfo.Size()
    }
}

...

type injectedFileSystem struct {
    fileSystem http.FileSystem
}

func (i *injectedFileSystem) Open(name string) (http.File, error) {
    f, err := i.fileSystem.Open(name) 
    if err != nil {
        return nil, err
    }
    return injectedFile{file: f}, nil
}
#

Leaving out much code but that's an overview

#

But I'll take a look at your suggesting when I have time

rustic hearth
#

Oh I didn't notice the interface required a Readdir and Stat method, you'll need to add those

#

I've updated the snippet, bonus: it almost compiles now ๐Ÿ™ƒ

pine mesa
# rustic hearth I've updated the snippet, bonus: it almost compiles now ๐Ÿ™ƒ

Thank you for the snippet but I've decided to try my way since I found out I was super close and my way is probably faster. What I've found out is that some pages for some reason is consistently displayed as a blank page and there is an error in the browser: ERR_CONTENT_LENGTH_MISMATCH 200 (OK) which apparently means "a byte difference between the size of the response in the header compared to the actual size of the response document". The funny thing though is that if I right click on the blank page and go to "view page source" the source shows up exactly as intended with the injected script at the bottom! So there might be just some small detail where it skips a byte or something which causes the issue....

My reader hasn't changed much

func (i injectedFile) Read(b []byte) (n int, err error) {
    fi, _ := i.Stat()

    n, err = i.file.Read(b)

    if err == io.EOF && path.Ext(fi.Name()) == ".html" && len(b) == len(JS) {
        for i, v := range JS {
            b[i] = v
        }
        n += len(JS)
    }

    return n, err
}
#

IF ANYONE HAS ANY IDEA WHAT CAUSES THIS LET ME KNOW

hollow tree
#

http file server set the content length with value returned by stat.
i think you need to mock or implement your own http.FS and http.File

pine mesa
#

It's possible it has to do with the fact I only insert the JS when I have a byte slice that's the size of the JS, but without this check it crashes because it doesn't hit EOF correctly, because for some reason it sometimes gives a byte slice that's the length of 1 and it doesn't read this byte... and so there is something fishy there

rustic hearth
rustic hearth
#

yours

#

as I explained previously, the size of b is more or less arbitrary, if it ends up being equal to some value is pure luck

pine mesa
#

Yeah but my setup is correct so I just need to change the reader a little then

rustic hearth
#

it's also slower as you are stating per read

pine mesa
#

Yeah it's a good point with the stat per read

rustic hearth
#

that's the most irrelevant bit mate, your code is simply incorrect

#

if it were faster it would just be wrong faster ๐Ÿ™ƒ

pine mesa
#
type injectedFileInfo struct {
    fileInfo fs.FileInfo
}

func (ifi injectedFileInfo) Name() string {
    return ifi.fileInfo.Name()
}

func (ifi injectedFileInfo) Size() int64 {
    if path.Ext(ifi.Name()) == ".html" {
        return ifi.fileInfo.Size() + int64(len(JS))
    } else {
        return ifi.fileInfo.Size()
    }
}

func (ifi injectedFileInfo) Mode() fs.FileMode {
    return ifi.fileInfo.Mode()
}

func (ifi injectedFileInfo) ModTime() time.Time {
    return ifi.fileInfo.ModTime()
}

func (ifi injectedFileInfo) IsDir() bool {
    return ifi.fileInfo.IsDir()
}

func (ifi injectedFileInfo) Sys() any {
    return ifi.fileInfo.Sys()
}

type injectedFile struct {
    file http.File
}

func (i injectedFile) Seek(offset int64, whence int) (int64, error) {
    return i.file.Seek(offset, whence)
}

func (i injectedFile) Readdir(count int) ([]fs.FileInfo, error) {
    return i.file.Readdir(count)
}

func (i injectedFile) Stat() (fs.FileInfo, error) {
    oldFi, err := i.file.Stat()
    if err != nil {
        return oldFi, err
    }
    newFi := injectedFileInfo{oldFi}
    return newFi, nil
}

func (i injectedFile) Close() error {
    return i.file.Close()
}

func (i injectedFile) Read(b []byte) (n int, err error) {
    fi, _ := i.Stat()

    n, err = i.file.Read(b)

    if err == io.EOF && path.Ext(fi.Name()) == ".html" && len(b) == len(JS) {
        for i, v := range JS {
            b[i] = v
        }
        n += len(JS)
    }

    return n, err
}

type injectedFileSystem struct {
    fileSystem http.FileSystem
}

func (i *injectedFileSystem) Open(name string) (http.File, error) {
    f, err := i.fileSystem.Open(name)
    if err != nil {
        return nil, err
    }
    return injectedFile{file: f}, nil
}
#

@rustic hearth this is what it looks like

rustic hearth
#

what are you having trouble with?

pine mesa
#

The content length mismatch from the incorrect reader

rustic hearth
#

yes, because your code is wrong for the aforementioned reasons

#

I'm asking what are you having trouble understanding

#

even the premise for the question is dubius, you can't inject javascript after the html ends

pine mesa
#

It makes sense to me that b is arbitrary and that checking the length like I do is incorrect. Read is designed to be called multiple times I get that. I think actually rn that I need to find a way to not add all the JS at once but be able to read it in chunks maybe.

pine mesa
rustic hearth
rustic hearth
pine mesa
rustic hearth
#

incorrect

pine mesa
rustic hearth
#

b can be any length, and it may have been written to already.

pine mesa
#

Written to by what?

rustic hearth
#

by your call to file.read

#

the fact that it returned EOF does not mean it didn't write anything

pine mesa
#

yeah that makes sense, so it's not an empty b necessarily, n could be > 0.

#

do I have to get an index for the first empty spot maybe? hmm

rustic hearth
#

if you insist on producing invalid html you need to detect when the html ended and then "switch" to serving your string

pine mesa
#

I guess I can use EOF in conjunction with n to do that, at least the first time around but after idk, I guess I have to play around a bit. Thank you so much for your help. I've spent a ton of time on this project... I've watched "in depth" videos about readers and done various things with them but apparently didn't fully grasp the basics. Also I'm quite blind since I've spent so much time with this code.

pine mesa
#
type injectedFile struct {
    file http.File
    JS   *bytes.Reader
}

func (i injectedFile) Read(b []byte) (n int, err error) {
    if !doneReadHTML {
        n, err = i.file.Read(b)
        if err == io.EOF {
            doneReadHTML = true
        }
        return n, err
    } else {
        n, err = i.JS.Read(b)
        return n, err
    }
}

My first attempt, doesn't work... doneReadHTML is global

rustic hearth
#

why would it be a global?

pine mesa
#

it needs to be global so that it can be referenced again, I wanted to put it in the file struct but that doesn't work since read cannot have a pointer receiver and so changing the bool wouldn't do anything (just forgotten on next read call)

#

but right now it reads correctly the file but the last chunk of it is read twice for some reason and the JS doesn't show up, hmmm

rustic hearth
#

to be clear, not only does it not need to be a global, it mustn't be a global, I want to understand your reasoning

rustic hearth
pine mesa
#

It's not global to the program, but to the package. It needs to be outside so that I can "flip the switch" so next time read is called I can get info wheter html was read or not, if it was in local scope it wouldn't stay flipped if u know what I mean

rustic hearth
#

why would it "not stay flipped"?

#

I'll give you a hint, would b here be flipped? go var b bool flip(b)

pine mesa
#

This is what I wanted but u can see that there is an error (ineffective assignment to field injectedFile.doneReadHTML)

rustic hearth
#

precisely, see my last msg

pine mesa
#

flip(b) same as b = true I guess, I think it would stay flipped but maybe not since ur asking the question xd

rustic hearth
#

can you tell me why?

pine mesa
#
package main

import "fmt"

var (
    b = false
)

func main() {
    change()
    fmt.Println(b)
}

func change() {
    b = true
}

๐Ÿ‘† This is more what I have and it does print true

#

as I expected

rustic hearth
#

I am aware that's what you have

#

that code is incorrect in the context of the problem you're trying to solve

rustic hearth
pine mesa
# rustic hearth ^

I don't see the connection with my code but yeah main() and flip() have different scopes and so b is local to their scope only, u would have to use a pointer in flip in order to flip the actual b

rustic hearth
#

in particular, when calling flip b is copied

#

in func Read(i injectedFile, b []byte) (n int, err error) what's happening to i?

pine mesa
#

It's copied?, but this is why I was talking about the pointer receivers earlier (which is not possible for how readers are supposed to be though so not an option). This is why I put the bool outside...

#

When opening a new html file I make sure to flip it to false again

rustic hearth
pine mesa
rustic hearth
#

oh you edited that message, I did not see the edit

#

then elaborate on why using a pointer receiver is not possible

#

take into account I gave you a mostly functioning (some details missing but working otherwise) solution that used them

pine mesa
#

cannot use injectedFile{โ€ฆ} (value of type injectedFile) as http.File value in return statement: injectedFile does not implement http.File (method Read has pointer receiver) hmmm

#

I can do this it seems like yeah now that u say it

rustic hearth
#

that compiler error is saying you cannot use injectedFile not that *injectedFile cannot implement an interface

#

you can't use injectedFile because injectedFile does not implement that interface, *injectedFile does

pine mesa
#

I think what happens is that it fills the space reserved for the javascript with a repeat of the last chunk of the html, but the code looks correct to me, hmmmmm

#

I FIGURED IT OUT IT SEEMS LIKE YAY

#
func (i *injectedFile) Read(b []byte) (n int, err error) {
    if !i.doneReadHTML {
        n, err = i.file.Read(b)
        if err == io.EOF {
            i.doneReadHTML = true
            n, err = i.JS.Read(b)
            return n, err
        } else {
            return n, err
        }
    } else {
        n, err = i.JS.Read(b)
        return n, err
    }
}
#

I'm just wondering if need to not throw away the first n if EOF is hit, maybe oldn+newn?

rustic hearth
#

when you reach EOF you are overwriting what you just wrote

#

your first attempt was closer to correct, albeit still not quite there

pine mesa
#

Ok gophersadsweat. The more incorrect code I make the more it works xd this one works perfectly from limited testing

#

hmm

rustic hearth
#

that's not impossible

pine mesa
#
func (i *injectedFile) Read(b []byte) (n int, err error) {
    if !i.doneReadHTML {
        n, err = i.file.Read(b)
        if err == io.EOF {
            i.doneReadHTML = true
            jn, err := i.JS.Read(b)
            return n + jn, err
        } else {
            return n, err
        }
    } else {
        n, err = i.JS.Read(b)
        return n, err
    }
}
rustic hearth
#

it's still incorrect as an EOF is not required to be accompanied by a 0 n

pine mesa
#

Ik it's incorrect but it's my first instinct

rustic hearth
rustic hearth
#

I don't understand that question

pine mesa
#

I mean I take into account that n, add it to returned n

#

but I see your point about overwriting b since it's the same b

rustic hearth
rustic hearth
pine mesa
#

Well I know the issue with my previous code is that it fills the space that supposed to have javascript with a repeat of some html

rustic hearth
pine mesa
#

It makes sense to me to return n, err after EOF is hit idk what else to do, maybe I need to not return but rather start reading on the JS but not from the beginning of b but from n, is this on the right track u think?

rustic hearth
#

what does EOF mean?

pine mesa
#

end of file, when the html file has been fully read in this case. This was always my assumption and I did print the output and it confirmed that EOF is hit after html is read

rustic hearth
pine mesa
#

I SEE

func (i *injectedFile) Read(b []byte) (n int, err error) {
    if !i.doneReadHTML {
        n, err = i.file.Read(b)
        if err == io.EOF {
            i.doneReadHTML = true
        }
        return n, nil
    } else {
        n, err = i.JS.Read(b)
        return n, err
    }
}
#

It works ๐Ÿ˜†

#

I guess I should account for if err is not nil and not eof though but yeah I get what you meant now

#

Thanks

rustic hearth
#

fwiw your Seek implementation is also wrong, you do not account for seeks that start after the original file contents

hollow tree
#

i'd rather make a wrapper that remove the content-length header so that seek would never be called.

#

though, range request wouldn't work. but you don't need it anyway.

subtle marlin
#

@pine mesa btw I havn't followed the conversation but would this solves what you want to do: reader := io.MultiReader(html, js) ?

pine mesa
#

hmm I didn't see that about seek

#

hmmm