JavaScript - file to bytes array

Viewed 652

I'm trying to get a script to work, which is called sfo.js.

The repo mentions only this usage:

keys = parse_sfo(Some_ArrayBuffer);
console.log(keys['TITLE']);

Looking into the sfo.js, parse_sfo has the sfoBytes argument.

From this I've concluded the sfoBytes argument needs to be an arraybuffer of file bytes.

I've tried to make a script that parses the SFO file into a array of bytes:

<script src="sfo.js"></script>
<script>
function stringToArrayBuffer(str) {
    var buf = [];
    
    for (var i=0, strLen=str.length; i<strLen; i++) {
        buf[i] = str.charCodeAt(i);
    }
    
    console.log(buf);

    return buf;
}

function testing(url) {
    var request = new XMLHttpRequest();
    request.open('GET', url, false);
    request.send(null);

    if (request.status === 200) {
        console.log(request.response);
        
        var response = request.response;
        var array = stringToArrayBuffer(response);

        return array;
    } else {
        alert('Error!');
    }
 }

var data = testing('param.sfo');
var sfo = parse_sfo(data);
</script>

That throws an error in the console:

Uncaught RangeError: byte length of Uint32Array should be a multiple of 4 at new Uint32Array (<anonymous>)
    at readUint32At (sfo.js:20)
    at parse_sfo (sfo.js:113)
    at (index):29

I'm pretty sure I'm doing something wrong. Does anybody understand how I can make the script work properly?

I have a sample file for param.sfo: https://filebin.net/gghosrp6u93jn7y8 (if linking to a download is not allowed please let me know)

3 Answers

You are using some 20 years old JavaScript. Please use fetch add TextEncoder for your own sanity.

fetch(url).then(res => res.json().then(data => {
  const encoder = new TextEncoder()
  const bytes = encoder.encode(data)
  parse_sfo(data)
})

sfoBytes should be the actual array buffer of the sfo file. It's simpler than what you are trying, you shouldn't need to convert the request response with stringToArrayBuffer since you can get an array buffer from XMLHttpRequest. Also, the SFO file isn't a text file, it's binary, so conversion from string wouldn't work anyway.

Changing your request to get a arraybuffer response type should do it like this:

function testing(url) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.responseType = "arraybuffer";
    request.send(null);

    if (request.status === 200) {
        console.log(request.response);
        
        var response = request.response;
        var sfo = parse_sfo(response);
    } else {
        alert('Error!');
    }
 }

Ok, finally the working example.

I've found a small param.sfo file in the internet.

Notes:

  • there are 2 versions of file reader: local and remote
  • in the snippet below you can test both (I've added my param.sfo as an external link to test 'remote'). To test 'local' you just need to select any sfo file from you PC.
  • as a result I show all the keys, not only 'TITLE' (as it is in your question). You can then select desired key

function getSfoLocal(callback) {
  // for now I use local file for testing
  document.querySelector('input').addEventListener('change', function() {

    var reader = new FileReader();
    reader.onload = function() {

      var arrayBuffer = this.result;
      var array = new Uint8Array(arrayBuffer);
      // var binaryString = String.fromCharCode.apply(null, array);

      if (typeof callback === 'function') callback(array);
    }
    reader.readAsArrayBuffer(this.files[0]);

  }, false);
}

function getSfoRemote(url, callback) {
  var concatArrayBuffers = function(buffer1, buffer2) {

    if (!buffer1) {
      return buffer2;
    } else if (!buffer2) {
      return buffer1;
    }

    var tmp = new Uint8Array(buffer1.length + buffer2.length);
    tmp.set(buffer1, 0);
    tmp.set(buffer2, buffer1.byteLength);
    return tmp.buffer;
  };

  fetch(url).then(res => {
    const reader = res.body.getReader();
    let charsReceived = 0;
    let result = new Uint8Array;

    reader.read().then(function processText({
      done,
      value
    }) {
      // Result objects contain two properties:
      // done  - true if the stream has already given you all its data.
      // value - some data. Always undefined when done is true.
      if (done) {
        if (typeof callback === 'function') callback(result);
        return result;
      }

      // value for fetch streams is a Uint8Array
      charsReceived += value.length;
      const chunk = value;
      result = concatArrayBuffers(result, chunk);

      // Read some more, and call this function again
      return reader.read().then(processText);
    });
  });
}

function getSfo(type, url, callback) {
  if (type === 'local') getSfoLocal(callback);
  if (type === 'remote') getSfoRemote(url, callback);
}

getSfo('local', null, (data) => {
  keys = parse_sfo(data);
  console.log('LOCAL =', keys);
});

function goremote() {
  getSfo('remote', 'https://srv-file9.gofile.io/download/Y0gVfw/PARAM.SFO', (data) => {
    keys = parse_sfo(data);
    console.log('REMOTE =', keys);
  });
}
div { padding: 4px; }
<!--script src="https://rawcdn.githack.com/KuromeSan/sfo.js/c7aa8209785cc5a39c4231e683f6a2d1b1e91153/sfo.js" for="release"></script-->
<script src="https://raw.githack.com/KuromeSan/sfo.js/master/sfo.js" for="develop"></script>

<div>Local version: <input type="file" /></div>
<div>Remote version: <button onclick="goremote()">Go remote</button></div>

P.S. It seems that gofile.io service that I've used for remote example is not visible sometimes. If you have permanent link to param.sfo just let me know. But now I have to visit my file, and only after that it becomes visible.

Related