Code View

fson / source / fson-1.1.0.0 / libs / internal / sdk / fson / Fson.cpp
// SPDX-License-Identifier: MIT
#include "Fson.hh"

#include "FsonParser.hh"
#include "FsonWriter.hh"
#include "IncludeResolver.hh"

#include <filesystem>
#include <fstream>
#include <chrono>
#include <cstdint>

using namespace std;

namespace fedem
{
  namespace fson
  {
    namespace
    {
      // FNV-1a 64-bit over a file's bytes. Not cryptographic — used only to
      // distinguish "same bytes" from "different bytes" for staleness.
      // Returns 0 when the file cannot be read.
      uint64_t hashFile( string const& path )
      {
        ifstream stream( path, ios::binary );
        if( !stream )
          return 0ULL;

        uint64_t hash = 14695981039346656037ULL;  // FNV offset basis
        char buffer[ 64 * 1024 ];
        while( stream.read( buffer, sizeof( buffer ) ) || stream.gcount( ) > 0 )
        {
          streamsize const got = stream.gcount( );
          for( streamsize i = 0; i < got; ++i )
          {
            hash ^= static_cast< unsigned char >( buffer[ i ] );
            hash *= 1099511628211ULL;  // FNV prime
          }
        }
        return hash;
      }

      // Captures mtime (seconds since epoch) and size for one file; zeros
      // both on failure.
      void statFile( string const& path, int64_t& mtime, uintmax_t& size )
      {
        error_code ec;
        auto const writeTime = filesystem::last_write_time( path, ec );
        mtime = ec ? 0
                   : static_cast< int64_t >(
                       chrono::duration_cast< chrono::seconds >(
                         writeTime.time_since_epoch( ) ).count( ) );
        error_code sizeEc;
        auto const bytes = filesystem::file_size( path, sizeEc );
        size = sizeEc ? 0U : bytes;
      }
    }  // end anonymous namespace

    Fson::LoadResult Fson::load( string const& filename )
    {
      LoadResult result;

      // fson 0.9.1.0: skip parsing entirely when the file does not exist.
      // The cparse layer (Parser::parse) writes an unconditional
      // "[Parser::parse] filesystem error: cannot make canonical path"
      // line to std::clog for a missing file. Optional-config callers
      // (which check .ok()/.document and fall back to defaults) then
      // produced that noise on every load of an absent file. Returning
      // the same not-found result (document == nullptr, empty errors)
      // WITHOUT invoking the parser preserves the existing contract
      // (.ok() stays false) while staying silent. A file that exists but
      // is unreadable/unparseable still goes through the parser and
      // surfaces its real diagnostics.
      error_code ec;
      if( !filesystem::exists( filename, ec ) || ec )
        return result;   // document == nullptr → ok() == false

      FsonParser parser;
      result.document = parser.parseFile( filename );
      result.errors   = parser.errors( );

      // Resolve %include directives into the query-visible merge view.
      // Errors from resolution (missing target, cycle, non-object merge
      // root) are appended; dependency paths are exposed for reload use.
      if( result.document )
      {
        IncludeResolver resolver;
        resolver.resolve( *result.document, filename );
        for( auto const& error : resolver.errors( ) )
          result.errors.push_back( error );
        result.dependencies = resolver.dependencies( );
        result.dependencySet = resolver.dependencySet( );
      }

      return result;
    }

    std::set< string > Fson::dependenciesOf( string const& filename )
    {
      // Mirror load()'s silent not-found contract: an absent host file has
      // no dependencies (and would otherwise make the parser log to clog).
      error_code ec;
      if( !filesystem::exists( filename, ec ) || ec )
        return { };

      FsonParser parser;
      unique_ptr< Document > document = parser.parseFile( filename );
      if( !document )
        return { };

      IncludeResolver resolver;
      resolver.resolve( *document, filename );
      return resolver.dependencySet( );
    }

    set< string > Fson::Snapshot::files( ) const
    {
      set< string > result;
      for( auto const& entry : entries )
        result.insert( entry.first );
      return result;
    }

    Fson::Snapshot Fson::snapshot( string const& filename )
    {
      Snapshot snap;

      // The files to record are exactly those needed to resolve the host
      // (host + includes). An absent host yields an empty snapshot.
      set< string > const files = dependenciesOf( filename );
      for( auto const& path : files )
      {
        Snapshot::Entry entry{ 0, 0U, 0ULL };
        statFile( path, entry.mtime, entry.size );
        entry.hash = hashFile( path );
        snap.entries.emplace( path, entry );
      }
      return snap;
    }

    bool Fson::isStale( string const& filename,
                        Snapshot const& snap,
                        StaleMode mode )
    {
      // Re-resolve the current dependency set. If the include graph changed
      // (a file added or removed since the snapshot), that alone is stale —
      // the resolved document can differ even if no recorded file's bytes
      // did. This also covers the empty-snapshot case: a host that was
      // missing (empty snapshot) but now resolves to a non-empty set.
      set< string > const current = dependenciesOf( filename );

      set< string > recorded;
      for( auto const& entry : snap.entries )
        recorded.insert( entry.first );

      if( current != recorded )
        return true;

      // Same file set: compare each file under the chosen mode.
      for( auto const& path : current )
      {
        auto const found = snap.entries.find( path );
        if( found == snap.entries.end( ) )
          return true;  // defensive; sets were equal, so unreachable
        Snapshot::Entry const& was = found->second;

        if( mode == StaleMode::ContentHash )
        {
          if( hashFile( path ) != was.hash )
            return true;
        }
        else  // MtimeSize
        {
          int64_t mtime = 0;
          uintmax_t size = 0U;
          statFile( path, mtime, size );
          if( mtime != was.mtime || size != was.size )
            return true;
        }
      }
      return false;
    }

    bool Fson::save( Document const& document, string const& filename )
    {
      FsonWriter writer;
      return writer.writeFile( document, filename );
    }
  }  // end namespace fson
}  // end namespace fedem