From 37b8091690f044e4a5f61118c971b24f34a91c41 Mon Sep 17 00:00:00 2001 From: "mahlon@martini.nu" Date: Mon, 30 Nov 2020 05:57:26 +0000 Subject: [PATCH] Checkpoint commit, start sketching out MDBX::Database. FossilOrigin-Name: 92f55b2d6cc9652cb9cb67e69588ed0f4f155086a5a160aae68a1f22e9114314 --- .rubocop.yml | 298 +++++++++++++++++++++++++++++++++++++ .ruby-version | 2 +- ext/mdbx_ext/database.c | 206 +++++++++++++++++++++++++ ext/mdbx_ext/mdbx_ext.c | 11 +- ext/mdbx_ext/mdbx_ext.h | 21 ++- lib/mdbx.rb | 4 +- lib/mdbx/database.rb | 43 ++++++ spec/data/testdb/mdbx.dat | Bin 0 -> 65536 bytes spec/data/testdb/mdbx.lck | 0 spec/lib/helper.rb | 10 +- spec/mdbx/database_spec.rb | 13 ++ 11 files changed, 600 insertions(+), 8 deletions(-) create mode 100644 .rubocop.yml create mode 100644 ext/mdbx_ext/database.c create mode 100644 lib/mdbx/database.rb create mode 100644 spec/data/testdb/mdbx.dat create mode 100644 spec/data/testdb/mdbx.lck create mode 100644 spec/mdbx/database_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..ad1f57a --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,298 @@ +# vim: set nosta et ts=4 sw=4: +# +# Rubocop is known to have breaking changes. +# Disable everything by default, enable only the rulesets +# we're interested in. +# +--- +AllCops: + StyleGuideBaseURL: https://app.laika.com/intranet/it/RubyStyleGuide + DisabledByDefault: true + UseCache: true + MaxFilesInCache: 250 + AllowSymlinksInCacheRootDirectory: true + NewCops: enable + Exclude: + - gem.deps.rb + - "*.gemspec" + +# https://docs.rubocop.org/rubocop/0.93/cops_layout.html +# Matching the LAIKA style guide as much as possible. +# +Layout/AccessModifierIndentation: + Enabled: true + EnforcedStyle: indent +Layout/ArgumentAlignment: + Enabled: true + EnforcedStyle: with_fixed_indentation +Layout/ArrayAlignment: + Enabled: true + EnforcedStyle: with_first_element +Layout/AssignmentIndentation: + Enabled: true +Layout/BeginEndAlignment: + Enabled: true + EnforcedStyleAlignWith: start_of_line +Layout/BlockAlignment: + Enabled: true + EnforcedStyleAlignWith: either +Layout/BlockEndNewline: + Enabled: true +Layout/ClassStructure: + ExpectedOrder: + - module_inclusion + - constants + - association + - macros + - public_class_methods + - initializer + - public_attribute_macros + - public_delegate + - public_methods + - protected_attribute_macros + - protected_methods + - private_attribute_macros + - private_delegate + - private_methods +Layout/ClosingParenthesisIndentation: + Enabled: true +Layout/CommentIndentation: + Enabled: true +Layout/ConditionPosition: + Enabled: true +Layout/DotPosition: + Enabled: true + EnforcedStyle: trailing +Layout/ElseAlignment: + Enabled: false +Layout/EmptyComment: + Enabled: true + AllowBorderComment: true + AllowMarginComment: true +Layout/EmptyLines: + Enabled: false +Layout/EmptyLineAfterMagicComment: + Enabled: true +Layout/EmptyLineBetweenDefs: + Enabled: true + AllowAdjacentOneLineDefs: true + NumberOfEmptyLines: 2 +Layout/EmptyLinesAroundArguments: + Enabled: true +Layout/EmptyLinesAroundAttributeAccessor: + Enabled: true + AllowAliasSyntax: true +Layout/EndOfLine: + Enabled: true + EnforcedStyle: lf +Layout/ExtraSpacing: + Enabled: true + AllowForAlignment: true + AllowBeforeTrailingComments: true +Layout/HashAlignment: + Enabled: false # not configurable enough + EnforcedHashRocketStyle: table + EnforcedColonStyle: table +Layout/HeredocIndentation: + Enabled: true +Layout/IndentationConsistency: + Enabled: true +Layout/IndentationStyle: + Enabled: true + EnforcedStyle: tabs +Layout/IndentationWidth: + Enabled: true + Width: 1 +Layout/InitialIndentation: + Enabled: true +Layout/LeadingCommentSpace: + Enabled: true +Layout/LineLength: + Max: 100 +Layout/MultilineArrayBraceLayout: + Enabled: true + EnforcedStyle: symmetrical +Layout/MultilineArrayLineBreaks: + Enabled: true +Layout/MultilineAssignmentLayout: + Enabled: true + EnforcedStyle: same_line +Layout/MultilineHashBraceLayout: + Enabled: true + EnforcedStyle: symmetrical +Layout/MultilineHashKeyLineBreaks: + Enabled: true +Layout/MultilineMethodArgumentLineBreaks: + Enabled: false +Layout/MultilineMethodCallIndentation: + Enabled: true + EnforcedStyle: indented +Layout/RescueEnsureAlignment: + Enabled: true +Layout/SpaceAfterColon: + Enabled: true +Layout/SpaceAfterComma: + Enabled: true +Layout/SpaceAfterMethodName: + Enabled: true +Layout/SpaceAfterNot: + Enabled: false +Layout/SpaceAroundBlockParameters: + Enabled: true + EnforcedStyleInsidePipes: no_space +Layout/SpaceAroundEqualsInParameterDefault: + Enabled: true + EnforcedStyle: no_space +Layout/SpaceAroundKeyword: + Enabled: true +Layout/SpaceAroundMethodCallOperator: + Enabled: true +Layout/SpaceBeforeBlockBraces: + Enabled: true + EnforcedStyle: no_space +Layout/SpaceBeforeComma: + Enabled: true +Layout/SpaceBeforeComment: + Enabled: true +Layout/SpaceBeforeFirstArg: + Enabled: true +Layout/SpaceBeforeSemicolon: + Enabled: true +Layout/SpaceInLambdaLiteral: + Enabled: true + EnforcedStyle: require_no_space +Layout/SpaceInsideArrayLiteralBrackets: + Enabled: false # not configurable enough + EnforcedStyle: space +Layout/SpaceInsideBlockBraces: + Enabled: true + SpaceBeforeBlockParameters: false +Layout/SpaceInsideHashLiteralBraces: + Enabled: false # not configurable enough + EnforcedStyle: space +Layout/SpaceInsideParens: + Enabled: true + EnforcedStyle: space +Layout/SpaceInsideReferenceBrackets: + Enabled: true + EnforcedStyle: space +Layout/TrailingEmptyLines: + Enabled: true + EnforcedStyle: final_blank_line +Layout/TrailingWhitespace: + Enabled: true + +# https://docs.rubocop.org/rubocop/0.93/cops_lint.html +# Enabling everything, these are warnings, and seem generally reasonable. +# +Lint: + Enabled: true + +# https://docs.rubocop.org/rubocop/0.93/cops_naming.html +# +Naming/AccessorMethodName: + Enabled: true +Naming/BinaryOperatorParameterName: + Enabled: true +Naming/ClassAndModuleCamelCase: + Enabled: true +Naming/ConstantName: + Enabled: true +Naming/FileName: + Enabled: true +Naming/HeredocDelimiterCase: + Enabled: true +Naming/MemoizedInstanceVariableName: + Enabled: true + EnforcedStyleForLeadingUnderscores: optional +Naming/MethodName: + Enabled: true +Naming/PredicateName: + Enabled: true +Naming/VariableName: + Enabled: true + EnforcedStyle: snake_case + + +# https://docs.rubocop.org/rubocop/0.93/cops_style.html +# +Style/Documentation: + Enabled: true +Style/AccessorGrouping: + Enabled: true + EnforcedStyle: separated +Style/AndOr: + Enabled: true +Style/Attr: + Enabled: true +Style/AutoResourceCleanup: + Enabled: true +Style/BisectedAttrAccessor: + Enabled: true +Style/BlockDelimiters: + Enabled: true +Style/ClassMethods: + Enabled: true +Style/ClassMethodsDefinitions: + Enabled: true + EnforcedStyle: def_self +Style/DocumentationMethod: + Enabled: true +Style/EachForSimpleLoop: + Enabled: true +Style/EmptyBlockParameter: + Enabled: true +Style/For: + Enabled: true + EnforcedStyle: each +Style/GlobalVars: + Enabled: true +Style/HashSyntax: + Enabled: true + EnforcedStyle: no_mixed_keys +Style/IfWithSemicolon: + Enabled: true +Style/ImplicitRuntimeError: + Enabled: true +Style/MethodCallWithoutArgsParentheses: + Enabled: true +Style/MethodDefParentheses: + Enabled: true +Style/NegatedIf: + Enabled: true +Style/NegatedUnless: + Enabled: true +Style/NestedModifier: + Enabled: true +Style/Next: + Enabled: true +Style/NilComparison: + Enabled: true +Style/NonNilCheck: + Enabled: true +Style/Not: + Enabled: true +Style/OrAssignment: + Enabled: true +Style/RedundantConditional: + Enabled: true +Style/RedundantFileExtensionInRequire: + Enabled: true +Style/ReturnNil: + Enabled: true +Style/TrailingCommaInArguments: + Enabled: true + EnforcedStyleForMultiline: no_comma +Style/TrailingCommaInArrayLiteral: + Enabled: true + EnforcedStyleForMultiline: no_comma +Style/TrailingCommaInBlockArgs: + Enabled: true +Style/TrailingCommaInHashLiteral: + Enabled: true + EnforcedStyleForMultiline: no_comma +Style/TrivialAccessors: + Enabled: true +Style/UnlessElse: + Enabled: true + diff --git a/.ruby-version b/.ruby-version index 860487c..37c2961 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.1 +2.7.2 diff --git a/ext/mdbx_ext/database.c b/ext/mdbx_ext/database.c new file mode 100644 index 0000000..76f83fa --- /dev/null +++ b/ext/mdbx_ext/database.c @@ -0,0 +1,206 @@ +/* vim: set noet sta sw=4 ts=4 : */ + +#include "mdbx_ext.h" + +/* VALUE str = rb_sprintf( "path: %+"PRIsVALUE", opts: %+"PRIsVALUE, path, opts ); */ +/* printf( "%s\n", StringValueCStr(str) ); */ + + +/* Shortcut for fetching current DB variables. + * + */ +#define UNWRAP_DB( val, db ) \ + rmdbx_db_t *db; \ + TypedData_Get_Struct( val, rmdbx_db_t, &rmdbx_db_data, db ); + + +/* + * A struct encapsulating an instance's DB state. + */ +struct rmdbx_db { + MDBX_env *env; + MDBX_dbi dbi; + MDBX_txn *txn; + MDBX_cursor *cursor; + int open; +}; +typedef struct rmdbx_db rmdbx_db_t; + + +/* + * Ruby allocation hook. + */ +void rmdbx_free( void *db ); /* forward declaration */ +static const rb_data_type_t rmdbx_db_data = { + .wrap_struct_name = "MDBX::Database::Data", + .function = { .dfree = rmdbx_free }, + .flags = RUBY_TYPED_FREE_IMMEDIATELY +}; + + +/* + * Allocate a DB environment onto the stack. + */ +VALUE +rmdbx_alloc( VALUE klass ) +{ + int *data = malloc( sizeof(rmdbx_db_t) ); + return TypedData_Wrap_Struct( klass, &rmdbx_db_data, data ); +} + + +/* + * Cleanup a previously allocated DB environment. + * FIXME: ... this should also close if not already closed? + */ +void +rmdbx_free( void *db ) +{ + if ( db ) free( db ); +} + + +/* + * Cleanly close an opened database. + */ +VALUE +rmdbx_close( VALUE self ) +{ + UNWRAP_DB( self, db ); + if ( db->cursor ) mdbx_cursor_close( db->cursor ); + if ( db->txn ) mdbx_txn_abort( db->txn ); + if ( db->dbi ) mdbx_dbi_close( db->env, db->dbi ); + if ( db->env ) mdbx_env_close( db->env ); + db->open = 0; + + // FIXME: or rather, maybe free() from here? + + return Qtrue; +} + + +/* + * call-seq: + * db.closed? #=> false + * + * Predicate: return true if the database has been closed, + * (or never was actually opened for some reason?) + */ +VALUE +rmdbx_closed_p( VALUE self ) +{ + UNWRAP_DB( self, db ); + return db->open == 1 ? Qfalse : Qtrue; +} + + +/* + * call-seq: + * MDBX::Database.open( path ) -> db + * MDBX::Database.open( path, options ) -> db + * MDBX::Database.open( path, options ) do |db| + * db... + * end + * + * Open an existing (or create a new) mdbx database at filesystem + * +path+. In block form, the database is automatically closed. + * + */ +VALUE +rmdbx_database_initialize( int argc, VALUE *argv, VALUE self ) +{ + int rc = 0; + int mode = 0644; + int db_flags = MDBX_ENV_DEFAULTS; + VALUE path, opts, opt; + + rb_scan_args( argc, argv, "11", &path, &opts ); + + /* Ensure options is a hash if it was passed in. + */ + if ( NIL_P(opts) ) { + opts = rb_hash_new(); + } + else { + Check_Type( opts, T_HASH ); + } + rb_hash_freeze( opts ); + + /* Options setup, overrides. + */ + opt = rb_hash_aref( opts, ID2SYM( rb_intern("mode") ) ); + if ( ! NIL_P(opt) ) mode = FIX2INT( opt ); + opt = rb_hash_aref( opts, ID2SYM( rb_intern("nosubdir") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_NOSUBDIR; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("readonly") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_RDONLY; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("exclusive") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_EXCLUSIVE; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("compat") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_ACCEDE; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("writemap") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_WRITEMAP; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_threadlocal") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_NOTLS; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_readahead") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_NORDAHEAD; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_memory_init") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_NOMEMINIT; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("coalesce") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_COALESCE; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("lifo_reclaim") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_LIFORECLAIM; + opt = rb_hash_aref( opts, ID2SYM( rb_intern("no_metasync") ) ); + if ( RTEST(opt) ) db_flags = db_flags | MDBX_NOMETASYNC; + + + /* Initialize the DB vals. + */ + UNWRAP_DB( self, db ); + db->env = NULL; + db->dbi = 0; + db->txn = NULL; + db->cursor = NULL; + db->open = 0; + + /* Allocate an mdbx environment. + */ + rc = mdbx_env_create( &db->env ); + if ( rc != MDBX_SUCCESS ) + rb_raise( rmdbx_eDatabaseError, "mdbx_env_create: (%d) %s", rc, mdbx_strerror(rc) ); + + /* Open the DB handle on disk. + */ + rc = mdbx_env_open( db->env, StringValueCStr(path), db_flags, mode ); + if ( rc != MDBX_SUCCESS ) { + rmdbx_close( self ); + rb_raise( rmdbx_eDatabaseError, "mdbx_env_open: (%d) %s", rc, mdbx_strerror(rc) ); + } + + /* Set instance variables. + */ + db->open = 1; + rb_iv_set( self, "@path", path ); + rb_iv_set( self, "@options", opts ); + + return self; +} + + +/* + * Initialization for the MDBX::Database class. + */ +void +rmdbx_init_database() +{ + rmdbx_cDatabase = rb_define_class_under( rmdbx_mMDBX, "Database", rb_cData ); + + rb_define_alloc_func( rmdbx_cDatabase, rmdbx_alloc ); + + rb_define_method( rmdbx_cDatabase, "close", rmdbx_close, 0 ); + rb_define_method( rmdbx_cDatabase, "closed?", rmdbx_closed_p, 0 ); + rb_define_protected_method( rmdbx_cDatabase, "initialize", rmdbx_database_initialize, -1 ); + + rb_require( "mdbx/database" ); +} + diff --git a/ext/mdbx_ext/mdbx_ext.c b/ext/mdbx_ext/mdbx_ext.c index 8a4ead8..574ad90 100644 --- a/ext/mdbx_ext/mdbx_ext.c +++ b/ext/mdbx_ext/mdbx_ext.c @@ -9,7 +9,14 @@ void Init_mdbx_ext() { - mdbx_mMDBX = rb_define_module( "MDBX" ); - rb_define_const( mdbx_mMDBX, "LIBRARY_VERSION", rb_str_new_cstr(mdbx_version.git.describe) ); + rmdbx_mMDBX = rb_define_module( "MDBX" ); + + /* The backend library version. */ + VALUE version = rb_str_new_cstr( mdbx_version.git.describe ); + rb_define_const( rmdbx_mMDBX, "LIBRARY_VERSION", version ); + + rmdbx_eDatabaseError = rb_define_class_under( rmdbx_mMDBX, "DatabaseError", rb_eRuntimeError ); + + rmdbx_init_database(); } diff --git a/ext/mdbx_ext/mdbx_ext.h b/ext/mdbx_ext/mdbx_ext.h index 07ac287..3b5e0e4 100644 --- a/ext/mdbx_ext/mdbx_ext.h +++ b/ext/mdbx_ext/mdbx_ext.h @@ -4,10 +4,25 @@ #include "mdbx.h" +#ifndef MDBX_EXT_0_9_2 +#define MDBX_EXT_0_9_2 + /* ------------------------------------------------------------ - Globals - ------------------------------------------------------------ */ + * Globals + * ------------------------------------------------------------ */ -VALUE mdbx_mMDBX; +VALUE rmdbx_mMDBX; +VALUE rmdbx_cDatabase; +VALUE rmdbx_eDatabaseError; + + +/* ------------------------------------------------------------ + * Functions + * ------------------------------------------------------------ */ +extern void Init_rmdbx ( void ); +extern void rmdbx_init_database ( void ); + + +#endif /* define MDBX_EXT_0_9_2 */ diff --git a/lib/mdbx.rb b/lib/mdbx.rb index 705c83e..9cd51b6 100644 --- a/lib/mdbx.rb +++ b/lib/mdbx.rb @@ -10,9 +10,11 @@ require 'mdbx_ext' module MDBX # The version of this gem. + # # Note: the MDBX library version can be found in # the 'LIBRARY_VERSION' constant. + # VERSION = '0.0.1' -end +end # module MDBX diff --git a/lib/mdbx/database.rb b/lib/mdbx/database.rb new file mode 100644 index 0000000..806bfc4 --- /dev/null +++ b/lib/mdbx/database.rb @@ -0,0 +1,43 @@ +# -*- ruby -*- +# vim: set nosta noet ts=4 sw=4 ft=ruby: +# encoding: utf-8 + +require 'mdbx' unless defined?( MDBX ) + + +# TODO: rdoc +# +class MDBX::Database + + + ### Open an existing (or create a new) mdbx database at filesystem + ### +path+. In block form, the database is automatically closed. + ### + ### MDBX::Database.open( path ) -> db + ### MDBX::Database.open( path, options ) -> db + ### MDBX::Database.open( path, options ) do |db| + ### db[ 'key' ] #=> value + ### end + ### + def self::open( *args, &block ) + db = new( *args ) + return db unless block_given? + + begin + yield db + ensure + db.close + end + end + + # Only instantiate Database objects via #open. + private_class_method :new + + # The options used to instantiate this database. + attr_reader :options + + # The path on disk of the database. + attr_reader :path + +end # class MDBX::Database + diff --git a/spec/data/testdb/mdbx.dat b/spec/data/testdb/mdbx.dat new file mode 100644 index 0000000000000000000000000000000000000000..94a178e822003021973c6a492b9523ffc2a950ee GIT binary patch literal 65536 zcmeI5dz@TFmB*V5c_4uV2=az;i4a{F14b4UF=B+MSp|s;xUij`?w*+@J>5-rPngI< z9-}~rg2E!;E(C}ID*{SXlvltk&j0~gKtu@;P%%KjC_zE?oVs(sAKovK^6s&3CDld&|x-eksXG3TY5|GjwR#-k*YnVjj)bm`}f^nE6yd9H+f zcvF&Q{rMz?B{>~=jXpW#{u$GH`xm{_dNt8cE%=jh4`{8en>;H?y zQlm5iX#~;;q!CCXkVYVlKpKHG0%-)&2&556Bk=zs0@40o_vE(!AB%Q}`~UHhH^CnU zjPrKkem?wcJAT`*hxv9cfNJ0JZM6G;^G);9^wRzRTZS#oOe2s+AdNs8fiwbX1kwnk z5lADDMj(wq8iD`U5s3Ex`V8Lo|J;vr|F7GA&HJA}|BnK!`}V&^sBQmGKUFu~|Nk|j zn`)#HNF$I&AdNs8fiwbX1kwnk5lADDMj(yA-%2Ilx zX^?*Gq8PXPY*heq1MGy-V^(g>sxNF$I&AdNs8fiwbX1kwnk z5%^yff$8GSn9SHrW2m>#YrjNq8+ZGqck|f(h@R!UztpFHukX+A|2NJopQObenY29J z{aZZK*E9lY1kwnk5lADDMj(wq8i6zdX#~;;q!CCX@V_(y`nUNqnelpFAe;WZh`{52w3-7s2$OGy-V^(g>sxNF$I&AdNs8fiwbX1kwnk z5lAEOe=P#hU-z3J-y(>Ter+K9yLtcD+LZd6Mj(wq8i6zdX#~;;q!CCXkVYVlKpKHG z0%-)&2>hiH7%#^Fulqlxzq2o-e+A$s@IArDfp72mCh|G~JPZ3B!5iQ^fqx5px%>u$ z`Lo#nc7yuU4c;YXH7VZ`I@@?2`tuL4XL|>oN~H5y=${Fm1;0OmZ{vAb@3o$bMCMND z%zQ_b5!%o5T-!xIy^MTABa^v9{<+lywbNw$j4h+j(P>YMU%hF>Cq^%$_Ro0x$pN1m zeOV+kCE%BB6@hI7KK%9w>=^Ldyg+xrf93-=HQ*oe4o(aBZ?=oT9sxhnH&lN^Mw7+i z+RdZSGoqJK{!{PZz5##JkF%QR8rP#l%*Np&@VmjU0lx?QX7De9CykRT>^a`nf&U$J zcJ}^;v`y>|D`1N zax-+MsIVk`;ruWgob$=qo`?F^!=BgCBc7{UbEJ*dzdr}xO!ADs2%UFJA>%erhV|Yg zWi@HBdttv1;>mG!3OL8rcRdgFUrgX#KF<20mx}qhndj=yO8B!KIOAj2by)90DX&S3 z-3Oi19gX-*@b@S9`3JO@^&ahY!g`N^KF9Nyz~7X53;y%@34AH|4)WgOWA`0xmrf1F z*^lh^Unc1M-gAxf{fLA9PdS>Dv%QBn8pTcTp;qtV;M1h6@to(Xvoo$2>o4_xfACq} zUgNn5_$NJA`<>*+^grXd>Td!2yyu~g-8WT7uLaX-dizl4lmwl#JXf7v4_~5&ZwD=UVTFDB)*SX-d z|2J^jKkm8uvzPo>y-$Jf4Zbnj`)PS^_M3w1n!)&vo@>7_4!eQVAHS^0=s4uMcX#=$ zNp-eD9JDS?%Bk~NN8|H@UzR!h?V$6Iuzy5?Vg4Tnehau=FIq48EeZBdgHwMB;y@k7 zhxWY*_I1y-A9-Dz4E_;$Z|yoA`~y&w8H*XT84$XT9sd_eZ^x zv_X=>cI^aC{i)#8|9kKci=MUjU~u~X8F1?B8bg!5P(KfTfaIC}FgW#f?W##%sQ)E! z>YoSxAt__}PkA2p%L}k)y|03^-amn}-m!9xXwvKh<;UvPYe$oE>hB6p{da>?-~Z7( zr}IJ4GX0Nwdu(pCr3n9da1|jg!AoO=x>eo_JL0U=lKKY z;mcvaE$pubr~Mt^oaY|^r@h{TG-N`l4%2vwuf|{T{IY6zum)us%9h?^)3fzy?27M-uuDXk2X(+{b=Vf$~iu_lFypL>zenYZ{Ygn zxgW2ivmzNc9dY?-)AKQ#ANA~1lkpkR$IKu& z>(%#~RG;SzUw}R1witX!%2*t}2_44md$4D{KZZT)y$|-R_hIk_sP`4dLK;SPlB`F=fKZIy>Us(T>~R2jOT^m)L#PrWhrO<^&@cle-}7)9st*SpXvM>oH{%opbpOm7DGqR7&WOJ zb>0O|o$27Z#x{S>0H@AZ!Krg0_*bBFJvenX(hiXn`uV8@J`bGr_Jf}*dglL+z*+BI z;M92lT=)K_^DH>+*Mg70zH6*}kfhlh=LdKmws#IV^$!C-PqfURL2$O~cyQ{R3jVLq zxd5E@mw=xE`|o=mw(C{c)1R^9q_8AyFZCyapD%gVuHC??GXtDD9{~RvbdCV0eHQ#| z*nb|J_Fn}5D(o-vJd8sb^AFb{7l7-tMQiUR;2)Lu#;@{R^>>zQ$M|)y{~UCF08SmA zpHRonjkRC&?922YNYLTA3UyY){x{J1B{==zc?)$m!2TuZya`Snp2JXQvNlMPzPuto z)~;>9sl)Rb>P&W>cq>pi_g+ z=b>{PICV}*&^ZJ4Cqw6KaO&9oG;G%r*q;KOE5NDq9nV9Z8(@DbbZ!Nw&fN(*t6+Z` zbabz$NnfbLbAI-_pAOype+#Z(?)%6mJN4*(LB1*YGU(WOn#SQtamvQ|F5X`K)cNNq z|9Pr^Rr?=6XK&BVewP1yV8HdvR8xQS;@ZW1am91fG5fCv-1IL`;CBSv&gEBmuKx2m z^`n8k)%){+*Zk*S1>E9e>u}Y-0r7bQ_7}-}i?f{%XnXTAM;YJT&sVC$@vt@KG0ii3 zdk(8QIq2BBJ=Ebo{X*!>fWAJ9G=1*h>CYBYR+H-MUe)Y*9?SCw+XsdF#CJKBNT<_y zQ2kq^ai)JN?0J5xYb{M`zd8K*hNBUuKR1KZ{vL4J@9N_d`tx3J>dXYE&avQZ*V&$j z@v(C`t(WoHN;_PVa(=IX_aoMugFWj#4V>r9i@~Y$O>mvNtY2;e=kp-_ZjUDQlRCB^ zQOA4m=Nv?PM-sV5~8>eRLn$6-i;JMm= z81bxvUm|?}Ghd;CF%#fa|qv z`rik?0bHLEYYP4RJ?wuV`KIFxJJ`RV0+KWix`r|PS3M8)ufzPN-yJghUxKfI&dZqh zIDU4)Ifi~0&U6;x9E10lcTNx^N$Tf=@?-Yz^*r=*NGDWDYQIu`%>FdbL;IUxe;@4c z1-~Esp#+^R{f0{G)is0FyQAk}y;<1*1oou_`>SC80PL?%u;1Ko*wmkUV85;Bp+6VH z{$ALBBf)+>>~*hX?cHvoe2}DJ;`cKT@?8D7UVhB}5YIz@s<2-M`xC+S-eEdlO3+yb z&ikVjbxHkMEC1Y#@v0fdH}&QHa2w_8eEWU&DBsEh+ee$udOyDHzHfX7Z@;bcrd99O zo+sTG^qHe3jT^6*O%+HA`Mqf0OH#ST?WyrCd-BV1eXWE2&KUpXyI~yCpXnGs{{o#e z!CCLHZ@2nSzRRS@&)B%~T#T<>MZ7a^W}E_@&O zJlJml|7Y-Nh%@815d1~hpXTEq#^*oLFZ90`@xN5sZvNN6>HlxR8RtKEF0N!AmLJpE z$oHdiJwq^V*OP{i`;Mf2{i|Z9N$u(Xc&x9;Pfy_dz69;{ce^yHKKqf+{b~P?uqW5w z_0Xg`jOX^?jHi9SRPA}aJPLb`!~JkQQfD?e$JK2K= z2uKR;Zv$t$cHXLG&vrcr&Un6-z<2i(XjtzH;2fXpJrBp{AHg|3$8O#7ljCzD`knFL z+w)N0&f%3)|0B?${&rEdE}lHEm<@g{uG10lV@1=huWx~W8}_$@^V}opx|;)gesAYA zaQ5%T;Ot*r8)(x0;_naI??ozSJhQld$p;WO@>3JIez#eZ>eEl%t7;1CeKLVBR6}q7 zd(pHwUy;CHPT)D*f9d~83H*7ihiN|^_Y3lc;B4<5;I#h?<_(@>ALV&?A2|#5yuOmg z?e0<7T%0)$4+Q6UwsR^CAIInKyuEfO_wTQR^L$_vKYrAn`}%{xc|M?LahgK?dEnF; z1n2&JY9td|_wxE>zntyuwcbC-KQ^v(PS&Jx;Ps;S6iv$6k1v6r51nypC`n(~zgL6n zy4UJ`D1mR~$EUibXP#!iJ;p8fcPGGJ+qI*V(R4Y+Gvlx_VZ2SmI)~#eQj7ZQHPOGz z1Mj};N99XA_wu9sC!YJ|$tX7+uG>}TXWri5E=KL2^qlKo8_@kch5%5L6q0jEuc`zWhx8s{QbQ za2qF0@Yki$#us_6aZsHB|C#p@>f8>UH=wf$d^6PhOaj-lJ5A~*$JK?7Mx6SWCGc%L zSO0lVSW4hL$L9I-xzOkLEiZzOJ_EA2T?)QN-W#`bRBhM05g!}x>ZiuU>>ro+nv_2+ zKgOR`C`mc%UF&GXpOW`x|Ayyby&h*Y+#5;1Sn|Vel-qc+>(1=|*%z?u%lH|dcl%f4 z*Z9xV0{)2qY}bX^+kL{uzwzn%M$%08Z}eZjs{i^-RMYI}Wz_%kJwGDg_WhN40pCRf zD`_Bl8P&1xG1LOS+}jTae9*`Al4~RlFo@$Tfg79E8ti7eZ{JPzvlVsfWO^O*uM(+bkElX{7BC? z1pHLbUBDzygR6#N0N5;YhF`7&(wb;DYw7j zpr7aIKa!OH5%wdFM%;dPOwm&CH(+mZR-HeA^Llrc`11Dq$ZEfl{^QfWzO14#;05q5 z@DcEh!R>pwipGNT_lU=VTUkZp!KeE7`Z5807WgLM{65m#z?Z;&Q*d4LXwnzk8|vo` z;G4mo&k;5UpX;wzMU%isz$b(2?=Wgov;{c7hrcDbyU}@jd)BYzr}_CnP3?CEmCpg+ z8agL{za8AxlZx~?sintYzb*I#KTjyy4t!tm?ZJ-#-vN9Cd`ECTr`QR6HSBE;R$nLk z^^Tf$!+s{Xoe`;h0o=xz^7Fvo37zHOyMeC*-yOW$@7EMf13v`(UEn8!>wVwSmEiWf z?V7g|d=J>a4!$S&vIU0el(wzToS@_XD5e&)F2cAADc%4}cfI_Xl4LZfB1ghvne*-3I0B zz&`|?$vBUl3BCuo?td-K1wRmc1l;CKEwc>#qp)8CuIpP%Qzl056depc3w$SHRbUSHY+Hb9hCzM$$f;3$EwSmQDb#gI^8a0Dl79 z{#Jl$P4w$-H64QezTgYM=Ybyweg^n3_?6%b!B>HQ4txXn@!;M5d4Zx6z-NM=2wnp} z3496o=fRhQpA5bZ{1osh{)=~tP6eLmAQehdv;=$>_@&@Y@XNrLf?p240(>d>8t`v`Pxc46ioOXx6Z{JBdEi%qpA3E# z_!98{0ACLNE%4RgSA)AhCg|h#ZSZdYU{BFC;B&#R1s?&w4ty#2cfePGe;0fW`1Rl$ z?<|f;QnU;4@z!!tx0Dd+255ZS~{|I~?_>JJ*yEylq|2Kip0lyi% z34ROsQt(^B9|m6zzHxV}-rK?V2mdkn0Qd^<3&8&qdOl z5BLc9z2M8hSAstQejoS-@cY51{awqS2f&X2Uj;q_{vi0(;17X60sa&4iMzJyeHeUy z@c#lI0DlDhJn%=smxDhBz7G7S;FG7e{COOFCiu_53*b+HkAVLid>QyJz#jvD5_|*r zYVhuNw)}hwd^Y&g;KSh0fG-7K1AZ6yv*7E%p97z`Tg#tcg6|LhEAV;XzXrbm{CV&d z;J*Q1555+B>h3LnUId>D{u1~I_&V@q;4g!(0e=O2<7usWe+xbpd_DLf;J*Vu8T|L) zSAxF^z6yK;_&V^{z`Nho^7D`2v%y~n9{_&?{2cH(;jXn~ z){S|2%!^|dcRk+q^{z|Cz0!5@_%p|^?Ru%}`K}kcUK{gr*L7Xr>B{ufi{)HTvpC%B z87%Y;XG@K2p;T`!jAaxn3$l%3GuvOR4i=krF_@Fh_6=3?*=$rYZV*VTL35~H$@W$2 z*?KWwT~JJr>0s4Ct6D1tT8-*ZJ?}(2nCGjtg_%aPo|l)w;-J3NngjJ>u8O0(Estar>WREK)Y z#g18}N^_qXNjdM`8#zIp2T!pO7}tiii!Ep+>QuZw@n9`JsA428oWh(cGZ!=5VDwzg@VWMO#^3 zxGlGXF!QDR%cVv$8YCGPM>k$#SBMrWN;3jI*UYAjiVk^&-cq)&oa=9N%G83TN?)~8 zp0>WX*k7u2%2ZW%adfCrMb**4+^`JLM(K0KP71JEkSo8HRlZzpL=qjQgH~n`lX|gM z&gB!UZY-2(rGxV_9)@aJnWkcs4|0O4%B8_lv!O%ZHAC7R>mhFlbhCJI^Mn__mX&Cu9B-S%+`kDS)G_wt>(eFVph=AKC7=(Z%DaT!#%Ic z-6F~g%IQ5oW@Wv~Sxly`LbjZ%_se}D)*zQzE$cla=3-l3P?z_moNKSE-7T6q^NwcvoIp_HKH8O3y3^b%*J61;= z4noI?xzMaixnNkt4LT|KxhGkn%^*dL5iE zrvV*1MYq%95AzDezT8l`nUz)YP%$ejMY%J}9-k)B?IqfE4^`^2gOTlHqD6APjAHRM z*WX{3(oVEc>>cWtE3{uW*zC8_xda>Vm`8dEW(V9g9q&8jWwXt4!|k7Pg+knI3tzUb z2Mw|WlQ=e-vbL<(VpSIERe`}caZqkKn$_w&y{)$s*J2%w(mGinYHbyL)pL#Lqbv-I zqdERPMsq>)P{pXAE(%>vK*3?pF}6tx5x>qkCupX1<9hE9qD=_MqLoymCGzHf0!v-xnj@3pZv_8?kc6a z({Xd5)ZbR`XpT%NvUiswxl*h(T9DbTA!^s9777}wgHrEnwOn%`)8~${Vm8kC&RJf$ zSsls`bS~$!f{DdRb}m(@$)ScjNAQA-V=S}PNGI7=u$AX#pSQ}B)kU_qo~v|dMKnm< zYA4=8Iakg0%3LdxYbk##OEKjZP?yV!2J%hD)ih4ZAVuBB{}m z4VB~u-5TU3j5XPL$TCV#ZDXx*6TA#ZS-N!07cQ$@S2Cy?IYG63rLr#)WJEoy7`tihcAqWbB$SnYxSZ$v6YJ@Di>tBCh6mxSRAZHQDzW{_RK(2mn-fmP|V9#&S0*v zpwyvO*N%FzS*pi9lCF(Tv?MRO0n1%O&nsfSw;fpe+~R@zX%e1H0r?=x&uPb(B zwI*{HHW_Ae7^>URj(blz