UNPKG

guy

Version:
334 lines (306 loc) 15.6 kB
'use strict' ############################################################################################################ H = require './_helpers' misfit = Symbol 'misfit' platform = ( require 'os' ).platform() rpr = ( require 'util' ).inspect debug = console.log #----------------------------------------------------------------------------------------------------------- ### Constants: ### C_cr = @_C_cr = 0x0d C_lf = @_C_lf = 0x0a C_empty_string = @_C_empty_string = '' C_empty_buffer = @_C_empty_buffer = Buffer.from C_empty_string C_cr_buffer = @_C_cr_buffer = Buffer.from [ C_cr, ] C_lf_buffer = @_C_lf_buffer = Buffer.from [ C_lf, ] #----------------------------------------------------------------------------------------------------------- H.types.declare 'guy_buffer_chr', ( x ) -> return true if ( @isa.integer x ) and ( 0x00 <= x <= 0xff ) return true if ( @isa.buffer x ) and ( x.length > 0 ) return true if ( @isa.text x ) and ( x.length > 0 ) return false #----------------------------------------------------------------------------------------------------------- H.types.declare 'guy_fs_walk_buffers_cfg', tests: "@isa.positive_integer x.chunk_size": ( x ) -> @isa.positive_integer x.chunk_size #----------------------------------------------------------------------------------------------------------- H.types.declare 'guy_fs_walk_lines_cfg', tests: "@isa.object x": ( x ) -> @isa.object x "@isa_optional.nonempty_text x.encoding": ( x ) -> @isa_optional.nonempty_text x.encoding "@isa.positive_integer x.chunk_size": ( x ) -> @isa.positive_integer x.chunk_size "@isa.boolean x.trim": ( x ) -> @isa.boolean x.trim "@isa.text x.prepend": ( x ) -> @isa.text x.prepend "@isa.text x.append": ( x ) -> @isa.text x.append #----------------------------------------------------------------------------------------------------------- H.types.declare 'guy_walk_circular_lines_cfg', tests: "@isa.object x": ( x ) -> @isa.object x "@isa.boolean x.decode": ( x ) -> @isa.boolean x.decode "( x.loop_count is +Infinity ) or ( @isa.cardinal x.loop_count )": ( x ) -> ( x.loop_count is +Infinity ) or ( @isa.cardinal x.loop_count ) "( x.line_count is +Infinity ) or ( @isa.cardinal x.line_count )": ( x ) -> ( x.line_count is +Infinity ) or ( @isa.cardinal x.line_count ) #----------------------------------------------------------------------------------------------------------- H.types.declare 'guy_get_content_hash_cfg', tests: "@isa.object x": ( x ) -> @isa.object x "@isa.cardinal x.length": ( x ) -> @isa.cardinal x.length #----------------------------------------------------------------------------------------------------------- defaults = guy_fs_walk_buffers_cfg: chunk_size: 16 * 1024 guy_fs_walk_lines_cfg: encoding: 'utf-8' chunk_size: 16 * 1024 trim: true prepend: '' append: '' guy_walk_circular_lines_cfg: decode: true loop_count: 1 line_count: +Infinity guy_get_content_hash_cfg: length: 17 fallback: misfit #----------------------------------------------------------------------------------------------------------- @walk_buffers = ( path, cfg ) -> for { buffer, } from @walk_buffers_with_positions path, cfg yield buffer return null #----------------------------------------------------------------------------------------------------------- @walk_buffers_with_positions = ( path, cfg ) -> H.types.validate.guy_fs_walk_buffers_cfg ( cfg = { defaults.guy_fs_walk_buffers_cfg..., cfg..., } ) H.types.validate.nonempty_text path { chunk_size } = cfg FS = require 'node:fs' fd = FS.openSync path byte_idx = 0 loop buffer = Buffer.alloc chunk_size byte_count = FS.readSync fd, buffer, 0, chunk_size, byte_idx break if byte_count is 0 buffer = buffer.subarray 0, byte_count if byte_count < chunk_size yield { buffer, byte_idx, } byte_idx += byte_count return null #----------------------------------------------------------------------------------------------------------- @walk_lines = ( path, cfg ) -> for { line, } from @walk_lines_with_positions path, cfg yield line return null #----------------------------------------------------------------------------------------------------------- @walk_lines_with_positions = ( path, cfg ) -> H.types.validate.guy_fs_walk_lines_cfg ( cfg = { defaults.guy_fs_walk_lines_cfg..., cfg..., } ) H.types.validate.nonempty_text path { chunk_size encoding trim prepend append } = cfg #......................................................................................................... count = 0 for d from @_walk_lines_with_positions path, chunk_size count++ if encoding? d.line = d.line.toString encoding d.line = d.line.trimEnd() if trim d.line = prepend + d.line unless prepend is '' d.line = d.line + append unless append is '' d.eol = d.eol.toString encoding yield d else yield d return null #----------------------------------------------------------------------------------------------------------- @_walk_lines_with_positions = ( path, chunk_size = 16 * 1024 ) -> line_cache = [] eol_cache = [] lnr = 0 # offset_bytes = null #......................................................................................................... flush = ( material = null, eol = null ) -> # return null if ( line_cache.length is 0 ) and ( eol_cache.length is 0 ) # byte_idx = file_byte_idx + offset_bytes line = Buffer.concat if material? then [ line_cache..., material, ] else line_cache eol = Buffer.concat if eol? then [ eol_cache..., eol, ] else eol_cache line_cache.length = 0 eol_cache.length = 0 lnr++ # yield { byte_idx, lnr, line, eol, } yield { lnr, line, eol, } #......................................................................................................... for { buffer, byte_idx: file_byte_idx, } from @walk_buffers_with_positions path, { chunk_size, } # debug '^_walk_lines_with_positions@23-4^', byte_idx, ( rpr buffer.toString() ) # offset_bytes = 0 for { material, eol, } from @_walk_lines__walk_advancements buffer # debug '^_walk_lines_with_positions@23-4^', { file_byte_idx, offset_bytes, }, ( rpr material.toString() ), ( rpr eol.toString() ) if ( eol_cache.length > 0 ) and not ( ( ( eol_cache.at -1 ) is C_cr_buffer ) and eol is C_lf_buffer ) yield from flush() switch eol when C_cr_buffer line_cache.push material if material.length > 0 eol_cache.push eol when C_lf_buffer yield from flush material, eol when C_empty_buffer line_cache.push material if material.length > 0 else throw new Error "^636456^ internal error" # offset_bytes += material.length + eol.length #......................................................................................................... has_extra_cr = ( eol_cache.length > 0 ) and ( ( eol_cache.at -1 ) is C_cr_buffer ) yield from flush() if has_extra_cr lnr++ yield { lnr, line: C_empty_buffer, eol: C_empty_buffer, } #......................................................................................................... return null #----------------------------------------------------------------------------------------------------------- @_walk_lines__walk_advancements = ( buffer, may_have_cr = true, may_have_lf = true ) -> first_idx = 0 last_idx = buffer.length - 1 loop break if first_idx > last_idx yield d = @_walk_lines__advance buffer, first_idx, may_have_cr, may_have_lf first_idx = d.next_idx return null #----------------------------------------------------------------------------------------------------------- ### TAINT add may_have_cr, may_have_lf as optimization to forego repeated unnecessary lookups ### @_walk_lines__advance = ( buffer, first_idx, may_have_cr = true, may_have_lf = true ) -> material = C_empty_buffer eol = C_empty_buffer next_idx_cr = -1 next_idx_lf = -1 next_idx_cr = buffer.indexOf C_cr, first_idx if may_have_cr next_idx_lf = buffer.indexOf C_lf, first_idx if may_have_lf next_idx = buffer.length #......................................................................................................... if next_idx_cr is -1 if next_idx_lf is -1 return { material: buffer, eol, next_idx, } if first_idx is 0 else next_idx = next_idx_lf eol = C_lf_buffer #......................................................................................................... else if ( next_idx_lf is -1 ) or ( next_idx_cr < next_idx_lf ) next_idx = next_idx_cr eol = C_cr_buffer #......................................................................................................... else next_idx = next_idx_lf eol = C_lf_buffer #......................................................................................................... material = buffer.subarray first_idx, next_idx return { material, eol, next_idx: next_idx + 1, } #=========================================================================================================== # #----------------------------------------------------------------------------------------------------------- @walk_circular_lines = ( path, cfg ) -> H.types.validate.guy_walk_circular_lines_cfg ( cfg = { defaults.guy_walk_circular_lines_cfg..., cfg..., } ) return if ( cfg.line_count is 0 ) or ( cfg.loop_count is 0 ) line_count = 0 loop_count = 0 loop for line from @walk_lines path, { decode: cfg.decode, } yield line line_count++; return null if line_count >= cfg.line_count loop_count++; return null if loop_count >= cfg.loop_count return null #----------------------------------------------------------------------------------------------------------- @get_file_size = ( path, fallback = misfit ) -> H.types.validate.nonempty_text path try return ( ( require 'fs' ).statSync path ).size catch error throw error if ( fallback is misfit ) throw error if ( error.code isnt 'ENOENT' ) return fallback #----------------------------------------------------------------------------------------------------------- @get_content_hash = ( path, cfg ) -> H.types.validate.nonempty_text path H.types.validate.guy_get_content_hash_cfg ( cfg = { defaults.guy_get_content_hash_cfg..., cfg..., } ) CP = require 'child_process' command = if platform is 'linux' then 'sha1sum' else 'shasum' result = CP.spawnSync command, [ '-b', path, ] if result.status isnt 0 return cfg.fallback unless cfg.fallback is misfit if result.stderr? throw new Error "^guy.fs.get_content_hash@1^ " + result.stderr.toString 'utf-8' else throw new Error "^guy.fs.get_content_hash@1^ " + ( require 'util' ).inspect result.error R = result R = R.stdout.toString 'utf-8' R = R.replace /\s.*$/, '' R = R[ ... cfg.length ] unless R.length is cfg.length throw new Error "^guy.fs.get_content_hash@1^ unable to generate hash of length #{cfg.length} using #{command}" return R #----------------------------------------------------------------------------------------------------------- @rename_sync = ( from_path, to_path ) -> ### Same as `FS.renameSync()`, but falls back to `FS.copyFileSync()`, `FS.unlinkSync()` in case device boundaries are crossed. Thx to https://github.com/sindresorhus/move-file/blob/main/index.js ### FS = require 'node:fs' try FS.renameSync from_path, to_path catch error throw error unless error.code is 'EXDEV' FS.copyFileSync from_path, to_path FS.unlinkSync from_path return null #----------------------------------------------------------------------------------------------------------- _get_descriptor_types_and_methods = [ { type: 'file', method_name: 'isFile', } { type: 'folder', method_name: 'isDirectory', } { type: 'block', method_name: 'isBlockDevice', } { type: 'character', method_name: 'isCharacterDevice', } { type: 'fifo', method_name: 'isFIFO', } { type: 'socket', method_name: 'isSocket', } ] #----------------------------------------------------------------------------------------------------------- @_get_descriptor = ( path ) -> FS = require 'node:fs' type = null link = false is_loop = false #......................................................................................................... [ lstat, link, ] = do => try lstat = FS.lstatSync path catch error return [ null, false, ] if error.code is 'ENOENT' throw error return [ lstat, lstat.isSymbolicLink(), ] #......................................................................................................... return { type, link, is_loop, } unless lstat? #......................................................................................................... if link then [ stat, is_loop, ] = do => try stat = FS.statSync path catch error return [ null, true, ] if error.code is 'ELOOP' return [ null, false, ] if error.code is 'ENOENT' throw error return [ stat, false, ] is_loop ?= false stat = lstat if is_loop or ( not link ) return { type: 'link', link, is_loop, } if ( not stat? ) and ( not is_loop ) type = do => for { type, method_name, } in _get_descriptor_types_and_methods return type if stat[ method_name ]() return null #......................................................................................................... type ?= 'link' return { type, link, is_loop, } #----------------------------------------------------------------------------------------------------------- @get_descriptor = ( P... ) -> @_get_descriptor P... # #----------------------------------------------------------------------------------------------------------- # get_long_file_descriptor = ( P... ) -> # throw new Error "not implemented" # { dsc, stats } = _get_descriptor P... # dev: 2114n, # ino: 48064969n, # mode: 33188n, # nlink: 1n, # uid: 85n, # gid: 100n, # rdev: 0n, # size: 527n, # blksize: 4096n, # blocks: 8n, # atimeMs: 1318289051000n, # mtimeMs: 1318289051000n, # ctimeMs: 1318289051000n, # birthtimeMs: 1318289051000n, # atimeNs: 1318289051000000000n, # mtimeNs: 1318289051000000000n, # ctimeNs: 1318289051000000000n, # birthtimeNs: 1318289051000000000n, # atime: Mon, 10 Oct 2011 23:24:11 GMT, # mtime: Mon, 10 Oct 2011 23:24:11 GMT, # ctime: Mon, 10 Oct 2011 23:24:11 GMT, # birthtime: Mon, 10 Oct 2011 23:24:11 GMT