stdlib/nodejs/file.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# backtick_javascript: true

require 'corelib/file'

%x{
  var warnings = {}, errno_codes = #{Errno.constants};

  function handle_unsupported_feature(message) {
    switch (Opal.config.unsupported_features_severity) {
    case 'error':
      #{Kernel.raise NotImplementedError, `message`}
      break;
    case 'warning':
      warn(message)
      break;
    default: // ignore
      // noop
    }
  }
  function warn(string) {
    if (warnings[string]) {
      return;
    }
    warnings[string] = true;
    #{warn(`string`)};
  }
  function is_utf8(bytes) {
    var i = 0;
    while (i < bytes.length) {
      if ((// ASCII
        bytes[i] === 0x09 ||
        bytes[i] === 0x0A ||
        bytes[i] === 0x0D ||
        (0x20 <= bytes[i] && bytes[i] <= 0x7E)
      )
      ) {
        i += 1;
        continue;
      }

      if ((// non-overlong 2-byte
        (0xC2 <= bytes[i] && bytes[i] <= 0xDF) &&
        (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF)
      )
      ) {
        i += 2;
        continue;
      }

      if ((// excluding overlongs
          bytes[i] === 0xE0 &&
          (0xA0 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
          (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF)
        ) ||
        (// straight 3-byte
          ((0xE1 <= bytes[i] && bytes[i] <= 0xEC) ||
            bytes[i] === 0xEE ||
            bytes[i] === 0xEF) &&
          (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
          (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF)
        ) ||
        (// excluding surrogates
          bytes[i] === 0xED &&
          (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0x9F) &&
          (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF)
        )
      ) {
        i += 3;
        continue;
      }

      if ((// planes 1-3
          bytes[i] === 0xF0 &&
          (0x90 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
          (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
          (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
        ) ||
        (// planes 4-15
          (0xF1 <= bytes[i] && bytes[i] <= 0xF3) &&
          (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
          (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
          (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
        ) ||
        (// plane 16
          bytes[i] === 0xF4 &&
          (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0x8F) &&
          (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
          (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
        )
      ) {
        i += 4;
        continue;
      }

      return false;
    }

    return true;
  }
  function executeIOAction(action) {
    try {
      return action();
    } catch (error) {
      if (errno_codes.indexOf(error.code) >= 0) {
        var error_class = #{Errno.const_get(`error.code`)}
        #{Kernel.raise `error_class`.new(`error.message`)}
      }
      #{Kernel.raise `error`}
    }
  }
}

class File < IO
  @__fs__ = `require('fs')`
  @__path__ = `require('path')`
  `var __fs__ = #{@__fs__}`
  `var __path__ = #{@__path__}`
  # Since Node.js 11+ TextEncoder and TextDecoder are now available on the global object.
  %x{
    var __TextEncoder__, __TextDecoder__;
    if (typeof TextEncoder !== 'undefined') {
        __TextEncoder__ = TextEncoder;
        __TextDecoder__ = TextDecoder;
    } else {
        var __util__ = require('util');
        __TextEncoder__ = __util__.TextEncoder;
        __TextDecoder__ = __util__.TextDecoder;
    }
    var __utf8TextDecoder__ = new __TextDecoder__('utf8');
    var __textEncoder__ = new __TextEncoder__();
  }

  if `__path__.sep !== #{Separator}`
    ALT_SEPARATOR = `__path__.sep`
  end


  def self.read(path)
    `return executeIOAction(function(){return __fs__.readFileSync(#{path}).toString()})`
  end

  def self.write(path, data)
    `executeIOAction(function(){return __fs__.writeFileSync(#{path}, #{data})})`
    data.size
  end

  def self.symlink(path, new_path)
    `executeIOAction(function(){return __fs__.symlinkSync(#{path}, #{new_path})})`
    0
  end

  def self.delete(path)
    `executeIOAction(function(){return __fs__.unlinkSync(#{path})})`
  end

  class << self
    alias unlink delete
  end

  def self.exist?(path)
    path = path.path if path.respond_to? :path
    `return executeIOAction(function(){return __fs__.existsSync(#{path})})`
  end

  def self.realpath(pathname, dir_string = nil, cache = nil, &block)
    pathname = join(dir_string, pathname) if dir_string
    if block_given?
      `
        __fs__.realpath(#{pathname}, #{cache}, function(error, realpath){
          if (error) Opal.IOError.$new(error.message)
          else #{block.call(`realpath`)}
        })
        `
    else
      `return executeIOAction(function(){return __fs__.realpathSync(#{pathname}, #{cache})})`
    end
  end

  def self.join(*paths)
    # by itself, `path.posix.join` normalizes leading // to /.
    # restore the leading / on UNC paths (i.e., paths starting with //).
    paths = paths.map(&:to_s)
    prefix = paths.first&.start_with?('//') ? '/' : ''
    `#{prefix} + __path__.posix.join.apply(__path__, #{paths})`
  end

  def self.directory?(path)
    return false unless exist? path
    result = `executeIOAction(function(){return !!__fs__.lstatSync(path).isDirectory()})`
    unless result
      realpath = realpath(path)
      if realpath != path
        result = `executeIOAction(function(){return !!__fs__.lstatSync(realpath).isDirectory()})`
      end
    end
    result
  end

  def self.file?(path)
    return false unless exist? path
    result = `executeIOAction(function(){return !!__fs__.lstatSync(path).isFile()})`
    unless result
      realpath = realpath(path)
      if realpath != path
        result = `executeIOAction(function(){return !!__fs__.lstatSync(realpath).isFile()})`
      end
    end
    result
  end

  def self.readable?(path)
    return false unless exist? path
    %x{
      try {
        __fs__.accessSync(path, __fs__.R_OK);
        return true;
      } catch (error) {
        return false;
      }
    }
  end

  def self.size(path)
    `return executeIOAction(function(){return __fs__.lstatSync(path).size})`
  end

  def self.open(path, mode = 'r')
    file = new(path, mode)
    if block_given?
      begin
        yield(file)
      ensure
        file.close
      end
    else
      file
    end
  end

  def self.stat(path)
    path = path.path if path.respond_to? :path
    File::Stat.new(path)
  end

  def self.mtime(path)
    `return executeIOAction(function(){return __fs__.statSync(#{path}).mtime})`
  end

  def self.symlink?(path)
    `return executeIOAction(function(){return __fs__.lstatSync(#{path}).isSymbolicLink()})`
  end

  def self.absolute_path(path, basedir = nil)
    path = path.respond_to?(:to_path) ? path.to_path : path
    basedir ||= Dir.pwd
    `return __path__.normalize(__path__.resolve(#{basedir.to_str}, #{path.to_str})).split(__path__.sep).join(__path__.posix.sep)`
  end

  def self.absolute_path?(path)
    `__path__.isAbsolute(path)`
  end

  # Instance Methods

  def initialize(path, flags = 'r')
    @binary_flag = flags.include?('b')
    # Node does not recognize this flag
    flags = flags.delete('b')
    # encoding flag is unsupported
    encoding_option_rx = /:(.*)/
    if encoding_option_rx.match?(flags)
      `handle_unsupported_feature("Encoding option (:encoding) is unsupported by Node.js openSync method and will be removed.")`
      flags = flags.sub(encoding_option_rx, '')
    end
    @path = path

    fd = `executeIOAction(function(){return __fs__.openSync(path, flags)})`
    super(fd, flags)
  end

  attr_reader :path

  def sysread(bytes)
    if @eof
      raise EOFError, 'end of file reached'
    else
      if @binary_flag
        %x{
          var buf = executeIOAction(function(){return __fs__.readFileSync(#{@path})})
          var content
          if (is_utf8(buf)) {
            content = buf.toString('utf8')
          } else {
            // coerce to utf8
            content = __utf8TextDecoder__.decode(__textEncoder__.encode(buf.toString('binary')))
          }
        }
        res = `content`
      else
        res = `executeIOAction(function(){return __fs__.readFileSync(#{@path}).toString('utf8')})`
      end
      @eof = true
      @lineno = res.size
      res
    end
  end

  def write(string)
    `executeIOAction(function(){return __fs__.writeSync(#{@fd}, #{string})})`
  end

  def flush
    `executeIOAction(function(){return __fs__.fsyncSync(#{@fd})})`
  end

  def close
    `executeIOAction(function(){return __fs__.closeSync(#{@fd})})`
    super
  end

  def mtime
    `return executeIOAction(function(){return __fs__.statSync(#{@path}).mtime})`
  end
end

class File::Stat
  @__fs__ = `require('fs')`
  `var __fs__ = #{@__fs__}`

  def initialize(path)
    @path = path
  end

  def file?
    `return executeIOAction(function(){return __fs__.statSync(#{@path}).isFile()})`
  end

  def directory?
    `return executeIOAction(function(){return __fs__.statSync(#{@path}).isDirectory()})`
  end

  def mtime
    `return executeIOAction(function(){return __fs__.statSync(#{@path}).mtime})`
  end

  def readable?
    `return executeIOAction(function(){return __fs__.accessSync(#{@path}, __fs__.constants.R_OK)})`
  end

  def writable?
    `return executeIOAction(function(){return __fs__.accessSync(#{@path}, __fs__.constants.W_OK)})`
  end

  def executable?
    `return executeIOAction(function(){return __fs__.accessSync(#{@path}, __fs__.constants.X_OK)})`
  end
end