4

In Node.js how do I change (overwrite) a byte in a binary file (at a certain offset) without adding bytes in between and changing its length?

In C I would just do something like fopen() the file with "r+", fseek() to the offset where I want to change, then overwrite the bytes with fwrite(). How would the equivalent in Node.js look like?

2
  • Why do you want to do this ? Commented Dec 17, 2018 at 17:41
  • 1
    I have some binary chunk file with a fixed length and I don't want to overwrite the whole chunk, instead I only want to manipulate some bytes in the file while the chunk file size should stay the same. Chunks like those from an open-world game like Minecraft :D Commented Dec 17, 2018 at 18:57

2 Answers 2

5

Okay, I figured out it is actually pretty straight forward ^^

fs.open(filename, "r+", (err, fd) => {
    if(!err) {
        fs.write(
            fd, new Uint8Array([byte]), 0, 1, offset,
            (err, bw, buf) => {
                if(!err) {
                    // succesfully wrote byte to offset
                }
            }
        );
    }
});
Sign up to request clarification or add additional context in comments.

Comments

0

I had to do something like what you described recently. I had to update a URL in an executable without changing the size. The key is to use a Transform on a stream. The idea is that the Transform will read in and write out the exact data you want and only modify the bytes you specify.

Here's a Transform class that does a find and replace within a stream. The constructor takes arguments for what the start and end sequences of bytes are for the chunk that should be replaced. There is also a padValue argument that is used to maintain that same size.

import { Transform } from 'stream'

export default class FindAndReplaceTransform extends Transform {
  constructor(startBuffer, endBuffer, replacementValueBuffer, padValue, options) {
    super(options);
    this.startBuffer = startBuffer;
    this.endBuffer = endBuffer;
    this.replacementValueBuffer = replacementValueBuffer;
    this.padValue = padValue;
  }

  _findInBuffer(sourceBuffer, searchBuffer) {
    let searchFound = -1;
    let lengthOfPartialMatch = 0;
    for (let i = 0; i < sourceBuffer.length; i++) {
      for (let j = 0; j < searchBuffer.length; j++) {
        if (i + j >= sourceBuffer.length) {
          if (j > 0) {
            lengthOfPartialMatch = j;
          }
          break;
        }
        if (sourceBuffer[i + j] !== searchBuffer[j]) {
          break;
        }
        if (j === searchBuffer.length - 1) {
          searchFound = i;
        }
      }
      if (searchFound >= 0 || lengthOfPartialMatch > 0) {
        break;
      }
    }
    return { searchFound, lengthOfPartialMatch };
  }

  _doReplacement(length) {
    let replacementValueBuffer = this.replacementValueBuffer;
    if (this.padValue !== undefined) {
      replacementValueBuffer = Buffer.concat([replacementValueBuffer, Buffer.alloc(length - replacementValueBuffer.length, this.padValue)], length);
    }
    this.push(replacementValueBuffer);
  }

  //override
  _transform(data, encoding, done) {
    if(this.lengthOfPartialStartMatch){
      data = Buffer.concat([this.startBuffer.slice(0, this.lengthOfPartialStartMatch), data], this.lengthOfPartialStartMatch + data.length);
      delete this.lengthOfPartialStartMatch;
    }
    if(this.lengthOfPartialEndMatch){
      data = Buffer.concat([this.endBuffer.slice(0, this.lengthOfPartialEndMatch), data], this.lengthOfPartialEndMatch + data.length);
      this.replacementBuffer = this.replacementBuffer.slice(0, this.replacementBuffer.length - this.lengthOfPartialEndMatch);
      delete this.lengthOfPartialEndMatch;
    }

    let startAlreadyFound = !!this.replacementBuffer
    let { searchFound: startIndex, lengthOfPartialMatch: lengthOfPartialStartMatch } = this._findInBuffer(data, this.startBuffer);
    let tail = data.slice(startIndex >= 0 && !startAlreadyFound ? startIndex : 0);
    let { searchFound: endIndex, lengthOfPartialMatch: lengthOfPartialEndMatch } = this._findInBuffer(tail, this.endBuffer);


    if (!startAlreadyFound && startIndex >= 0) {
      this.push(data.slice(0, startIndex))
      this.replacementBuffer = Buffer.alloc(0);
      startAlreadyFound = true;
    }
    if (startAlreadyFound) {
      if (endIndex >= 0) {
        let replacementLength = this.replacementBuffer.length + endIndex + this.endBuffer.length;
        this._doReplacement(replacementLength);
        delete this.replacementBuffer;
        if (endIndex + this.endBuffer.length < tail.length) {
          let remainder = tail.slice(endIndex + this.endBuffer.length)
          this._transform(remainder, encoding, done);
          return;
        }
      } else {
        this.lengthOfPartialEndMatch = lengthOfPartialEndMatch;
        this.replacementBuffer = Buffer.concat([this.replacementBuffer, tail], this.replacementBuffer.length + tail.length);
      }
    } else {
      this.lengthOfPartialStartMatch = lengthOfPartialStartMatch;
      this.push(data.slice(0, data.length - lengthOfPartialStartMatch))
    }
    done();
  }

  //override
  _flush(done) {
    if (this.replacementBuffer) {
      this.push(this.replacementBuffer)
    }
    if(this.lengthOfPartialStartMatch){
      this.push(this.startBuffer.slice(0, this.lengthOfPartialStartMatch));
    }
    delete this.replacementBuffer;
    delete this.lengthOfPartialStartMatch;
    delete this.lengthOfPartialEndMatch;
    done()
  }
}

To use the above transform, you could do something like this:

let stream = fs.createReadStream(inputFile);
let padding = 0x00;
let startSequence = Buffer.from('${', 'utf16le');
let endSequence = Buffer.from('}', 'utf16le');
let transform = new FindAndReplaceTransform(startSequence, endSequence, Buffer.from(replacementValue, 'utf16le'), paddingValue);
stream = stream.pipe(transform);
stream.pipe(fs.createWriteStream(outputFile));

Obviously, if all you want to do is change a byte at a certain offset, the Transform class will be significantly simpler. I provided that above code because I had it and if you want to do something a little more complex, you have it for reference.

The main method that you want to make sure to implement is the _transform method. You also may need to implement the _flush method depending on you implementation. The other class methods in the above code are for my implementation of the replacement code and are not needed for a Transform to work.

2 Comments

WTF? O.o All this code for modifying a single byte in a file? WTF?
The code is just an example of using a stream Transform class in node. The code required to change a single byte would be considerably smaller and simpler.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.