Class: Store::Digest::Entry

Inherits:
Object
  • Object
show all
Defined in:
lib/store/digest/entry.rb,
lib/store/digest/readwrapper.rb

Overview

for the symbol

Defined Under Namespace

Classes: Flags

Constant Summary collapse

TYPE_CHECKED =

flag constants

1 << 0
TYPE_VALID =
1 << 1
CHARSET_CHECKED =
1 << 2
CHARSET_VALID =
1 << 3
ENCODING_CHECKED =
1 << 4
ENCODING_VALID =
1 << 5
SYNTAX_CHECKED =
1 << 6
SYNTAX_VALID =
1 << 7
IS_CACHE =
1 << 8
STRINGIO_MAX =
2**16

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(content = nil, store: nil, digests: nil, mtime: nil, type: nil, charset: nil, encoding: nil, language: nil, flags: 0, cache: false, strict: false, scan: false, &block) ⇒ Store::Digest::Entry

Note:

use scan or #scan to populate the digests.

Create a new object, naively recording whatever it is handed.

Parameters:

  • content (IO, String, Proc, File, Pathname, ...) (defaults to: nil)

    some content

  • store (Store::Digest) (defaults to: nil)

    the associated store, if present

  • digests (Hash) (defaults to: nil)

    the digests ascribed to the content

  • type (String) (defaults to: nil)

    assert the object's MIME type

  • charset (String) (defaults to: nil)

    the character set, if applicable

  • language (String) (defaults to: nil)

    the (RFC5646) language tag, if applicable

  • encoding (String) (defaults to: nil)

    the content-encoding (e.g. compression)

  • mtime (Time) (defaults to: nil)

    assert object modification time

  • flags (Integer, Flags) (defaults to: 0)

    validation state flags

  • strict (true, false) (defaults to: false)

    raise an error on bad input



353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/store/digest/entry.rb', line 353

def initialize content = nil, store: nil, digests: nil, mtime: nil,
    type: nil, charset: nil, encoding: nil, language: nil, flags: 0,
    cache: false, strict: false, scan: false, &block

  # set the associated store, if one is passed in
  if store
    raise 'Store must be an instance of Store::Digest' unless
      store.is_a? Store::Digest
    @store = store
  end

  now = Time.now(in: ?Z)

  # this sets the empty digest hash and the scanning state to false
  self.content = content if content

  # we do this little ballet because `content=` may set mtime and type
  @mtime = mtime || @mtime || now
  type ||= @type || MimeMagic[nil]

  # the following can be strings or symbols:
  b = binding
  TOKENS.keys.each do |k|
    if x = b.local_variable_get(k)
      x = if strict
            coerce_token(x, k)
          else
            coerce_token(x, k) rescue nil
          end
      instance_variable_set "@#{k}", x.freeze if x
    end
  end

  # warn "wtf #{@type.inspect}"

  # we let the empty through
  digests = coerce_digests digests, empty: true
  if digests.is_a? Hash
    @digests    = digests
    @algorithms = digests.empty? ? algorithms : digests.keys.to_set
    @scanned    = !digests.empty?
  elsif !digests.empty?
    @algorithms = digests.to_set
  end

  # we use this for `#get`
  if block
    hash = block.call @content

    raise TypeError,
      "Block return value must be Hash, not #{hash.class}" unless
      hash.is_a? Hash
    #
    @scanned = true if hash[:digests]
    merge_meta hash, content: true
  elsif @content.nil?
    raise ArgumentError,
      'Must initialize with either content, or a block, or both'
  end

  # just make sure the times are in
  @ctime ||= now
  @mtime ||= mtime || @ctime
  @ptime ||= @ctime

  # set the flags
  @flags ||= Flags.from(flags || 0)
  if cache
    raise NotImplementedError, 'Associated store does not support caching' if
      @store and !@store.can_cache?
    @flags.cache = !!cache
    @dtime = compute_cache cache
  end

  # scan preemptively if so directed
  scan! if scan
end

Instance Attribute Details

#charsetObject (readonly)

Returns the value of attribute charset.



431
432
433
# File 'lib/store/digest/entry.rb', line 431

def charset
  @charset
end

#ctimeObject (readonly)

Returns the value of attribute ctime.



431
432
433
# File 'lib/store/digest/entry.rb', line 431

def ctime
  @ctime
end

#dtimeObject (readonly)

Returns the value of attribute dtime.



431
432
433
# File 'lib/store/digest/entry.rb', line 431

def dtime
  @dtime
end

#encodingObject (readonly)

Returns the value of attribute encoding.



431
432
433
# File 'lib/store/digest/entry.rb', line 431

def encoding
  @encoding
end

#flagsObject

Returns the value of attribute flags.



431
432
433
# File 'lib/store/digest/entry.rb', line 431

def flags
  @flags
end

#languageObject (readonly)

Returns the value of attribute language.



431
432
433
# File 'lib/store/digest/entry.rb', line 431

def language
  @language
end

#mtimeObject (readonly)

Returns the value of attribute mtime.



431
432
433
# File 'lib/store/digest/entry.rb', line 431

def mtime
  @mtime
end

#ptimeObject (readonly)

Returns the value of attribute ptime.



431
432
433
# File 'lib/store/digest/entry.rb', line 431

def ptime
  @ptime
end

#storeObject (readonly)

Returns the value of attribute store.



431
432
433
# File 'lib/store/digest/entry.rb', line 431

def store
  @store
end

#typeObject (readonly)

Returns the value of attribute type.



431
432
433
# File 'lib/store/digest/entry.rb', line 431

def type
  @type
end

Class Method Details

.coerce_digests(digests, algorithms: nil, empty: false, normative: nil) ⇒ Array<Symbol>, Hash{Symbol=>URI::NI}

This will take an array or hash or individual symbol or string or URI::NI object and try to coerce it into something it can use.

  • Individual strings/symbols/URI::NI objects will get wrapped in an array.
  • Strings will be scanned for conformance to RFC6920 and transformed into URI::NI objects if they match, otherwise they will be turned into symbols and matched against the repertoire of hash algorithms.
  • If a URI::NI object isn't valid (e.g., not the full length, algorithm not supported), this will raise an error; likewise if the symbol is not in the repertoire of algorithms.
  • Arrays must contain all the same kind of thing (strings, symbols, URI::NI objects)
  • Hash keys must coerce to symbols (via #to_s, #to_sym) that match the repertoire of algorithms.
  • Hash values must either be a string representing the decimal, base64, or hexadecimal digest of a length corresponding to the algorithm in the key, or a string representing an RFC6920 URI, or a URI::NI.
  • (Base64 strings may be padded or not, and use the standard non-URL-safe representation, or not)
  • Strings will then subsequently be transformed into URI::NI objects.
  • Hash values that are (either already or coerced into) URI::NI objects must be valid and their algorithms must match the hash key with which they are associated.

The input (and thus the output) has two "moods":

  1. Anticipative: "These are the digest algorithms we want to see hashes for."
  2. Normative: "These are the hashes we already have for the input, and it should match them when scanned."

In general inputs that coerce to arrays (except arrays whose contents coerce to URI::NI objects, which in turn will coerce to hashes) are considered anticipative, whereas inputs that coerce to hashes are considered normative. The return value will depend on the adjudicated intent: Array for anticipative, Hash for normative. The caller should inspect the return value to see which it is, because the difference is whether a subsequent scan of the content is intended to verify it (normative) or not (anticipative).

Parameters:

  • digests (#to_sym, #to_s, URI::NI, #to_a<#to_sym,#to_s,URI::NI>, #to_h{#to_sym=>#to_s)

    , #to_hStore::Digest::Entry#to_sym=>URI#to_sym=>URI::NI] the thing to be coerced into digests

  • empty (false, true) (defaults to: false)

    whether the set is allowed to be empty

  • normative (nil, false, true) (defaults to: nil)

    whether to assert the normative mood (true), the anticipative mood (false), or leave it to the caller (nil)

Returns:

  • (Array<Symbol>, Hash{Symbol=>URI::NI})

Raises:

  • (ArgumentError)


492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
# File 'lib/store/digest/entry.rb', line 492

def self.coerce_digests digests, algorithms: nil, empty: false, normative: nil
  algorithms ||= URI::NI.algorithms

  # handle nil
  digests = [] if digests.nil?

  # first we coerce into an array; note hashes respond to `#to_a`
  digests = [digests] unless digests.respond_to? :to_a

  raise ArgumentError,
    'Digest list can\'t be empty' if !empty and digests.empty?

  if digests.is_a? Hash
    # digests = digests[:digests] if digests.key? :digests
    out = digests.map do |k, v|
      # keys must go to symbols; symbols must be valid
      k = k.to_s.downcase.to_sym unless k.is_a? Symbol
      raise ArgumentError,
        "#{k} is not a supported algorithm in this configuration" unless
        algorithms.include? k

      # this should raise on any invalid values
      v = URI::NI.ingest k, v

      # then we assert that the result itself is valid
      raise ArgumentError, "Hash URI #{v} is invalid" unless v.valid?

      [k, v]
    end.to_h

    # warn out

    # note we are explicitly looking to see if normative is false
    # rather than nil
    return normative == false ? out.keys : out
  end

  # otherwise it should be an array so we'll make it into a set
  digests = digests.to_a.map do |thing|
    case thing
    when Symbol then thing
    when URI then URI::NI.ingest thing
    else
      # whatever it is, it should now be a string
      thing = thing.to_s
      if %r{^(?i:ni|https?)://}.match?(thing) and uri = URI::NI.ingest(thing)
        uri
      else
        # turn it into a symbol
        thing.strip.downcase.to_sym
      end
    end
  end.uniq

  # warn digests.inspect

  if digests.all? { |d| d.is_a? URI::NI }
    # we are expressly asking for anticipative if normative is literally false
    return digests.map(&:algorithm) if normative == false

    # otherwise if these are all digest URIs then this is normative;
    # return as a hash
    return digests.map do |d|
      raise ArgumentError,
        "#{d} is not a supported algorithm" unless
        algorithms.include? d.algorithm

      [d.algorithm.to_sym, d]
    end.to_h
  elsif digests.all? { |d| d.is_a? Symbol }
    raise ArgumentError, 'Normative expressly normative' if normative

    return digests
  end

  # if we get here, it's an error
  raise ArgumentError,
    'Input must coerce to either all URIs or all Symbols'
end

.scan(content, store: nil, digests: URI::NI.algorithms, mtime: nil, type: nil, language: nil, charset: nil, encoding: nil, blocksize: BLOCKSIZE, &block) ⇒ Store::Digest::Entry

Preemptively scan a blob and return an entry.

Parameters:

  • content (String, Pathname, IO, #each, #read, #call)

    anything that represents bytes or can be coerced or wrapped by ReadWrapper

  • store (Store::Digest) (defaults to: nil)
  • digests (Array<Symbol,#to_sym,URI::NI>, Hash{Symbol=>URI::NI}) (defaults to: URI::NI.algorithms)

Returns:



735
736
737
738
739
740
741
# File 'lib/store/digest/entry.rb', line 735

def self.scan content, store: nil, digests: URI::NI.algorithms, mtime: nil,
    type: nil, language: nil, charset: nil, encoding: nil,
    blocksize: BLOCKSIZE, &block
  self.new content, store: store, digests: digests, mtime: mtime,
    type: type, language: language, charset: charset, encoding: encoding,
    scan: blocksize, &block
end

.scan_raw(content, algorithms: URI::NI.algorithms, blocksize: BLOCKSIZE, type: false) {|a| ... } ⇒ Array(Hash{Symbol=>URI::NI}, Integer)

Note:

The content is assumed to be at position zero.

Scan a blob and return the digests and byte count.

Parameters:

  • content (#read)

    the object to be scanned

  • algorithms (Array<Symbol,#to_sym>) (defaults to: URI::NI.algorithms)

    the algorithms

  • blocksize (Integer) (defaults to: BLOCKSIZE)

    the block size to use

  • type (false, true) (defaults to: false)

    scan content for media type

Yield Parameters:

  • a (String)

    chunk of input

Returns:

  • (Array(Hash{Symbol=>URI::NI}, Integer))

    a pair containing a hash of the digests and the size in bytes of the blob.

Raises:

  • (ArgumentError)

    the content can't be coerced to something that quacks like #read

  • (ArgumentError)

    the algorithms supplied aren't supported



590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
# File 'lib/store/digest/entry.rb', line 590

def self.scan_raw content, algorithms: URI::NI.algorithms,
    blocksize: BLOCKSIZE, type: false, &block
  # this will raise if it can't be coerced
  content = Store::Digest::ReadWrapper.coerce content

  # coerce digests

  digests = begin
              case algorithms
              when Array,  -> x { x.respond_to? :to_a }
                algorithms.to_a.map(&:to_sym)
              when Symbol, -> x { x.respond_to? :to_sym }
                [algorithms.to_sym]
              else
                raise ArgumentError
              end
            rescue ArgumentError, TypeError, NoMethodError
              raise ArgumentError,
                "Digest algorithms must be coercible to an Array of Symbols"
            end

  # oh this shouldn't be empty btw
  raise ArgumentError, 'Algorithm list should not be empty' if digests.empty?

  # double-check if the digests are supported
  raise ArgumentError,
    "Unsupported digest algorithm(s) #{digests - URI::NI.algorithms}" unless
    (digests - URI::NI.algorithms).empty?

  # now queue up the contexts
  digests = digests.map { |d| [d, URI::NI.context(d)] }.to_h

  # we'll just make a uniform sequence to cycle through, why not
  procs = digests.values.map { |u| -> buf { u << buf } }
  procs << block if block

  if type
    sample = StringIO.new
    procs << -> buf do
      sample << buf
      # take this out of the loop if we have enough
      procs.pop if sample.pos >= SAMPLE
    end
  end

  bytes = 0
  while buf = content.read(blocksize)
    buf = buf.to_s.b # ensure these are bytes we're reading
    bytes += buf.size
    procs.each { |b| b.call buf }
  end

  # apparently i do this because i painted myself into a corner with
  # URI::NI and/or past me previously discovered that there is much
  # more to the hash state than just the digest itself and forgot to
  # tell later-past me when i discovered it a second time around
  digests = digests.map do |k, v|
    [k, URI::NI.compute(v, algorithm: k).freeze]
  end.to_h

  # return the gathered information; everything else is out of band
  out = { digests: digests, size: bytes }

  if sample
    # felt cute lol
    out[:type] = %i[by_magic default_type].lazy.filter_map do |m|
      sample.rewind
      MimeMagic.send m, sample
    end.first
  end

  out
end

Instance Method Details

#add(store = nil) ⇒ Object

Note:

This entry will become associated with the store if it isn't already. If this entry has already been scanned, it will be scanned again.

Add this entry to a Store::Digest instance.

Raises:

  • (ArgumentError)


670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
# File 'lib/store/digest/entry.rb', line 670

def add store = nil
  raise ArgumentError,
    'no store associated with the entry and none passed in' if
    [store, @store].all?(&:nil?)

  # use the internal store if one is not supplied
  store ||= @store
  raise TypeError, 'Argument must be an instance of Store::Digest' unless
    store.is_a? Store::Digest

  # do this if not scanned

  unless scanned? && store.has?(self)
    # ok add the thing
    hash = store.send :add_raw, @content, **meta_hash
    merge_meta hash, content: true
  end

  # set the internal store if one is supplied and not present; do
  # this after because calling store.has? will cause the record to
  # be scanned, potentially against the very store, so it would be
  # scanned by the same store twice.
  @store ||= store

  self
end

#algorithmsArray

Return the algorithms used in the object.

Returns:

  • (Array)


944
945
946
# File 'lib/store/digest/entry.rb', line 944

def algorithms
  @algorithms ||= (@store || URI::NI).algorithms.to_set
end

#cache=(value) ⇒ void

This method returns an undefined value.

Assigns the cache status.

Parameters:

  • value (false, true)

    anything falsy/truthy



1052
1053
1054
# File 'lib/store/digest/entry.rb', line 1052

def cache= value
  @flags.cache = !!value
end

#cache?false, true

Returns whether the object is cache.

Returns:

  • (false, true)


1042
1043
1044
# File 'lib/store/digest/entry.rb', line 1042

def cache?
  !!@flags.cache
end

#charset_checked?false, true

Returns true if the character set has been checked.

Returns:

  • (false, true)


1079
1080
1081
# File 'lib/store/digest/entry.rb', line 1079

def charset_checked?
  @flags.charset_checked
end

#charset_valid?nil, ...

Returns true if the character set has been checked and is valid.

Returns:

  • (nil, false, true)


1087
1088
1089
1090
# File 'lib/store/digest/entry.rb', line 1087

def charset_valid?
  return nil unless @flags.charset_checked
  @flags.charset_valid
end

#closeself

No-op of IO#close.

Returns:

  • (self)


922
923
924
925
# File 'lib/store/digest/entry.rb', line 922

def close
  rewind
  self
end

#contentself?

Note:

This is a vestigial method since Store::Digest::Entry now proxies IO calls.

Returns the content stored in the object.

Returns:

  • (self, nil)

    no-op if there is content, nil if not.



987
988
989
# File 'lib/store/digest/entry.rb', line 987

def content
  self if @content
end

#content=(content) ⇒ Object

Reset the content (and unset the scanned state).

Parameters:

  • content (IO, String, Proc, File, Pathname, ...)

    some content



1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
# File 'lib/store/digest/entry.rb', line 1003

def content= content
  @digests = {}
  @scanned = false
  @content = Store::Digest::ReadWrapper.coerce content, thunk: true

  if @content.respond_to?(:path) and path = @content.path
    # warn MimeMagic.by_path path
    @type = MimeMagic.by_path path
  end

  @mtime = @content.respond_to?(:stat) ? @content.stat.mtime : Time.now(in: ?Z)
end

#content?false, true

Determines if there is content embedded in the object.

Returns:

  • (false, true)


995
996
997
# File 'lib/store/digest/entry.rb', line 995

def content?
  !!@content
end

#deleted?false, true

Just a plain old predicate to determine whether the blob has been deleted from the store (but implicitly the metadata record remains).

Returns:

  • (false, true)


1156
1157
1158
# File 'lib/store/digest/entry.rb', line 1156

def deleted?
  stale? or @dtime && !cache?
end

#digest(symbol) ⇒ URI::NI? Also known as: []

Return a particular digest. Returns nil if there is no match.

Parameters:

  • symbol (Symbol, #to_s, #to_sym)

    the digest

Returns:

  • (URI::NI, nil)

Raises:

  • (ArgumentError)


972
973
974
975
976
# File 'lib/store/digest/entry.rb', line 972

def digest symbol
  raise ArgumentError, "This method takes a symbol" unless
    symbol.respond_to? :to_sym
  digests[symbol.to_sym]
end

#digestsHash

Get the digest hash.

Returns:

  • (Hash)

    the digests



952
953
954
955
# File 'lib/store/digest/entry.rb', line 952

def digests
  scan
  @digests
end

#each(sep = $/, limit = nil, chomp: false) {|chunk| ... } ⇒ self

Iterate over the blob contents.

Yield Parameters:

  • chunk (String)

    the chunk of blob

Returns:

  • (self)


845
846
847
848
849
# File 'lib/store/digest/entry.rb', line 845

def each sep = $/, limit = nil, chomp: false, &block
  scan
  dereference?
  @content.each(sep, limit, chomp: chomp, &block)
end

#encoding_checked?false, true

Returns true if the content encoding (e.g. gzip, deflate) has been checked.

Returns:

  • (false, true)


1097
1098
1099
# File 'lib/store/digest/entry.rb', line 1097

def encoding_checked?
  @flags.encoding_checked
end

#encoding_valid?nil, ...

Returns true if the content encoding has been checked and is valid.

Returns:

  • (nil, false, true)


1105
1106
1107
1108
# File 'lib/store/digest/entry.rb', line 1105

def encoding_valid?
  return nil unless @flags.encoding_checked
  @flags.encoding_valid
end

#gets(sep = $/, chomp = false) ⇒ String

Emulate IO#gets.

Returns:

  • (String)

    the next character



868
869
870
871
872
# File 'lib/store/digest/entry.rb', line 868

def gets sep = $/, chomp = false
  scan
  dereference?
  @content.gets sep, chomp
end

#inspectObject



1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
# File 'lib/store/digest/entry.rb', line 1202

def inspect
  text = if scanned?
           ds = digests.values.map(&:to_s).sort.join ', '
           "size=#{size} type=#{type} (#{})"
         else
           "(not scanned)"
         end

  "<#{self.class} #{text}>"
end

#open(*args) ⇒ self

Note:

Once the blob is scanned, an internal file handle is opened and stays open.

No-op of IO#open for parity.

Returns:

  • (self)


913
914
915
916
# File 'lib/store/digest/entry.rb', line 913

def open *args
  rewind
  self
end

#posObject Also known as: tell



880
881
882
883
884
# File 'lib/store/digest/entry.rb', line 880

def pos
  scan
  dereference?
  @content.pos
end

#pos=(position) ⇒ Object



888
889
890
891
892
# File 'lib/store/digest/entry.rb', line 888

def pos= position
  scan
  dereference?
  @content.pos = position
end

#read(length = nil, buffer = nil) ⇒ String?

Emulate IO#read.

Parameters:

  • length (Integer) (defaults to: nil)

    the number of bytes to read

Returns:

  • (String, nil)

    up to length bytes or nil on EOF



857
858
859
860
861
862
# File 'lib/store/digest/entry.rb', line 857

def read length = nil, buffer = nil
  scan
  dereference?
  # this should be set by scan
  @content.read length, buffer
end

#remove(store = nil, forget: false) ⇒ Object

Remove this entry from a store. Dissociates the entry from the store in the process. Will not signal if the entry wasn't in the store to begin with.

Parameters:

  • store (nil, Store::Digest) (defaults to: nil)

    the store to remove the entry

  • forget (false, true) (defaults to: false)

    whether to purge the entry completely from the metadata or just delete the blob

Raises:

  • (ArgumentError)


705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
# File 'lib/store/digest/entry.rb', line 705

def remove store = nil, forget: false
  raise ArgumentError,
    'no store associated with the entry and none passed in' if
    [store, @store].all?(&:nil?)
  store ||= @store

  raise TypeError, 'store must be a Store::Digest instance' unless
    store.is_a? Store::Digest

  # eliminate the relationship
  @store = nil if @store.equal? store

  rm = forget ? :forget : true
  # this circumvents `private`; ignore return value
  store.send :get_raw, digests[store.primary], remove: rm

  self
end

#rewind0

Emulate IO#rewind.

Returns:

  • (0)

    always zero



898
899
900
901
902
903
904
# File 'lib/store/digest/entry.rb', line 898

def rewind
  scan
  dereference?

  # content should be rewindable after a scan
  @content.rewind
end

#scanself

Scan the blob if it hasn't already been scanned (idempotent).

Returns:

  • (self)


747
748
749
750
# File 'lib/store/digest/entry.rb', line 747

def scan
  scan! if @content && !scanned?
  self
end

#scan!self

Scan the blob unconditionally. May raise an error if the byte size or digests are asserted in the constructor and don't match the scan.

Returns:

  • (self)

Raises:

  • (Store::Digest::Error:Integrity)


761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
# File 'lib/store/digest/entry.rb', line 761

def scan!
  raise Store::Digest::Error::Deleted, 'Entry has no content' unless @content

  if @store
    # we use the store if one is associated
    hash = @store.send :add_raw, @content, **meta_hash

    @content = hash[:content]
  elsif @content.respond_to? :rewind and seekable?(@content)
    # we don't need a temporary file; we'll just reuse this file handle
    @content.rewind
    hash = self.class.scan_raw @content, algorithms: @algorithms, type: true
    @content.rewind
  else
    # start with a stringio
    tmp = StringIO.new
    lam = -> buf do
      tmp << buf

      # check if it's too big
      if tmp.size >= STRINGIO_MAX
        # make an actual file
        file = Tempfile.create anonymous: true, binmode: true

        # put the string into it
        tmp.rewind
        file << tmp.read

        # reassign tmp with the file
        tmp = file

        # reassign lam with this condition removed so we don't
        # needlessly test it over and over with every iteration
        lam = -> buf { file << buf }
      end
    end

    # now we wrap lam in another block so it picks up the reassignment
    hash = self.class.scan_raw(
      @content, algorithms: @algorithms, type: true) { |buf| lam.call buf }
    tmp.rewind
    @content = tmp
  end

  # i suppose this is where the integrity is checked
  if @scanned
    # size
    raise Store::Digest::Error::Integrity,
      "Scanned size #{hash[:size]} does not match asserted #{@size}" if
      hash[:size] != @size

    # digests
    (@digests.keys & hash[:digests].keys).each do |k|
      scanned  = hash[:digests][k]
      asserted = @digests[k]
      raise Store::Digest::Error::CryptographicIntegrity,
        "Scanned digest #{scanned} does not match asserted #{asserted}" if
        scanned != asserted
    end
    # XXX also do content type??
  end

  merge_meta hash

  # unconditionally set this now
  @scanned = true

  self
end

#scanned?false, true

Determines if the object has been scanned.

Returns:

  • (false, true)


835
836
837
# File 'lib/store/digest/entry.rb', line 835

def scanned?
  !!@scanned
end

#seek(offset, whence = IO::SEEK_SET) ⇒ Object



874
875
876
877
878
# File 'lib/store/digest/entry.rb', line 874

def seek offset, whence = IO::SEEK_SET
  scan
  dereference?
  @content.seek offset, whence
end

#sizeInteger

Get the byte size.

Returns:

  • (Integer)

    the bytes



961
962
963
964
# File 'lib/store/digest/entry.rb', line 961

def size
  scan
  @size
end

#stale?Boolean

If the entry is flagged as cache and the expiry time is in the past, then the entry is stale.

Returns:

  • (Boolean)


1146
1147
1148
# File 'lib/store/digest/entry.rb', line 1146

def stale?
  cache? && @dtime && @dtime < Time.now(in: ?Z)
end

#stored?nil, ...

Determine (if possible) if the object is in the store. Returns nil if no store is associated with the entry, otherwise it will query the store.

Returns:

  • (nil, false, true)

    the status of the entry



933
934
935
936
937
938
# File 'lib/store/digest/entry.rb', line 933

def stored?
  # warn @digests
  scan
  # warn scanned?
  @store.has?(digests) if @store
end

#syntax_checked?false, true

Returns true if the blob's syntax has been checked.

Returns:

  • (false, true)


1114
1115
1116
# File 'lib/store/digest/entry.rb', line 1114

def syntax_checked?
  @flags.syntax_checked
end

#syntax_valid?nil, ...

Returns true if the blob's syntax has been checked and is valid.

Returns:

  • (nil, false, true)


1122
1123
1124
1125
# File 'lib/store/digest/entry.rb', line 1122

def syntax_valid?
  return nil unless @flags.syntax_checked
  @flags.syntax_valid
end

#to_h(content: false) ⇒ Hash

Return the object as a hash. Omits the content by default.

Parameters:

  • content (false, true) (defaults to: false)

    include the content if true

Returns:

  • (Hash)

    the object as a hash



1165
1166
1167
1168
1169
1170
1171
# File 'lib/store/digest/entry.rb', line 1165

def to_h content: false
  main = %i[content digests]
  main.shift unless content
  (main + MANDATORY + OPTIONAL + [:flags]).map do |k|
    [k, send(k).dup]
  end.to_h
end

#to_sString

Outputs a human-readable string representation of the object.

Returns:

  • (String)

    said representation



1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
# File 'lib/store/digest/entry.rb', line 1177

def to_s
  out = "#{self.class}\n  Digests:\n"

  # disgorge the digests
  digests.values.sort { |a, b| a.to_s <=> b.to_s }.each do |d|
    out << "    #{d}\n"
  end

  # now the fields
  MANDATORY.each { |m| out << "  #{LABELS[m]}: #{send m}\n" }
  OPTIONAL.each do |o|
    val = send o
    out << "  #{LABELS[o]}: #{val}\n" if val
  end

  # now the validation statuses
  out << "Validation:\n"
  FLAG.each_index do |i|
    x = flags.to_i >> (3 - i) & 3
    out << ("  %-16s: %s\n" % [FLAG[i], STATE[x]])
  end

  out
end

#type_charsetString

Returns the type and charset, suitable for an HTTP header.

Returns:

  • (String)


1020
1021
1022
1023
1024
# File 'lib/store/digest/entry.rb', line 1020

def type_charset
  out = type.to_s
  out += ";charset=#{charset}" if charset
  out
end

#type_checked?false, true

Returns true if the content type has been checked.

Returns:

  • (false, true)


1062
1063
1064
# File 'lib/store/digest/entry.rb', line 1062

def type_checked?
  @flags.type_checked
end

#type_valid?nil, ...

Returns true if the content type has been checked and is valid.

Returns:

  • (nil, false, true)


1070
1071
1072
1073
# File 'lib/store/digest/entry.rb', line 1070

def type_valid?
  return nil unless @flags.type_checked
  @flags.type_valid
end