From 967c0855a54ae1979b45eadfe7806b26a8741e93 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Mon, 18 May 2026 11:33:11 +0300 Subject: [PATCH] Bug fixes: composite PK, nl_to_sql sandbox, FK check, SQL injection, storage correctness Critical fixes: - Composite PK: execInsert + validateConstraints use all PK columns - nl_to_sql: non-SELECT SQL no longer executed directly during validation - FK check: removed O(N) scanMemTable fallback, uses db.get() with SSTables - exprToSql: nkIdent wrapped in quotes to prevent SQL injection - restoreSchema: try/except around tokenize/parse for crash resilience - recovery.nim: lastTxnId tracking + putUnsafe/deleteUnsafe without WAL - SCRAM: verifyClientProof length check + DefaultIterationCount restored Storage fixes: - lsm.nim: SSTable sort order fixed (ascending), close() flushes all memtables - compaction.nim: tombstones preserved during compaction - wal.nim: header written for empty existing files, readEntries checks magic - btree.nim: B+ tree leaf split keeps boundary key - bloom.nim: deserialize raises on short data - mmap.nim: bounds checks for adviseWillNeed/DontNeed + posix.close() Protocol fixes: - zerocopy.nim: readString bounds check - wire.nim: deserializeValue 32-bit underflow check - auth.nim: JWT JSON escaping for claims - server.nim: readUint32BE bounds check + specific exception handling - raft.nim: readData checks + specific exception handling Tests: - Added Composite Primary Key test suite (4 tests) Build: 0 warnings, 0 errors --- src/barabadb/client/client.nim | 4 +- src/barabadb/core/backup.nim | 4 +- src/barabadb/core/deadlock.nim | 46 ++++++-- src/barabadb/core/disttxn.nim | 123 ++++++++++++-------- src/barabadb/core/raft.nim | 8 +- src/barabadb/core/server.nim | 18 ++- src/barabadb/fts/engine | Bin 0 -> 73016 bytes src/barabadb/protocol/auth.nim | 7 +- src/barabadb/protocol/scram.nim | 16 ++- src/barabadb/protocol/wire.nim | 6 +- src/barabadb/protocol/zerocopy.nim | 2 + src/barabadb/query/executor.nim | 168 ++++++++++++++++++---------- src/barabadb/query/parser.nim | 9 +- src/barabadb/storage/bloom.nim | 2 +- src/barabadb/storage/btree | Bin 0 -> 73016 bytes src/barabadb/storage/btree.nim | 7 +- src/barabadb/storage/compaction.nim | 5 +- src/barabadb/storage/lsm.nim | 23 +++- src/barabadb/storage/mmap.nim | 15 ++- src/barabadb/storage/recovery.nim | 13 +-- src/barabadb/storage/wal.nim | 18 +-- tests/fuzz_test | Bin 0 -> 728680 bytes tests/nimforum_smoke_test | Bin 0 -> 589880 bytes tests/prop_test | Bin 0 -> 2974144 bytes tests/test_all.nim | 53 +++++++++ 25 files changed, 376 insertions(+), 171 deletions(-) create mode 100755 src/barabadb/fts/engine create mode 100755 src/barabadb/storage/btree create mode 100755 tests/fuzz_test create mode 100755 tests/nimforum_smoke_test create mode 100755 tests/prop_test diff --git a/src/barabadb/client/client.nim b/src/barabadb/client/client.nim index c55065f..c9bc642 100644 --- a/src/barabadb/client/client.nim +++ b/src/barabadb/client/client.nim @@ -109,10 +109,10 @@ proc disconnect*(client: SyncClient) = try: let msg = makeQueryMessage(0, "DISCONNECT") netmod.send(client.socket, cast[string](msg)) - except: discard + except IOError, OSError: discard netmod.close(client.socket) client.connected = false - deinitLock(client.lock) + deinitLock(client.lock) proc query*(client: SyncClient, sql: string): QueryResult = acquire(client.lock) diff --git a/src/barabadb/core/backup.nim b/src/barabadb/core/backup.nim index 25a4d76..1d01068 100644 --- a/src/barabadb/core/backup.nim +++ b/src/barabadb/core/backup.nim @@ -177,7 +177,7 @@ proc logRestore*(archive: string, dataDir: string, success: bool, dryRun: bool = let f = open(logPath, fmAppend) f.write(entry) f.close() - except: + except IOError: discard # Silently fail if log cannot be written proc readHistory*(): seq[string] = @@ -191,7 +191,7 @@ proc readHistory*(): seq[string] = for line in splitLines(content): if line.len > 0: result.add(line) - except: + except IOError: discard proc backupDataDir*(dataDir: string, output: string, excludes: seq[string] = @[], compression: int = DEFAULT_COMPRESSION, verbose: bool = false): bool = diff --git a/src/barabadb/core/deadlock.nim b/src/barabadb/core/deadlock.nim index 46b3314..0e24c47 100644 --- a/src/barabadb/core/deadlock.nim +++ b/src/barabadb/core/deadlock.nim @@ -2,6 +2,7 @@ import std/tables import std/sets import std/algorithm +import std/locks type WaitEdge* = object @@ -12,15 +13,18 @@ type edges: seq[WaitEdge] adjacency: Table[uint64, seq[uint64]] # waiter -> holders txnIds: HashSet[uint64] + lock: Lock proc newDeadlockDetector*(): DeadlockDetector = - DeadlockDetector( - edges: @[], - adjacency: initTable[uint64, seq[uint64]](), - txnIds: initHashSet[uint64](), - ) + new(result) + initLock(result.lock) + result.edges = @[] + result.adjacency = initTable[uint64, seq[uint64]]() + result.txnIds = initHashSet[uint64]() proc addWait*(dd: DeadlockDetector, waiter, holder: uint64) = + acquire(dd.lock) + defer: release(dd.lock) dd.edges.add(WaitEdge(waiter: waiter, holder: holder)) dd.txnIds.incl(waiter) dd.txnIds.incl(holder) @@ -29,6 +33,8 @@ proc addWait*(dd: DeadlockDetector, waiter, holder: uint64) = dd.adjacency[waiter].add(holder) proc removeWait*(dd: DeadlockDetector, waiter, holder: uint64) = + acquire(dd.lock) + defer: release(dd.lock) var newEdges: seq[WaitEdge] = @[] for edge in dd.edges: if edge.waiter != waiter or edge.holder != holder: @@ -43,6 +49,8 @@ proc removeWait*(dd: DeadlockDetector, waiter, holder: uint64) = dd.adjacency[waiter] = newAdj proc removeTxn*(dd: DeadlockDetector, txnId: uint64) = + acquire(dd.lock) + defer: release(dd.lock) dd.txnIds.excl(txnId) dd.adjacency.del(txnId) var newEdges: seq[WaitEdge] = @[] @@ -57,7 +65,7 @@ proc removeTxn*(dd: DeadlockDetector, txnId: uint64) = newH.add(h) holders = newH -proc detectCycle*(dd: DeadlockDetector): seq[uint64] = +proc detectCycleUnsafe(dd: DeadlockDetector): seq[uint64] = var visited = initHashSet[uint64]() var inStack = initHashSet[uint64]() var parent = initTable[uint64, uint64]() @@ -98,8 +106,15 @@ proc detectCycle*(dd: DeadlockDetector): seq[uint64] = return cycle return @[] +proc detectCycle*(dd: DeadlockDetector): seq[uint64] = + acquire(dd.lock) + defer: release(dd.lock) + detectCycleUnsafe(dd) + proc findDeadlockVictim*(dd: DeadlockDetector): uint64 = - let cycle = dd.detectCycle() + acquire(dd.lock) + defer: release(dd.lock) + let cycle = detectCycleUnsafe(dd) if cycle.len == 0: return 0 # Choose youngest txn (highest id) as victim @@ -109,12 +124,23 @@ proc findDeadlockVictim*(dd: DeadlockDetector): uint64 = result = id proc hasDeadlock*(dd: DeadlockDetector): bool = - return dd.detectCycle().len > 0 + acquire(dd.lock) + defer: release(dd.lock) + return detectCycleUnsafe(dd).len > 0 proc clear*(dd: DeadlockDetector) = + acquire(dd.lock) + defer: release(dd.lock) dd.edges.setLen(0) dd.adjacency.clear() dd.txnIds.clear() -proc edgeCount*(dd: DeadlockDetector): int = dd.edges.len -proc txnCount*(dd: DeadlockDetector): int = dd.txnIds.len +proc edgeCount*(dd: DeadlockDetector): int = + acquire(dd.lock) + defer: release(dd.lock) + dd.edges.len + +proc txnCount*(dd: DeadlockDetector): int = + acquire(dd.lock) + defer: release(dd.lock) + dd.txnIds.len diff --git a/src/barabadb/core/disttxn.nim b/src/barabadb/core/disttxn.nim index 5244c5e..11e416e 100644 --- a/src/barabadb/core/disttxn.nim +++ b/src/barabadb/core/disttxn.nim @@ -108,86 +108,119 @@ proc sendDistTxnRpc(host: string, port: int, txnId: uint64, action: string, time except CatchableError: return false +type + ParticipantInfo = object + nodeId: string + host: string + port: int + proc prepare*(txn: DistributedTransaction): bool = + # Phase 1: validate state and collect participants while holding lock + var participants: seq[ParticipantInfo] = @[] + var wasActive = false acquire(txn.lock) - if txn.state != dtsActive: - release(txn.lock) + if txn.state == dtsActive: + txn.state = dtsPreparing + wasActive = true + for nodeId, participant in txn.participants: + if participant.host.len > 0 and participant.port > 0: + participants.add(ParticipantInfo(nodeId: nodeId, host: participant.host, port: participant.port)) + release(txn.lock) + + if not wasActive: return false - txn.state = dtsPreparing - - var allOk = true + # Phase 2: perform network I/O without holding the lock var preparedNodes: seq[string] = @[] - for nodeId, participant in txn.participants.mpairs: - if participant.host.len > 0 and participant.port > 0: - participant.prepared = sendDistTxnRpc(participant.host, participant.port, txn.id, "PREPARE") - else: - participant.prepared = true # local participant - if participant.prepared: - preparedNodes.add(nodeId) + var allOk = true + for p in participants: + let ok = sendDistTxnRpc(p.host, p.port, txn.id, "PREPARE") + if ok: + preparedNodes.add(p.nodeId) else: allOk = false + # Phase 3: update state while holding lock + acquire(txn.lock) if allOk: txn.state = dtsPrepared + for nodeId, _ in txn.participants.mpairs: + txn.participants[nodeId].prepared = true else: - # Rollback already-prepared participants to maintain atomicity + # Rollback already-prepared participants for nodeId in preparedNodes: - if txn.participants[nodeId].host.len > 0 and txn.participants[nodeId].port > 0: + if txn.participants.hasKey(nodeId): discard sendDistTxnRpc(txn.participants[nodeId].host, txn.participants[nodeId].port, txn.id, "ROLLBACK") - txn.participants[nodeId].prepared = false - txn.participants[nodeId].aborted = true + txn.participants[nodeId].prepared = false + txn.participants[nodeId].aborted = true txn.state = dtsAborted - release(txn.lock) return allOk proc commit*(txn: DistributedTransaction): bool = + # Phase 1: validate state and collect participants while holding lock + var participants: seq[ParticipantInfo] = @[] + var wasPrepared = false acquire(txn.lock) - if txn.state != dtsPrepared: - release(txn.lock) + if txn.state == dtsPrepared: + txn.state = dtsCommitting + wasPrepared = true + for nodeId, participant in txn.participants: + if participant.host.len > 0 and participant.port > 0: + participants.add(ParticipantInfo(nodeId: nodeId, host: participant.host, port: participant.port)) + release(txn.lock) + + if not wasPrepared: return false - txn.state = dtsCommitting - - var allOk = true + # Phase 2: perform network I/O without holding the lock var committedNodes: seq[string] = @[] - for nodeId, participant in txn.participants.mpairs: - if participant.host.len > 0 and participant.port > 0: - participant.committed = sendDistTxnRpc(participant.host, participant.port, txn.id, "COMMIT") - else: - participant.committed = true # local participant - if participant.committed: - committedNodes.add(nodeId) + var allOk = true + for p in participants: + let ok = sendDistTxnRpc(p.host, p.port, txn.id, "COMMIT") + if ok: + committedNodes.add(p.nodeId) else: allOk = false + # Phase 3: update state while holding lock + acquire(txn.lock) if allOk: txn.state = dtsCommitted + for nodeId, _ in txn.participants.mpairs: + txn.participants[nodeId].committed = true + elif committedNodes.len > 0: + txn.state = dtsCommitted + for nodeId in committedNodes: + if txn.participants.hasKey(nodeId): + txn.participants[nodeId].committed = true else: - if committedNodes.len > 0: - # Some participants already committed — cannot rollback committed nodes - # without violating atomicity. Mark as committed and rely on - # reconciliation/retry for the remaining participants. - txn.state = dtsCommitted - else: - # No participant committed yet — safe to abort - txn.state = dtsAborted + txn.state = dtsAborted release(txn.lock) return allOk or committedNodes.len > 0 proc rollback*(txn: DistributedTransaction): bool = + # Phase 1: validate state and collect participants while holding lock + var participants: seq[ParticipantInfo] = @[] + var canRollback = false acquire(txn.lock) - if txn.state notin {dtsActive, dtsPreparing, dtsPrepared}: - release(txn.lock) + if txn.state in {dtsActive, dtsPreparing, dtsPrepared}: + txn.state = dtsAborting + canRollback = true + for nodeId, participant in txn.participants: + if participant.host.len > 0 and participant.port > 0: + participants.add(ParticipantInfo(nodeId: nodeId, host: participant.host, port: participant.port)) + release(txn.lock) + + if not canRollback: return false - txn.state = dtsAborting - for nodeId, participant in txn.participants.mpairs: - if participant.host.len > 0 and participant.port > 0: - participant.aborted = sendDistTxnRpc(participant.host, participant.port, txn.id, "ROLLBACK") - else: - participant.aborted = true + # Phase 2: perform network I/O without holding the lock + for p in participants: + discard sendDistTxnRpc(p.host, p.port, txn.id, "ROLLBACK") + + # Phase 3: update state while holding lock + acquire(txn.lock) txn.state = dtsAborted release(txn.lock) return true diff --git a/src/barabadb/core/raft.nim b/src/barabadb/core/raft.nim index 87133c5..00e4aef 100644 --- a/src/barabadb/core/raft.nim +++ b/src/barabadb/core/raft.nim @@ -124,9 +124,10 @@ proc loadState(node: RaftNode) = let dataLen = int(s.readUint32()) var data = newSeq[byte](dataLen) if dataLen > 0: - discard s.readData(addr data[0], dataLen) + if s.readData(addr data[0], dataLen) != dataLen: + raise newException(IOError, "Incomplete Raft log data read") node.log[i] = LogEntry(term: term, index: index, command: cmd, data: data) - except: + except IOError, OSError: discard s.close() @@ -431,7 +432,8 @@ proc readString(s: Stream): string = let len = int(s.readUint32()) if len > 0: result = newString(len) - discard s.readData(result[0].addr, len) + if s.readData(result[0].addr, len) != len: + raise newException(IOError, "Incomplete string read from stream") else: result = "" diff --git a/src/barabadb/core/server.nim b/src/barabadb/core/server.nim index a461f21..444f600 100644 --- a/src/barabadb/core/server.nim +++ b/src/barabadb/core/server.nim @@ -99,6 +99,8 @@ proc newServer*(config: BaraConfig): Server = # ---------------------------------------------------------------------- proc readUint32BE(data: string, pos: int): uint32 = + if pos + 4 > data.len: + raise newException(ValueError, "readUint32BE: index out of bounds") var bytes: array[4, byte] for i in 0..3: bytes[i] = byte(data[pos + i]) @@ -108,13 +110,9 @@ proc parseHeader(data: string): (bool, MessageHeader) = if data.len < 12: return (false, MessageHeader()) let rawKind = readUint32BE(data, 0) - let kind = cast[MsgKind](rawKind) - case kind - of mkClientHandshake, mkQuery, mkQueryParams, mkExecute, mkBatch, mkTransaction, mkClose, mkPing, mkAuth, - mkServerHandshake, mkReady, mkData, mkComplete, mkError, mkAuthChallenge, mkAuthOk, mkSchemaChange, mkPong, mkTransactionState: - discard - else: + if rawKind < 0x01 or (rawKind > 0x09 and rawKind < 0x80) or rawKind > 0x89: return (false, MessageHeader()) + let kind = cast[MsgKind](rawKind) let length = readUint32BE(data, 4) let requestId = readUint32BE(data, 8) return (true, MessageHeader(kind: kind, length: length, requestId: requestId)) @@ -143,11 +141,11 @@ proc valueToWire(val: string, colType: string): WireValue = if t.startsWith("INT") or t == "SERIAL" or t == "BIGINT" or t == "SMALLINT" or t == "BIGSERIAL" or t == "SMALLSERIAL": try: return WireValue(kind: fkInt64, int64Val: parseInt(val)) - except: discard + except ValueError: discard elif t.startsWith("FLOAT") or t == "REAL" or t == "DOUBLE" or t == "NUMERIC" or t.startsWith("DOUBLE"): try: return WireValue(kind: fkFloat64, float64Val: parseFloat(val)) - except: discard + except ValueError: discard elif t == "BOOLEAN" or t == "BOOL": let lv = val.toLower() if lv in ["true", "t", "yes", "1"]: @@ -286,7 +284,7 @@ proc slowQueryLog(logPath: string, query: string, durationMs: int, clientId: int defer: f.close() let line = $getMonoTime().ticks() & " | " & $clientId & " | " & $durationMs & "ms | " & query & "\n" f.write(line) - except: discard + except IOError: discard proc verifyToken(secret, tokenStr: string): (bool, string, string) = try: @@ -296,7 +294,7 @@ proc verifyToken(secret, tokenStr: string): (bool, string, string) = let userId = token.claims["sub"].node.str let role = if "role" in token.claims: token.claims["role"].node.str else: "user" return (true, userId, role) - except: + except ValueError, KeyError: return (false, "", "") proc recvWithTimeout(client: AsyncSocket, size: int, timeoutMs: int): Future[string] {.async.} = diff --git a/src/barabadb/fts/engine b/src/barabadb/fts/engine new file mode 100755 index 0000000000000000000000000000000000000000..5995748953e7ca404ce29011e6bbae9b60f08dc3 GIT binary patch literal 73016 zcmeEv3w%_?_5W@XU?DL#C~8!!iv~>;br%9;CCF~rkhx(4K|rvef)IqtBf>^eF(lmt zxGooBYt^>4rMA_!YNb9=5%Pcpd=tOAv|RN-!pUX-A9sY{r!HQe}Db4 ze8}vbJC8GG&YW}RoHMga^L?WdtX7NhNwi#Rp(l55k>l215Le7zdIUVQ4+%Nr^6L7yJ@Pv7ImWjs?E{uv|^1MTDY z`e#zTc=(KGyC~iP&SJe@@g`9qK7CS$-$)HO$DtnK^ZR}bIIq+DGsY92I%uNsF;aax zELYE+bIXwItLM(Va_*e@Ws9#|oHOLgA=y_gT5#1sUY_8hK8O|z#!R#%R9I>$&!|t| z``|wfhu+_?&AIo}s{^C8FYo$!=)kY8pYHwL0HY16LuKeg<#mwwGkurvpUUv@TPrC) zf%2&TZ6$Bba~%#E%ZuK{e6sO$W=xKFxS)&t2a#_VMPmNaSUPK`q>FsHi~Qm)@}EJz z9slD$laSF_zh`ujPdMtVJmIgi{F}PSuS32a|KmT`AmifrOgvwS|Gh02SlnNVm&UrZ zlq{a(x6CV>KW}=eW!9{@Ws7E87Wo&>Etzk*`DW1AjGGtvr!Vy1Ja77(`FJt2WZ^=~ z!s&AsmEiS^#nW$|HD~_xxpVF=vCJ!(x2VK#Sv2QXWYE)$(z`6P?p!#>k9X8KHAeI~ z#8Tjs^G4o0@G57v`E^j_>%gl9TW*#o6y1!zN^YIA$X~K>Lea>%3+9(hn10J#>i5=p z3+C%!-7JcBS`Y(G0KHoASNyYrlSIDLH=^4_%TJIR>+ym6AH)Fw-{%eU+8w8Z5*3?hu9lbW+d`{{A{ZoM6QTz<3OPVFI4kLW&{Z)}Om z*I6nr*c_E_;D}!AjLJ{tCB>hXsC+3Z-=K5ie`!(q7Ja-VS7^-{A6ryD@jc^{7M0KC zy8a?PDxbz?eC$#A)Q9oOjLO$l2mZ)3C_OQTbZN(Gvowd%bOdb9_fpYfEyDgTlBdrtu@U9L3GPN7wiMiVSX9NA$ zq9CEaXShYWsKSB|I^ZeF-_Oerrg5{ZQ%q@Nbzh8Nb?u4F+Q_z@^7lalXXp*s!Y#9; zi@cHgUr9eYUNQVe)U}jdF_8s^@Bav2nX=cP!h*wRp_;b!6mT*V@HTcEjB?f+(YM(@ z)eqf?mpVM22@{#}?RXZPWMj%#ta`iuDqrwo7CeW!uD6xn&g%9gF*V=D633@8C4mLc z;yHNoDYw{IAkoT#6LTWf&SI|dw(`qSCC4bpt0l6!mL!%q64j>hYEy3XOeMIO^0AhA z14a-$t92`XKB}eb&p*ZUzXN{{j>K%HnAF z6Ak!(g6t^z*_Hjn;D2@=$CFO~6Zpfwj{kXRC6<1sh<1to$40|HQsjgFksSs91wRn} zujWR<|Je8+2>(-PB^Lf)i*||rW8e>ne9-@x82G0^7)PyN55&ut1``h)$Yg<2Go)pg zLSKN?OMobUf1wV=G4{o-(*4>%12vdWd~E5nev`o*P6frEhl}}olNah!IUkK zDhVvBS@l}1(u=jyi{2jLS&BE~99DTux(u){+XdBxts8yAvYhue&&x(9N$lt+)U2{i zR<={)b)jTxJ+S8_tJ4ygwVtU-KZATqkh2;J)TtL(^Q9Me<)P_|c+-8*^oTqOO|Rlj z@5+AR{h#i>U?iI5aK7hLwzInZX8_hS(Jp%Es`a`8)z_0+-%71Zy!A8Ddbs7AZql-A zdF$W4HNC3$oo_q=!21}0ZztFZ@crj^)pTcor?GY6Pm?E*>c(7Kq(Cdav2`2YJ^1Qd zr}pgv8v3FGpRz@af_Ko`ud5E~N2=a4V^E%o=A}QqqZ!R#%bOo`@jFuz23>s#;J>6J z{QbLXz5&f&5Yv1Lnjdj@+E+i;p&!PZuYB_t#SKMy$$$*Zqnkj{(g1IpIm@`Ku72Yb=7=F=xx;ejK`mSq2&yY`zNXSy3m<`d$4Cc zX~%4xBp2Md74~s44#wvgWzxIqrQ;yFv%s;Q(z12boZ4+VG+H@u09Z_7v+|Ur3}0}b z<&|`LsT}fUeOjnyU!kp>ipOBStx)ZgQK)(|ZGjWEMVB9GNRp~QN6G44{#5COUMp-z z8WN=HB!W7yDU*~n52=xE)a}I-oAM79^!I9Kir>}_g#sGgzIK+?W^G_u^?^^@(dNP? zq|REUE>Mg8NG>Ynrn+`6JQUL$^x8n3BX31Y2G{j{%0E2o&j1vm3@E5V@Aa%dmom=c z8Y{~4GGrqSS&-#ZwtCi+MK*L8N-uxco1Q-7Puo1}C3nDSe3SA^JSn@R7bl#5Y8DEg3bi65vIp=?G?*K-XX0#VX_u*#hsSY>yERh|&6vN?`bhQKO6 zMYA%vn6ix;uhY6@mGuScgfoemZ6{`Sj=?HV2v%7U$0|SNaBlM{uL)MUpI9Y&=!({B z3y8y`^;&9O01j4pLa@p!<5*<~tWp3D9#73;nRUr3I|Dq1Rqm7m@5ix94XpB0pHeGE z!8_RBC9A9hFYBrEG68uEt3>l>#<9u}SY4`2UIe&|zn2=>SMY~TbqdjS{|IQ#P7 znw)(y=j@@u20s-1v9;OQKk-J0KeoQ=QyK(wY<-635@&3E+^4)Q*kbD={44Rq*8Afa z;su-`0vyf|$xg$|a)x*do?;l{7sL>a{=oy&0r8&zF;igNzzX+f*jQ6i2KupRZyv{h zf=L-HI03nVP3aA@BD_?Lo7XStqQ2U(zG>_cdaKe77VJ-Es&{CZ9o^0PwMBP<{ZBW~ zTNZ4Kb>mp@!d`fMITgSO7Of6H_8ZQ0wc8H@R+uMTNX1@uEWUMw*Ou?x>I{1z@WbJk zE=EHygFRWa`U9wrUXod$J?!s>!P*+Mdr>B;1!12;eO@J|5$&g_@7zw6ndGhK)ww-H zR=%EyUGdYiGYL#=><{{{;UdJ^n?dzSaTyR^Gu@;hXUolAwc}3(NCZutKBtR$W{61*T#{8y>V{XoLlW z4-cgAui)>MfOwjXkxxqlTqtMZqhvuEH4D^PS>5MJ%-Y111t@eYtXVdJhG=ly7Ps_5 zBM7vKx=e1BKrht0zdLi4+R6?$)G*~Rt2^FPdZFGZbw=wTvRjFAnQT}w%2ne?_T z7Cs7AxzEyurmVHh)y$-^^~}|Xq>U*r5`dwLKrO~T6v_bG6=o6KBeY*N+zKpW^U-vSu;T2?fl@&^JUYW@0@eVFX)H8I(s z)KprnQuTRQB(#pSOTgs110 z&}__6%|sx3JW~e%0Xu~60Za!-ms&5>=Z~+SG&XE9)&+5~Dcl~tp%|mnt^#n-gMn4d zCn_qFNhpXSRVYB~16<)7 zawZ}DVauWvS*JVbo0$1lrd)56l>c^<6pn-eO>(f>=;q8;X3AedXE_4jKIN0H z@j;koibiPRo4X1coxsn7#|S^a;O!HNL{EgJr()Q*8&h_iDMwlG4qGKCjtnq=MJtu3 z@W*=I*1#w2fx2|@RCHdg9B65m1GVYc6g3stQN*%CW9k`O9zwkrYMHbl$?CJ#s_X;{ zXx(KfdOJ?ZbuHbPI;N#ieXjcf;;BLZVL*LnI!XB?rj*;6(#Y!eq0n>A6tK9!Cl@hg z$zhw2<^XNS=Sp4A^*>F*dZl+e_Vs**mTiL}|7rm^An`~K)uOXRuTmG9%DGPk0Z8&l z`U%7qwZJ@#`b^gwTOl>QwyKD#iKVOSXDa{+Ud zdwTwyMmfclH(@{yP~6f(P3SFAImSm>L;EF6NAQkx!t@xm%1X@_D*h%|`;Yk3jUf=5 zm3q6=jrKTtflpcaQr-b8($=Hf`mKD3x-WYtu;(Hw4z|5x-_iH_y-0xQbuFGrp6mIZ znGj1$%pjW?=(aPmVdvGt0tkeHY!1U-`0HO_uGV8DBX;bUUf2$z>2t2?OE|HP);-(N z!n$!W)1QkM)$f23xRdp3^ttx=q%r$El_$mgy@TR-Kpk@e^~b0Vy~>^<WvOzG*k;uFg5SEyyF-KPl(vD$ER$&WaZBfK+2UBtIWzXTfCP9X#8HK_UKM;zuMuv z`{l2qd*}AyG$YbOG_#4ywjG+j2zoGUGuWH7>JUl=SMo_!UOW0hzt?o?TYf!Wz%?nR zG>UQ!l%$-i(~A!RQzsc;aPAIUQT0Ko>S5FdDa*l$5SNja!;}eMVbZrtRUcDI|O#_Qx3{a`CI`jLYca6!Lv@K zK&{&BQ*#^S;Btd$^$xY5dBMM>s(0x7u-Ueq`V!20NKZeB}(GNOX(@~r&^ebJi4o4RztTveobwgo1P66`b`C&)e9;hLz)_GAb2=ZnwLo=?x^m~5x6kLf>$1fj6Ax% z-vgY?r0NtDfz^(4N-W-IiFiXt2}fL>^Q}Q>2v(7JX2DUJ@Q_3C=+EC1*S2nxD_dy& z`CN_u9|cC`KzZUy!5(Fcb(Di;ttQ=8$Xor*me$Yo_Bzq89TELn*Pn!btuw!)q+~;X zf~Y^bo%Ng0p4Je-PxR)G^d6ZrJ7|ztbcUvAitApj19g4ka-paBlseH#-7!;GZ$(FP z-LdZItgzpNEoo5IxiTGmXff%Bu6icC?vKAK1gQ%$WLt)9Y z`xLhw8FnKh9T~8cAk2V7sbKKTHKEDoA1Jtb9(Lj8`Z&Uw62t^a)kNjX&|O5HCcU8p zwC8OCC#fx~^>D5WVj-){j+Ud%{iy9Gj1L%}z=Swf_DGXNaHQ+is zv;%|T`)eIf4Kz5~vjR`Mp(>|{r6nuhc$6b@-M8I6fqf(7z~+R)V3G!mwMhXGBQTZ- zgwZxpuDr)q3v87yVI-Zx*DhT|Eh&ez-e4LsAVcg3NPfI68j!TsEQ5dW{lNtshQq9> z9vBXLv7&d>7xwF;(a6v)`WaDv0psm*Y-kg}PSFW*x4VE6@k*$&o|{6mkX59>R`_Y? z5V5=n#p&yhWr_W@uAq_r3eHR2EIkA2RHy{73w%P0>8RJc%tgOiEc!Yu`Yq9mUgRt9 zhmv>c`!7DuCW8*PiKTlDIdN>N&XZY(WEh| ze=itjcaF=iP{iw+ohwy`sBnI=1=1)XGQsocYLa-}pAHXnadV0Ml*SnSC9 zKq+=$t07e-QPCOOnd^glFGr9O6ix7bqIwoAwHuouI1}k%q21V!uwXS&a_DLlBt1dd zneRNP!@Y`um|zH9{yb3d*XVVuTqZISnIt@#J3rR4(9sDlJjY96rmYl6IpDRm2>pMFr8iQX7JZ4 zd?>;cu*D$1D1BO&r`3L7vbAPbjW+CXI2<}iOjYMUhn%~#>!J7qr&86+coF&o{uufM zbxB)_BIFrcijbQZs35EaS$5WB;QTdU*ygr^7_xO#I=2@tp8$Noj$I6VY&Y=nKC|v- zm7jB3Lb&Cn0XN~}Oq3|wqa3z^n6jRy=>ctpp2E}_a)y_vRSvg)7{xGZ5TBxsl1v{i zR>Y?WkHcE2JRwN`9?U9&Gn0HOOfCu*IjmJQpa21%mkTXu36w3GFsbToDjURT>R9C` zg4h>=*k1#&{{+Nd=*aqqf~g^5f02sL*vee5mtVnxg&_8c7O84JZ-93DV0zVC{h)Sw zxLv1qOl6RdEkr_)<3FDM9)a2&M}HtiPJl#w%49)!Sv!gDI-|Nvjd_iuyIGyk9fyTw zy~PyqZ3c+Z)v&a$Odjo6!UNU>=-4!a@V<&7JVAB3Jx1RTRJ1-K;DF9Y3;0ES0hf)C zRewg*8ZG~k){}2Q!PaOg{1__~s;^j~t-q|%f`>qON_}gKXis~h9fbTc9>ji8R*Ep6 zU{tiRV@)1A5Nipkco5}-dD&S%<}<2#6WmtJpGluv3zz!@eHIe0GwAc}X!<-3`sC}i zCrW@mzqDfhb^0uHgpNniCofT}d>KohOf4Ylgv1x5X^`a~b+SY}bf3)>1r1rn=`slT zIbBNC75Hk>=2@H(e=piR5ur_Bv@m@p8`kF_^&wR~^<4xCc(VJifch08{bN>*8L!O= z)u*nZc1WImMZPp*lxoUBI=vjlFu#*vlln!tWLNZhM+bT}K?B9kWQqWj=`@>3fbv1c z5H8Azp+_nU`o)XBe4IqRQTt7UU(O|dxq^?sFOOG8lr;!heTC0egr3JTwaj5;3$asv zg*N0UC)#L|p2-qLhp|0tPmq@m^u0r;@7IC**1wwcjncp`(FYOv_0dEwUG z1juTxm>Tu()BY#IGXnhpMg%q;p8KHjGIb*0!LC13hX=~E_T}q%5!(>#GqCSr zcdzdSf_Y@IN)zgO`4zIdg3EbzVY<)NBvt*63ox_{gH87~CjGs4*-}GKsD|Vym6#;M zu5~I2#r*@An*9xl61WE;=b#kcokn{`)JqxahvVN$eYJif^j6&fpytAPH56F)--swZ z|736Qx?Hd8`rNWHEO>n;Q+h)c%Y@OAteCJ^Lapf@ngiZ$=nsY=A-E75={!3Cwit#4 zFBua0Ld=PMGc-ZP$!07BXyBi~fva&zPiq|%Lf&prLOxgel9)0MO5eHGJO{-a81@G_ zSXCR@U%F!Vm#|loUlFA@LPA4fjqJ!_uS7f?*(={K?6+hK>`N7ViyK_A7+Nw@-e)eu zdLb*~mBL?zieDp|`Zu1h!~gyBHJ5LtNj`O)uePQC-t)C)3(ePYeRJd^Gf!}Anq1k= z?Q_GUR_^^`nZb+Njq3)kI;rXzq8l=_7>n&1shaH5!cLFM*_!sfSoMP?kgjXg!~8*OBtV@QnZ6Feflq^22EcA`cLXF-YMc;=-EaZ z6Nf`=hELD8Ip2?e{+~DB4VzBScbuu(obMO+{^!m2j(R@doGa9V;engl@Q~fVnH~a~ zm8s+4`+<{tPZB~mlD^8579@3-dMBL|DHZH<4wMR11NY!)@NY*g)A=o$BC1__!)|8-!%whMk;O(%7z{W)v(a=d^ zxE@6~1{gV%4OqkGz#8Upc(Ym*56oe(rcux$dsudDDu>0vLl%dWk4UyiRlnvI2U3W1 z7bgqSL$PiWqq1O^+~V*qV8D}5oppP|pQ#nNKeWI{;qc2O2=z_aMdm;@eWoePvhX!H!l&Vd8gD;rD<+SsN ztgg@C@;PE{@Gy0e!_3#s4SltyeIO~d6sV^dr&2(XD4=a!p^g3!`>Q?X{z1D9Ypr#i z`D_Br4WP5ReZg(VL|8&oxP4(WuZpD(lHx;)DCjtIRfhA)3_NlMaqRWZ-=l@lggHTovJ0aQOPH`^|OtrBp@n zhEX)Jo705p6KOVmBIB^`-dzfxNDnj%ywy@*9fMCqG776VxMk~RpTb8>KFDD{rOl_n z;PtW2z=G~LssYE6r7@c@>srFA#As0Xkaq5u#G*GL=)&~qahD3>feia#Hn@0b99Lv3 zFaV48=}u()6iY_vk6Pj$JWn>CgQZwUv$2l8B=;TG5lAGPsrL~YfvDvZ-sGMm!K5ya z#uxZh$i!D$oWF$l6w6Y%zl1qL_ha=K`m6AlVD<3*vaFoT8TBtw!r17BR#NduUqm0m zU;QmNSwjY_qPPw6upqRbyeGI+L#{k2mJP*K(6W)0Mq&uUicM}5JL&|BbFu2G>6TJ+ z;jG|vj}vJh81@4NUdAY2BG@^1CPE(F&KJ&h+4T-dl;ZT6g~O7*5fWwFcP$^zp14F= zfbPZkktgi9ofvVkzWFHQ4Y|^)7m%;b`NUXK)A*8_=}XKI0_!ZiHslFRF(k@_@QS+J z8TSN#{paU8dOQ zV7-C=ig0}w*4t~CwhrsfL>bOzlSmLdx*4?Q-hr$`@CZkK%*vZtsHE`RNM*hPH_h}Z z0zrsY^fLS<|o{;kcY?!+HeekzC8=!WTzvIr4!_b+wSeeNrPzMEv@U z)+1v5nL>}qO5yk+=d2jQ*ANd1Cwj;q|%u`OeqpK5o=Eg=N(Kq|5dB?6%nTxdST4f5Q6IC}v~Z2_Cs|Gr|wCVS`G!>Kq6XpL!3J(~ry@+oRyqa$Puy znD4iOGeEZXK}lV<_QY2foDHQEG2u(NP=qgBfpVn>>ck7z{f2P8#SpIVMuaQx0`WTE zU%1|a{>eW)K;N%0^;N+t4QIbp{T_jcEsOEWShs&ezSf(ROm()ds?N4Ut_~1#H5spU zxk_qWv|NP?R$Lmk2sR?;!O#$1)E&2PWGde`k}fy;ZDYNreA|Vex}zGahgM)W!?%ti zo+lztBl_53U7ki_dUhj5I*;~_x(vEP|bdUv!0G=DEtp0>FTF6s@ z7(xc*snBT;buCXDAWvawt=F!4hU>JvL68&XLLhGlHjJ_|HzR5%%A@ej_}l6Z@-*=+ ztWSyp68e;O$@_S=o6k{kKy&ptAoMEy(2%Aa^^m5Ob%I2qrKuh#^__Yy_K6WWI{cr= zQGr@AOB6)Q(QEL4cp)=Z=s(+DG9F&R)Ao|i?IjOEH|)w@asj-a(e{#I@BHt!mteb0 z#zv)wxWTUgE*%(&g2wqdoJ7ZddIy+AU{4~#NfCkkXJQn%v<)9_{_W5H2&vee9Oy^ZU&yfgEmYgM;|WZd{5POvJ;|`=>;oMxg#pR>ythnCxH z9{!@!{x+IzA(u^i{AJ+v#=bWAAL4Ht`2@GeGnEdho59Sn0StMd9^oZr>fN{oV-*Q2 zSzSxsR=63ZDxB5=Eh8wm2bK*vM$eI|e}%lr`J0(OShJBs)=MM6p$KLk+O;6gp$KNe ze;()XTx^^J+7nxENI#)bz=7+Z<8#&fXL^G_&dqmC&Mo_~9E9OWnbao3pyIK*9Ud}f z>weB2p+deb7V(m1GjNc1hQ|r0B~se zp3doEN(1c&zJneJ5I6QI|KI6hO6cmxF|vp)Nx*d+R~)95Unad+a0<7faVq2M8Hcom z_8e-*#>KG5$V$6@SW<>@Sq?5Xf}uunnxvYtS_+!EpHI2Enn!N9-j}NO;1vrR!BIFE z1JPSf`z57&>*prE(TdKIh<+FaOl>jaHjW`~<0$cylsEOb4cKWDH z@N;fP97f0UoOC%1-4TKFqx#`dYK6yIfRhoV-yMpP`KQQy0`Lj~(OU1pZW-}g!7&&+ zuD`oJDvAv2$fq=hEKOiOz+U$wqj1A+H%`~QcWS^Ome=7Ih@vs${QPPA0G;Ofr}lv_ zw*tLC*gh~G`_u1ZAGl&8akoRYh3fx7`#|cWo!bY_ffM3iv=4mxYOH-A5sTx0seJ%v zR)p`^xDp3(6t4HAsx7pv$pgF`Uyh9I;g+iJfp63x5?^+3`C7A z!u2|IZ9-_=8oJL1{O1DVKV@gfvmUJ&&GQD78}(~>^qXRce+|1#s=gYEKvXOO-#-~J zhGyU;&SUEL%Oko3Jq_bM4UX#R&Zx*{H^C10L^N@9DDh*zqfu!_JaRNY)Y~VSJ1o+! z@)fyW{6jHbVX^uQ;c|$Y4zqssENcg|?wqAeJ|)g>2>lZ0Y~s(@tR?Kn2D{*l4V;Td z@q!1tNYw|Z48^G5ArNfRp$b>qMN4)TJ z8d6n1ZbH2s9({5D3pT*?U>tem5ii-o!KU^ZM5;1>;DaIg+@+rqj{Bz?2n`(^m8;3m zs!e;Duq>kR=@3=$I)n~cqgqG2p^4)7^E{WJvL0IVKXRU@037Qyv#GCA%Oyy_u=Pd5cbr? z`9(9H#XP?_&H>Fzj4L?K5!%_6of6Z|>4p3I_C(t$-^GKe{|SHJyT&pM97`*oYtA84 zUPr78)=4j`bGuL2yeU@dMNTDHEbzPX_2N?w@dX#)#)6^X;ry*a>HZ`vqmmpBo1R|B1aiA2*Htqgj zac3Ge@E8m(KBWw|g#Ac%eR%Igy5Si&K2r<|nX7P$qflL9?`hnY6B>f5!VVt|7>=13 zSbDyW5Uh(4e&TiMG-^$V4q@xp_vQ6$+i|+EC}zC;QX4K0NSg+lC`cu${fQmgFVAPe zdm*FBAZnUy2phm$zZ2VK=y$)QHCRE-y^@4K%(WW_+F_s@-BZ7q2fR4n*66lbLSrC) zXnqmRbxJqde*>G|4q7k#e#7XIE4!np|0RS>UvO`M*YzY6M z<&#dv8dqz1Z|#7I{!VBKbbl-bkA6(^OKXw5lN5c%?>H5!kdBrz*HB4) zp1yGX2&Z3ZMgEa8XhqH>`KqtT9DMhw*)YK!@ku_!MULrgJ?f;7;FS#Rg$??GlyN1}3wFxZj2 z?UZb7j6z;$1DAyrz;#3)@NA$J=_3>u8BSKLd0><9Z7@A;D%5<=>bk(JwE9sruCqA! z6KsnX9;wIW^uI;p7_>i4dLiH9S<&N2J?^Lf4Q0ln{1I&Wi0?pRjaHXj-P3u*m$hFT z^$Tn!?m@#fGO<}<#m~Z}y0}(UU1GIq{ZW9UCv*#SAIyiqzEdzi|2GCLf|hd1|yi>{t&_lyqWejL&_{u|7yx=;3rSd%$)_)Fnt8mwi$4)Iq94D^LOV)vIv5$a# zL(y8lh+<#E(zW4^NS)8Szd* z+;cps&8MQKvMull+)c-9^9a0#W-$ugqx6^7Cuh~bN2ANB52l3Pj?gVg z0QMd2Rphi*N5LPZ54=lb^x%>2re0p$cWN0HwNB9A-SEOtS{S}$SH}jpi$fef*%b4bL)!D7L6k5!6 zs|sL(eqU@7NCMZ8Z}-Ff^S%+O&!d(K3}d#8=7bsAJmUKxN0)kMy6kF}st%&5h%pRd&+AXN!J#3>DCL*Oc%*t5wFZQ8RT2EoE{4iV8)hCSPl>#Y)SRL(dD zOzm;2bBsOPMRxIQZ*VO9A!BmO5PBbP#pU;zLmz{+Jv5S|ql)0fAnweB*_+L{(;?0m zlG%Hy%#Rm_u8QC-(p@L#77KB1k+{>xYazDiy7P!U2E4*{j@E_koGIdbA|C~uHT(xo zBJ-;lmwBOP#5!JV@GHvIPZoX;Q@1vJ6(G8m(~2=((zkHBliz#(ea<jV~ zAodh~5Ds7TgLF7V2Q~uTx|!~mk2y$}8YvY_Nk!;79i;0MdyuZiuyYux5wv*`Q*f)m zF8>1XZ0J?Dwc(OE=}r%RrV&adKclaCzzrx4MR=&7UVR*H=F5@u5$IPaIps#3jfWaG z@ck_0KbRRcLO+uN(YP;G2(3e}5m|hh((J z^-5|e)YRRKHx>FL^eS#~gT({My!fK{8;4qo?TJ%fY;sgyus&=3V znG&ADOAuWKj+{j`z<)mozve}I2?%WbZmg6=YLz3;NP6&N%!=VZ&sv#y+Pnlwoos*tBa`gu|Xj??rv#ejJS(81K1+1seML ztS6ch?eiD-n?@%3=gdbdUI|;0>VxH%`mAt3+yP46&wJIrT~6~(nw>SI7gO(onl~rO z)08yAe$`b}`Dz=~mV@O#g6P5KaEX-SQ}+9;2NCXK)7GN_QN&ydFXN!5xJTKiUU?g? zGBao=?P*eE4%K2Y_#o!g$-uxFCvL@D^meC7y-|+q&I1mnHE!_+3;MybYFI7$=qy;&9P*V6#=KcpMM( zb8q~!kzLXqdaShi50tBV=i$#8^+RbkQCIag0$0Gt8S9XR^EkF^2?~W_VSTI>;}TB~ z{o1Nj)s1QcJ$$(yaZ}cIlq8!}vWJ`~68#_;oGMaMPQFFA@gtQcKQU*8dc!(48I);TJpbi5 zeZ@G^WLO2XcI+5J#QnJb#1DUokS868o%Fu9cyIRS!>t%Idlt_$KbS#iVvSPe0#wj0Oau;UcvYdlMAa5IG+@y!=1uWxAs5QV>ML}0 zPD9$_Atq$}iWD;HMj;icBhD-6B<7L|*{bidFo}L1{%keF=PvOGW8y8$1i$;8}=Z zN}s?FK>v1^G$oHpvy|q74|Xhg(Lg+1Jut^S=!c za&NS>US#5HBcFZxLwH!-j`{Lo|INjx7PCReAgR!ivfISl{jj!23WTl$mS)w+($kwu z3o*LVe4q4mYbkbtZWh+sqW^`NuoAkuZW_PL(U`4gW99VK=m2C8860R zzQ>d+pX2>c>3vdX?-8$x_qWh{`eDS_s)z8ZfL_thhI4-{sY!kMXOa;co4RCinPAn= zF&E}UiqpOkKhCAUCAbgOVo)1P;if1i41v4pX(k>)or@jD2}CgBbhiBGnV7c7skkgI zj}ayns4F%P9JWbSzeLYbhwDF5x;$JTSnL>Eo*4n(2rt6j0e0vXNqkWJHJ$FnPfECP zDpDBa>Wi6rLmF4`=pCd7V2lwQ_*Ju;r_(Sz5HsBp=+~!6Bq$^4v4sC>FF1VD$n-P$ zU`N`yq(}kq`0W-sunCglN<73#icQk$zf-P|6vH2O(n_6SNQxZ%;KeFr0e2)J41vLw zj*E}bd@nH^O_2R?QmzTxPJ7<4t3F51@})?<64C`P&{yI?9u@+ zu95RuOt>FP*WGsG=T%^Npoen(TzMY(DTFsT)C6J?bjr^mO4VLuaNDWwp8+Pbbs9S2 z-)Q`bNbB=m@aqFwA{ZfI$>i4|w5ZL4vW0>30Xs1-bYEfMCyrvic6ZzPW(WD8*>4DvZ>hofr5H}e{oUd5aJAP1!{SLTIE#hN7PPda>IR`fE9A%3SP|(eiN2f5g&$w@m=S3SZnF#HJIz4J}TAJ zLx(CWBED6NwczIP<4E#Yv zym1Cj-r;wdRhy;EJ{6h|=A*sB1zPLhbvlUdOZ)h1>ZtV>ko)HEpK<>Wzv0|)!D5>Q zn*`lg;!~%AnSa`Pb%dYd6fY^8u>IkZiX?Fv3Jjp2eC>94c3Gg+S~}k6YQ6V8Y#-dr zmCyWBnc9~(SLE8e^a>p=c&gD3{amiz7M8Kzjw?i7n*;ZL=y???w59b@u}>g0n@Y~R zm}*Y{jrqk^5B-`883l!jiLG)2FgF1=<&a1u z;vxIFKLPgE{n|ws*-YQCOO|XkxzJKX31#xYBG{U+0c#SN7j?lgDHoW8sj0U723(x2 zK1L&ooe%2M+DQ9wU+`LNXR)P7LwWxh*z%csuVg525LDU;DDAw#Km#I`%fO!+mb}bQ zsPV3>`4k`H?_*%*4JzK0&m{m#sCX zJ~6>r`%*H7&o}bZfImHcTiJ`H@)di+xT_UA2b%>d16#hFhvY&Hsvd#a@u`0x4Dsou z@g~D9NDkmbuytd8i{s~a0&KfSq4;xaHx0cv)p{3}8*`V!$DNgw5g~*!;&-|Q169t` zfzp19Y8(m87$MCeRh@~89Xb6}EXgRglwF3ZDf+y6ag{QKDtI$;$}eQ=z_aLc5iaQ| z9|e9ejVK}Uwr3y$E|%;RPoW{w);!@IH=G}<`N4) zTE!jBv1F&~X>rGLQMs?^*f^V;K;Rg>fwaEevbZ;ol*S?$Ca`^tc80>^(P_|!2s3zO zF|UBSKIL2OZ`f>@*g)3;3!xMMZ}8t3phFizp7QxLAtG+|{uKbxpb~xMvE@$-x;|KT zo1VpKTD#@}K!C7X{6KU$(QAFK zqf4A$NejFg#SB-{OdG8xVeJo+Rqtj1Q=ASx4?d%q&=$z2SFyB7c$Y(=hiPHJ2p;8v zyA{Kh3cQet_WNAl$dd015f~JBfQsL?IbEvYw_XKmZBoS%9i$axA;2&@QT&U33$5q2 zleosUNvde%<-^j7N%Td*U-% zb{HWCy}B(7nC4Q02&<7Q=+L!*lkegHC+~W+(e;OX*x1rtf*h$zMW5!>kq_{cPpu&S z2KNMaWNAT676ockngAy9Ezfz4lHSV1r`|a_I~0^S{soYA9LQ z_J+>qgR250fR(27Tx4+P+cM74h&WG}j(QvFouOa_ZC)hoNC^esg^9P$ljhvmh zWA;tj*aH+}b1?K5{<4Di7qzbC$#5L zo_>cypP0}RW8DdUR-|kue)a|_g{DdvSej-jOM>zZ;d#(Mj%+(Hvo6B<_C><#v<6dB zq#Tt4P1sA+(Tv4fotmW8q=|sQ@9@D(s9T$0rI`&ZPs8R0JkhWLN36Xk3CqE5gC=rh zN11_XN5Si~)4B|P4>Pcc*@-d(-=tAZM9+Pf8F=sGoX)<}3_Lr=3>^OapEVz423|(Z z8>)y@y^%&{n1Y4gYibiF-y}PGv|adT=&beSj`rZYksop!>oI1311`;tN|-e5k}qg~ z(-hg?Oa#$sY8xug;N&OhJ$_$7LP9%;Ve0#eiwruBN%hcj}%(TuMkNe->@h@||TY~AYM zJsGf5zY0l&-)DgZ2b9;*2(jBBNu=VZSA44Zivqt_2`+Agc&Te61dB03$jcwdh_yn% zsLu7Bj&_JIv9|u_><~?@<$RxE?B{2|QmOxz2x1D1i{h}8jruu}K&aUGJrN&@2ab(h z>M(w=fp5O0s?T^c|Kt}%IG)MmL8wDORIyLJ6&niNp`^!^`_$nX{m3eDxO1aOFBo~C zgn`UHrw}(7*TrwWllsb~lBT^y%M`Z$0A6DUPIh$)<)Cl&vTnSVj9?E(5dzc%tLqP% zz|Ti=0bfyohd5T(D6Jldk&V_3?9=U3+>go;3;XrRGYlqVVJEfvi_ZWFymGzx>5HnX ziHCyM_BxVJ7Iim0#%cmJ#{G&1z`#SjqU40~l~!CC#Z~B1K;<38*}#xLH2MYXdT{BK zJq8a;H?EI^yw&ov=(hDD7zbAOLa#3`2C$G7bLP)1nWcAwP4iUsI2hSoDWOt6;LVe zfJ(7nwjR{R=}nP0CgN6@0dPq4qzJcyRJ9NpGQ494LvyfA;Q|3bVaiRDcP6&8hQ1zo z4@5%O>*yai{(I1x8DHpm^(-{DE){=51HnY2>Noa<#MKo=iqG>YIdGZr;NCYd5n`Rj z@mpd42jytbLi7;%5RpMwg#+MiOrh2n@;US+m&O(76F-%r+mq_xErLA>rbne6Kk@=N zghIh0KYp=_a|-O`#P46|_M|N^+eF)wN}_)7fwO{lgZ)e=16M8{t5d!7>aMtC8xs&Sgq#h z^lKp}z#*ubSL(DcCxF0hUTVQbv-BU7>VP4*$!+6LP5N(V>G#zMYq8pfZ8>(*{O1wL z%gHkRSv7Qiv$B!bGJg|Fs=6b|VAcFrMn1qaFqri)rQGnF^(99aeXm^X_!t0it8q$y zJ{E;RE^VdTAw71KOX9ha%3xG(ZaS{4qi&rbS)iV`+hl1*juEfP7=j7{wGiE?0iR40 z*#fH@h?$YkWF!9q*Ufrj(ZmX&9@z59oPSJ4AXU(VZAm*2s&vEqK>H(@R?f_)X1IOC z>zoID;I~*4Q!}9Aeu$96kQHlHtP38&w-jCAmQsE$Vbwr9kcz&o-DlN&u3BtfYQeN% zc8ZRMCZ!(h)=%dX9u|uJ$3FCsrah{M8rH-2{1^%0JZtv);>STFV3 z-Z>-!!2a>9wf_c-nWm8f`Vh-EA%hhumlwGwP3}PtI#7#GC26?7cbmkb9S3Wswe>hpy#A{r5AED^Q&O!4$!bgO>c&kyTxj; zD%d(&8~C`3f>+7ao26>va}gjsDViXDoxau?aNq^hwf?mSW%CSoHYeHTCR#Yqc4R*HE=)S+Eg7K8M{ z^fRs=J8n(T4e$KKmmsmwGS0eay+fHN_(?4EqMaE8s=-K(t|pXG26BN;G!Fc6lIY7x z2)P&?PEM0JkfpKj5?Xm2c^pbtr5Obi zpaqGE*fcp}9*w%c&BHg+=&VR85>2c|6A}LMPsed}m*ZHS16g4bL})*?$@Mm$iji)> zAhDznhCneC=e-dFDSpSx_VSH4VxcH$jqZeA(SxW(>@$Ych$5I=)F%nzbNMsu(n^VT zL4msTBG)OY`X*wq7(Hg`D4DGKgO%`K#S?s4s-6imc&HkGjQxc24+1xn1+iLrNUjgd zAxBzO0Z;|dpuY&vGhe3;CJ=ft8Fa>qz-jcJV?z_j;i2nTIe_&Xg7tQrcKafj9jOr- zbO!4Cf>;@1dZ;qi$i=oS{!FG~W8GW~M}O9bQpM$%?^n;G>RIr?_QexaBmVoouTZ1d ztztcE98}0Jv!hq&M?y?GAk5iyim1h2$3P~z7aGV`BURL6uIO~kt3G8fj@h&h=ksQ^ zcUw$*E2bu3rE>zSwF>u;(WF$dm|DW_GEaJ;uf1wl*(*jP3|Eucw0MGbpkfwH`6;FfL1mGtdTDDk2FRZF5P}ilIsxc`+TM+$(t1g~ zyT|wVD1e9L$Q3e&Tyhc@+68vBZ3!jdf%py18&=M72pI`?G0q$cX{X#yDoV$ro?J*m zin0B`&+q3Yd2h8w@10Wz^_1G?$9_+yPzyyY90JuJ30RY zd%6d|nYf_Mv|G@PP88hcWkLMxydK;(b0;C?`)(=s!ALIFuh?HxJN$$|V3W-mMts}Z zXpYZM{C!P`AH9%)A0Y^X&pvL|*4E8H-*6g+mf}m4E<^D1qhLDd*o9n~L3PP${A-$0 z4DM|0*ewLiAqdA!q@+Vwmt{F&!wf*#3QZZw{R+_RS|OM%ISC;8D>b^hMEKdd<%m0 zhE^j-=QoCV!=m+q`oKvA(g;&R@q0!zAJ=a5>^3t<5k{x8%z1p|J*w38Jfinl9Y1tF zV2<#2F`v|)8Ly}xA`E%ZbPd^eUpfwCn#v^{q?HHSL9i@vPbO!LP#Vbd(%8cye!mr3 z%&UD-g_c_G*%a=@zk2vrcM1#M1AA&%wjT0OAdGSnzk+>4svt{@Tp50w1n>={imgcF zvJj4Xvhc!(sCK&%zI!Hl+b9|h=)Zb86l1j@feq)iM!@8b7D&gFmVE`iVgr4p6AG6e z0=K^j9n>HuG66SSQ+a817&#m4Qvj()@n4CxGzF$Toc~{neA*|jolKee`=P2j$cq2R}_*0}8Q_h*Cq~NqneFAc<{vOf` zDd(&}28ghi2yD9)AcM8{H9H*!W{eSEiLUmB&gGohqRsdSgjl(W2qQ|-s3GThRV#Su zkvyy$icrwEZIR#_<>v^iTTcYzJlOiUzCN5=aXhR)W&ua|c59<!!u` zAE6{Zgem|) zlLUB&>F`R`PJ9a$@WSFf&fCRkE&{~jI3vn zNGJ2mWyRnA(Svr<=3~s=zx9YS+ zrH+oy{TPFXz)4eT6{jcY?{-r0TTh0($W>}R(u@9Xr)DANlwlm`FuPA+TM~K|5b5$G z*zE?yRB{dvge2g+4@rVwaA}6|n;t|X&=_lG1xFOF@dDqYNNp7nJ_xOz6}bCMYxx-z zAX8axv-tbG>A8$&KMV(J%Q=oHQY zgZ$5i6SeSjcZka->TCv*H)0)Hw8p8%c*iRzHe5vTtHZNkL_!a?RAtvx)Gasi?%_3K>V~t`@8-? zMnmn7`a=>uJcI`>-8Z6Z%aqV^JR5S(u;N*^>LlpQ4M$yieFQDdrs>91e%mG0-u{k?k1l7~&Y}$Z@!w4yV&O&^gGN?HueJ z;vDMCak>UN208~095`rT_Q1gdhYTD#FlV4^kYkW@(7-{124xQ#JZQ+Ep@VV;xw0ME z&g_BNgR--;2WJn-9-5t#?HcSD>>NCB@Swrjg9i^DGI;3VoWZUkjv>w=1BVP6l09Vb zkRd~c4#^qf8tNG896E65prP4A2M--Gbm-8Wp{^W9jx%Rq&Y+y^oWVIma)##Q|*=U-zlo>N+4pD}mAqLP_c^|F-B zzis}4JLlUM&AD~{^trt(C?@-S*VyH`b4zZWKG(jWv}ECQ|C|N$Q5Nq;Q45}hbNsXC zmH6k(u$L@cxL_e((Yx{a1=mL^F7hu}IQ`ZV`}7$zN)|1;%AV=*`%C7P`tAM&_Jt+W zXWC~iTrkf*f6m+yS*XtY9Ww#d=ljuSS*gEdru~+?>?8dP=UzEd^zF&J4h1~7%wMo@ z9w0-xd2{9)uo?J@Ps0|gH36SQ{gV{;>GmC;?*GM4kN?jfWAzznJbtba8E%U7SZvp+ z?Un^)^Jgv^VE4}kr7W25pFU@P$wK>_MRuCNyJDK4!$tVG7l87vv@cj_M@>P4;>8`) z=a!Wi?Jk@?U*zI1ZA9^zGG!qMYz~N#6T~(7L{QhDmD$th&zxH_lm6RF7SAXtB_h1W zZoMFJ*psl6`cb!ebU56O?-jmq*pBvUkhZ|+T8s2yc#hhTZo&JOqHs6|?<>ZH!?Tgv z$KpNGTBKVjzc?IjL+TzE4yVI9ZGpASjkFf&Y@`*qDyaskdqOz84ypZyaM*?|w&SL7 zcrwybq@_sLB3*&B8R;WP?Kg+R&mo9m)QjMNr3+)5DVH=rSk*2|{I}PbHq<*CK znc?sskyaqxiL?!=4ZC>ztZ>*%yZKwuFVYsIi;)(?N4gSe+FX=JYDaniX(>`mPr!q; z57Od!;czC>X$Z8*rTo%xcnQ*q+c7?*YZn4eO5wOm!=h}tBOIQJwBjz{3u!5gj_)9y zwiNGSZ)is9Mp|4E4p)%<;iuv714!56ROFwLuBi@(*CVY#d_^Zn0|q|GppYDi1rg~^45CGFS1D_L588xC(mYDY@8q&B2q)AR2D4@^mE zh~&wj^beSCq>euUpGc=Yj`~PzkUm2BPoRCI?kCYN>`7~o`jOT?4S12ZtPO`#((wEY z>LIOp7IcDi+H;r(xY~+e42LHmU4zt*)UhrcUW2p^=_aK1^}sV0#I@qCqwxt=JAa1)W#C zgYrm=-^V<|IJWj5u;U;t{s8nt#;u8d5a}OFW+2%wy9)l+Njj@LMi!9TZLuB z`NJ+Bl);;!`d<8>%m6c$(?0`{PR0K<=?@?|g$`ylmepkQGp`7*|<$M^&hbTup_`M`N0RL)2 zero#4L{F-HS&}z3GtkYOTAcWNVz1OpWMOJh1k;h9oZyYi8kKCJ_69(xrGYp108bX} z<;AvFfc8T1?Rh%3=Sk*tO20YRJPCG9G-m--%*6x{Jw1T@_PBho1Iv@hw@(IdrSkYt zT8q2^$n#JJqdB!$u!Jl-k_=qg!6*YrI>5cUS7~W~1Lsd{2SwT7&$0 z%IEazC3*#N0!d3vaxF;C#_JrE^+GOHnq@~vXnV9-*pqytLFT|6%9f(uBdA9_ZjlI-8l}H#H$m`4hk?P2kMdB^}{PLnc4ttFG~RIfyB|N_FF_* z4=pqS48HR9b`o{gpw4YnhxZY+KJ(2k2yK=e{2u^1YC(BAU==_A`GTGt37>#=FqbAd zv1C3*(gWVPF&zE}s?vve(GujXxhWj}m@>qA+=yvH4S|K9Y}0;)gCgel>Xxax&8FEj_) z(5W7t1KDrRK>^JH`q$?GV_MeTOLO4G9Q=sp09hU8fM}`$Z7rD#IjQ4EO@Nq*=24?l z9f`N;BQc4pAeri}N8OhB&~d2lC{w=h^_uuA)X@dZ=;VaC<^zyGb=y(*f!qJ}x&u%* z8#*HMhr_qxJK=97&8f~OFwTVQIr$6WM);nI@+(lj+$_)6*+}9m1*wi3d!^ceKvCYC zd{5MqpaIfNx1j!7ovw&3)*){u^u_4BH;`vp91b6hD%XO%5ATZ2`x<%c?upGKefNQT z!{O@*MtlfIeUaCKJdR(CQ>C>QKR17hpBoQfTcj^P06mo)jr1Y9nTxzN$a9)>gZ857 zhOb?nZi*9Kozu;z0gts>a;HNe*u+QPajj$Z?#k`sr-=ZywoQ=2K! z#arNK|LtBm9c570$0{tvuQ}zZ%Ku{YkV{q5iP>&`>wKCNdxC zfbAsm+{mlLcZ2_u4n*(L!4$v7yQ}D3A<5Ml^osMOY`nAGA4@+Bd7u3ZdNHjhz zqd<*HE{e+mGtWembJ2bq+Mh`9bG{KJZwn0$AoS^nQ8wc@;qbXS-9+oocbd$sAUUDI ze8@{qxWRlNrJ8iM7NjS!U%OBrQ-^f|`U`YVm5DtGqGzmO)7<*(bLlaJD>vSl2$z1nYjJCeUet9&S z5^IB{+7sNVX;99HRvyOtjK{*^YW@8(+B*eEMkg#1lZ=9B%Z~T7M_+-m&+BEeuc^Uu zDoBPNZ}TNvrXgRW_8t$1v%2C#(Y$xs)J{Q{lM|l-@9adM@+S91A3p5y+x`L@Lu9?r z2LxPyr1x|2J`H>S&fe4dUWxaPzaf+a-|0hhz6N5r#b> z&5s*(`@R$oUqJYUoLENso1oJ%=A6jM<>q&AmT9TBhDH=8JXfIpNz`8vJ3cNSJK))! z@NlQ(SD2i*xO2kfv{#INH1w0PJ{(p}I$%*cTZJ_t3@yy0fKkb%(U0C_u%*abQiP|W z&0N@?ro9>tAJ^NA<_D3vF=TtfJ)Oc1r2etf7lp~x2idt!!mjnYi64-6SNomfgh`z? zVTc_z!uo~Y(_u6uI9s=T4PN-E2&e&lgeo!{#1eX zo^00b2-w3cqrC)QKP2V74Pa!AskZBquS-oIsXuW2Y#r()ZNpw1Z3wGVTj3rjkGWc z4_q$FUE!oKb!}SWd0inN)k*h!fZBTxJO$r{O=k=rn;Va7@h%tdUZZ!hbOKm)I`Jiw z1qL)Tk`zoxL0uq#8&LaSqm3iczQ>?dH~+l;N28@ z=OdkXER@y;KoLmzf9+jeaAZey?iJ*x5)&k6!+LQD!#YlEu$B2Ai4;|%zug^Mf4kDI zcbAZ>tC_3O?9Sibxw9)dp-Q9zm^cY*V&kolkfm^fi-bgFh@YTJ!9YPCC{@D4rocqS zL&6JK6a@){;C!dgIdiYBMz|^usXVA%*6I88>C@e(yHB^eTW2X9PqEE5(mz4z79F^% z>mSUIsUv;q_RI7t+y=rnfc+LQ8nzS&dlJ}%7~iwN79)IUg84MCL`vu_uUF^UaqSrXxlQyG-nR9g5P` zHU&JRv*9tRpyIcsB3f^~7snN+4O;$xz0BilQloTz$xW&Ns0r^i&OuRL5-=w!( zbDN1&dN@6td@z-LOT}AGrMs#71rnis2#uFlVeTYCboibZT*r?LPtz4#$A$7gd4T-q55T`Ha+3eRHB>nV>G|81{};_sa%CC|{o3^- z@RNS#pJ8Tu?|>-%6JUf728`SLeMEml@U6!gxA%sKPAGnY@e~u|2)^;V4E(*|zTlgG z%YZ%GNA#xP#rqkzZvhc~PVieFX58MxAWH7;sGh}lGH&mm5&gR0JNGdDStf?wW?KF~ zX29M_B07NiL-?&p#?`*xEx0K#{sSf^FZkvf1NP1y(J8@qt}t%jjUakS<;VoGZ`*)h z6}bp?zx53s%>$A%_#VdXI|M{OFZkvM7`OM|i2j@4iF+6yV`2t=n&v$0 z^9BR<4F#gd2p66xxnKBi$i~OM0SCkE0sL2l-#*j>pVt4~z!Ooy690(s-}(>(_HG!_ z&tt($U{|@cyEoHw!msPM{hI)=d`a-_`)U%HnO85V~A-iYeCEA_YUyTI~(;8f35k@K@6=;OjaDDA@Vu;DquHx~KZzF9<+ z-hh()d9h2nzyG?$2aWz-z`5{dk8u5n(EkAbx`CtSS()!zpQDISJ=?K*rUhS%;Zwl- z$v;mx>RBvvp~F&kSMa6B8Mq)g{@HcQe@pC!>hlj(j_6t6)juQnp!~wHC2~FkoZ59e z)~>G*zwkuKzXKl{7&4E&z$iX#_#W}2{u?#^)_01p#`q_E>+Ou|`f?mN>Fu$Ye%>Vf zyCvq=c{c`}>iM6sdcH$&=EnOFN$-lBM2b^3gy9Lnw`UkQFZkaHe&Y!S?E4)=UjR-_gzws#sXx~iX%fJ&hw)e-2!ax5O#_iuIAo_ja zRPKDN+~YsbdWipC^lQMWA9rH?_%`7E#v45h8^V8Pi_2Ae_1nZxdOOOvB%60B{z1m= zTO>sPoaCTijMP*6GYYyjk$gUuDjBNrp+_B>$Tte^Br*(Ky6c?lA-V#b^c-JzeBs}Y*}caJXAygT ze%RvJpNKy1lhls_r}489o7XP@r}nDdIU@W!mVd}>d{zKS_z}s+xGGA&DF4s>0^=rz z9}|36^#6p2yH|0UzhYiZUhw1t%-9hAw=4de3_Ll&v?X|38Y;uWycamx`Hh&Je@ggw zu5*5E-t&SRu`@C)%|8mhNw z;{Pi6U`(Gs1WtB!={wA?^XQG3_$NeWl)M%AodcMBpwUpYIQGe9Ms+=15`3%3xb{n1 za6K2&{oZ?l4`F^4WA*t{!cotS7;YBIevle&8OL##Yfj)+O|cD+5%9r4o+5_JW^u-G z&g8CNX|G)g<|>P)uGJULwnmp94MyGTPC8@UO2u(Fhttib^V#8Yt6q79Y@?>w@S0Wl z3y@M!i%!smL$|VlUjVOKLk)Z#b$C?o{A1qT{&THB)Xi+I0^; zAcm9uIh>VRv*x(I@2)#uGw|2Vitjc&r_yaS){!Oh<$yET!>WoXc6yElIuWHTK3dAO z-G|RyOpcbPFPBHljfrbrKQrcJZQ~D2&iv%$G<85wDR$a^?V1~S#ZIMGtp%NKP^)Y4 zVa+P7dfoLEr@m@tN8M5PVmmkPFJHJe>gMMr+7Df9B&$vu4L*5xY-Y-tnw@kU!+D)Z z?beNJKfk^8G!g?&RS<@H<@Ig`z zCR+8DAM$3>qobx?AKTlAd?uG<3F-m`UvPsGYij{sTujvqPIs%Gtwbm{KQdA<9WR*1 z@C{ZM0{?s(P3qJh^+KKuT+NW&SmA|aeuSormAKfNaRa{=X?H%8N}Go3UpYmiwKt{U zie`kXEFM~<56}Uov)a0DwJ-zUDe$5a$pu$4R2KZC01PUZWHeoh3?}pt+Jad=>2(6X zwLaFYOx8MWczi*Nu66;=b!=Tbp_#jPD5dE zgmaDi?j#(@?9+8RpF%S_wI(u#sxD-62M^~=7W3!S>^W!Zv`nMZlV`bSth*iC5hdfC znw}dUn|9_-o?Mt(bQZ_Pr>7j-PU=73_bPMCmoZ)=N-ZSQS?Y(ePPNG+s=_03VHK0D z!evaUbCbQ31wM#+Gl2Bd8F%4|CDNu0I?T?Qxo$AGGUGMEv^;zw!Q5KI7b+o}EpUe7 zsp-=AghQ7K=-O8vOQfHAZMlr`=K6utaBIz;2q;h5RD#J4tB|5$(b_UL4W<<1|&t&Zo?zST` z(o~Px8U{+OS!%+KjVU)O6Yy^nn4kq5-O{JzEF3!-EDn209z}z^3Vbc0U#w4@XxX{Y z!p5uvTs@5#IM%U!bh_1wj%YN;%=;cy4kOzdt9@!Ln05;-(v=tvtMbM!e+HAg?uA`J zy$Q?i*3Xh^_mz}0yv+7bP187a+J38wb<~03>(&F)Xt?cOPm_2W%e;)(Mkd2*!NpaQ zl4rf^mxeN-{YM6#y$q}c z40OCgG_gFt1Mfh(#EIF5Mq}nXlgd%|Ph)$cqn2l4TlYinzDvt!3XZmP_Y*gF@Lpz`P?mRf-d(S^TwQ;$HNs%yNwd2SLLyOEKF{8+oxEj@DX zIu@0cdbhLME1zaw8IygH3>K)Gim+vcOlH*htyXmL$-w0u3|6zEL%q*(Jesom{L`M> z#-xZ?rhEod0MmKV&O|#nx@GdfCH)iW{v0+{YGJiz{b$u^vhS3K?>Gvm!O z4ynJ?w40BaRwc4Of->@Er8AACq}#TOX;`a#3X42O39Tr|)x|<9>kBV_Y!##vZ3kn+ zXUjNQ$FGQecUS3%c(_dDnH*w;3@kRA`1x)`&v`gaq!M7))IQ4+$D<<|H@c%F8b%-# zox(D)ulHb$WXBOQV*KZ6Gr9oBfc1UbQ7qi3$65S7AiLmp;4kdi*cUQTDes_DNm|to z+E}NtE2!;Vf>!Lf+%s7n2U{KaWz6n@)W+@I0lk9R&Gp}qlM_mvXZv(GlEgy8#;wv@ zz2aK|XwY%WYcA~~pz%k&y_L7W{6S%|(#vWZDjQ3^nDcO&_uE6|@M{1RiIriG{OVNm znlZzj^+w<>g9bj+RgGaRd4Agr<6n{VhO5o)Fm@&ILJ7x%A++4B)hiFyDgup-mmZ`e z4$Hpkc2>=BWxa_an8IY*6!<8#`-JF+gEZf(yOcpNv95A zYS~@ju(vAb4679g(J%1AD6jxNcHn#(M>%ezRz{6l0m(-dX-_$9GvVL^=O;n*1nGYl zr*mRD+mvHEGyf`!XJ7*rbr%w7@BcqTN;H2$@+sZy<>ZJY_lo&gEdRDJDAoF^82wgC@}uSNNPea6UusUK zTE3!m4H3<+_vA`(?X)Mv+y80Ful;AXgk9)-EJH$)m3$5nDqr*K`xT|+gEbmr;>+c)Dag`yjJ@I$Y-(p6k@$&Rr z>9@g-D}Y{peJ7|?`B-W=eii>A5@_tIn)E$VkCJi_X#P>qeAQ}ZjO_e#ERL3r-@K3$Zxl1mgeg*Ip{?~S^{igY)b?2P?b4?zC{e^av YBx 0: payloadJson &= ",\"exp\":" & $claims.exp if claims.iat > 0: diff --git a/src/barabadb/protocol/scram.nim b/src/barabadb/protocol/scram.nim index d69c7e6..7820034 100644 --- a/src/barabadb/protocol/scram.nim +++ b/src/barabadb/protocol/scram.nim @@ -40,7 +40,9 @@ proc hmacSha256*(key, message: string): array[32, byte] = var ctx = initSha_256() ctx.update(k.toOpenArray(0, k.len-1)) let hash = ctx.digest() - k = $hash + var kHashed = newString(32) + copyMem(addr kHashed[0], unsafeAddr hash[0], 32) + k = kHashed while k.len < 64: k &= "\x00" @@ -130,6 +132,8 @@ proc generateNonce*(): string = ## Generate a cryptographically secure random nonce (base64-encoded). when defined(linux) or defined(macosx) or defined(bsd): let f = open("/dev/urandom") + if f == nil: + raise newException(IOError, "Failed to open /dev/urandom") defer: f.close() var bytes = newString(NonceBytes) let readLen = f.readBuffer(addr bytes[0], NonceBytes) @@ -209,18 +213,18 @@ proc parseClientFinal*(msg: string): (string, string, seq[byte]) = ## Returns: (channel_binding, nonce, proof_bytes) var cbind = "" var nonce = "" - var proofHex = "" + var proofEncoded = "" for p in msg.split(","): if p.startsWith("c="): cbind = p[2..^1] elif p.startsWith("r="): nonce = p[2..^1] elif p.startsWith("p="): - proofHex = p[2..^1] - if nonce.len == 0 or proofHex.len == 0: + proofEncoded = p[2..^1] + if nonce.len == 0 or proofEncoded.len == 0: raise newException(ValueError, "Missing nonce or proof in client-final-message") # proof is base64-encoded - var proof = decode(proofHex) + var proof = decode(proofEncoded) var proofBytes = newSeq[byte](proof.len) if proof.len > 0: copyMem(addr proofBytes[0], addr proof[0], proof.len) @@ -240,6 +244,8 @@ proc buildServerFinal*(serverSignature: openArray[byte]): string = # --------------------------------------------------------------------------- proc verifyClientProof*(state: ScramServerState, clientProof: openArray[byte]): bool = + if clientProof.len != 32: + return false let clientKey = xorBytes(clientProof, @(hmacSha256(state.storedKey, state.authMessage))) let computedStoredKey = sha256(clientKey) for i in 0..<32: diff --git a/src/barabadb/protocol/wire.nim b/src/barabadb/protocol/wire.nim index 4218148..88cc5cd 100644 --- a/src/barabadb/protocol/wire.nim +++ b/src/barabadb/protocol/wire.nim @@ -245,7 +245,7 @@ proc deserializeValue*(buf: openArray[byte], pos: var int, depth: int = 0): Wire result = WireValue(kind: fkBytes, bytesVal: readBytes(buf, pos)) of fkArray: let count = int(readUint32(buf, pos)) - if count > MaxWireArrayLen: + if count < 0 or count > MaxWireArrayLen: raise newException(ValueError, "Wire protocol: array exceeds max length") var arr: seq[WireValue] = @[] for i in 0.. MaxWireObjectLen: + if count < 0 or count > MaxWireObjectLen: raise newException(ValueError, "Wire protocol: object exceeds max length") var obj: seq[(string, WireValue)] = @[] for i in 0.. MaxWireVectorLen: + if count < 0 or count > MaxWireVectorLen: raise newException(ValueError, "Wire protocol: vector exceeds max length") if pos + count * 4 > buf.len: raise newException(ValueError, "Wire protocol: truncated vector data") diff --git a/src/barabadb/protocol/zerocopy.nim b/src/barabadb/protocol/zerocopy.nim index 269190f..54071ab 100644 --- a/src/barabadb/protocol/zerocopy.nim +++ b/src/barabadb/protocol/zerocopy.nim @@ -141,6 +141,8 @@ proc readString*(buf: ZeroBuf, offset: var int): string = let len = int(buf.readInt64(offset)) offset += 8 if len > 0: + if offset + len > buf.capacity: + raise newException(ValueError, "ZeroCopy readString: buffer overflow") result = newString(len) copyMem(addr result[0], addr buf.data[offset], len) offset += len diff --git a/src/barabadb/query/executor.nim b/src/barabadb/query/executor.nim index 1d5fa90..83ceb11 100644 --- a/src/barabadb/query/executor.nim +++ b/src/barabadb/query/executor.nim @@ -185,6 +185,14 @@ proc newExecutionContext*(db: LSMTree): ExecutionContext = # AST to SQL serializer (for VIEW DDL persistence) # ---------------------------------------------------------------------- +proc sqlEscapeIdent*(ident: string): string = + ## Escape SQL identifiers by doubling double-quotes. + result = ident.replace("\"", "\"\"") + +proc sqlEscapeString*(s: string): string = + ## Escape SQL string literals by doubling single-quotes. + result = s.replace("'", "''") + proc exprToSql(node: Node): string = if node == nil: return "" @@ -200,7 +208,7 @@ proc exprToSql(node: Node): string = of nkNullLit: return "null" of nkIdent: - return node.identName + return "\"" & node.identName.replace("\"", "\"\"") & "\"" of nkStar: return "*" of nkBinOp: @@ -297,8 +305,13 @@ proc restoreSchema(ctx: ExecutionContext) = if not entry.key.startsWith("_schema:"): continue let ddl = cast[string](entry.value) if ddl.len == 0: continue - let tokens = qlex.tokenize(ddl) - let astNode = qpar.parse(tokens) + var astNode: Node + try: + let tokens = qlex.tokenize(ddl) + astNode = qpar.parse(tokens) + except: + # Skip corrupted schema entries during startup + continue if astNode.stmts.len > 0: let stmt = astNode.stmts[0] case stmt.kind @@ -992,9 +1005,15 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext = if expr.binRight.kind == irekSubquery: let subRows = executePlan(ctx, expr.binRight.subqueryPlan) for row in subRows: + # Compare against the first non-internal column only (SQL semantics) + var firstVal = "" + var found = false for k, v in row: if k.startsWith("$"): continue - if v == left: return "true" + firstVal = v + found = true + break + if found and firstVal == left: return "true" return "false" try: let lv = parseFloat(left) @@ -1006,9 +1025,15 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext = if expr.binRight.kind == irekSubquery: let subRows = executePlan(ctx, expr.binRight.subqueryPlan) for row in subRows: + # Compare against the first non-internal column only (SQL semantics) + var firstVal = "" + var found = false for k, v in row: if k.startsWith("$"): continue - if v == left: return "false" + firstVal = v + found = true + break + if found and firstVal == left: return "false" return "true" try: let lv = parseFloat(left) @@ -1334,23 +1359,43 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext = if sql.len == 0: return "" - # Validate by trying EXPLAIN or LIMIT-wrapped query - var validateSql = sql - if validateSql.toLower().startsWith("select"): - validateSql = "SELECT * FROM (" & sql & ") LIMIT 0" - let tokens = qlex.tokenize(validateSql) - let astNode = qpar.parse(tokens) - if astNode.stmts.len > 0: - let validateRes = executeQuery(ctx, astNode) - if not validateRes.success: - # Self-correction: send error back to LLM - let correctionPrompt = "Schema:\n" & schemaInfo & "\nQuestion: " & question & "\n\nPrevious SQL: " & sql & "\n\nError: " & validateRes.message & "\n\nGenerate corrected SQL:" + # Validate by parsing only (never execute LLM-generated SQL directly) + let sqlLower = sql.toLower().strip() + let isSafeQuery = sqlLower.startsWith("select") or sqlLower.startsWith("explain") or + sqlLower.startsWith("with") + let allowDml = ctx.sessionVars.getOrDefault("nl_to_sql.allow_dml", "false") == "true" + if not isSafeQuery and not allowDml: + # For non-SELECT: only do syntax validation via tokenize+parse, no execution + let tokens = qlex.tokenize(sql) + let astNode = qpar.parse(tokens) + if astNode.stmts.len > 0: + return sql + else: + let correctionPrompt = "Schema:\n" & schemaInfo & "\nQuestion: " & question & "\n\nPrevious SQL: " & sql & "\n\nError: Syntax error. Generate corrected SQL:" var correctedResponse = llmmod.generate(ctx.llmClient, correctionPrompt, systemPrompt) var correctedSql = llmmod.extractSQL(correctedResponse) if correctedSql.len > 0: return correctedSql - - return sql + return "" + else: + # For SELECT/EXPLAIN or when ALLOW_DML is set: validate with LIMIT 0 or EXPLAIN + var validateSql = sql + if sqlLower.startsWith("select"): + validateSql = "SELECT * FROM (" & sql & ") LIMIT 0" + elif sqlLower.startsWith("with"): + validateSql = "WITH _cte_ AS (" & sql & ") SELECT 1 LIMIT 0" + let tokens = qlex.tokenize(validateSql) + let astNode = qpar.parse(tokens) + if astNode.stmts.len > 0: + let validateRes = executeQuery(ctx, astNode) + if not validateRes.success: + # Self-correction: send error back to LLM + let correctionPrompt = "Schema:\n" & schemaInfo & "\nQuestion: " & question & "\n\nPrevious SQL: " & sql & "\n\nError: " & validateRes.message & "\n\nGenerate corrected SQL:" + var correctedResponse = llmmod.generate(ctx.llmClient, correctionPrompt, systemPrompt) + var correctedSql = llmmod.extractSQL(correctedResponse) + if correctedSql.len > 0: + return correctedSql + return sql of "schema_prompt": if expr.irFuncArgs.len < 1: return "" let table = evalExpr(expr.irFuncArgs[0], row, ctx) @@ -1358,33 +1403,33 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext = return "Table '" & table & "' not found" let tbl = ctx.tables[table] - var result = "" - result.add("CREATE TABLE " & table & " (\n") + var ddl = "" + ddl.add("CREATE TABLE " & table & " (\n") for i, col in tbl.columns: - result.add(" " & col.name & " " & col.colType) - if col.isPk: result.add(" PRIMARY KEY") - if col.isNotNull: result.add(" NOT NULL") - if col.autoIncrement: result.add(" AUTO_INCREMENT") + ddl.add(" " & col.name & " " & col.colType) + if col.isPk: ddl.add(" PRIMARY KEY") + if col.isNotNull: ddl.add(" NOT NULL") + if col.autoIncrement: ddl.add(" AUTO_INCREMENT") if col.fkTable.len > 0: - result.add(" REFERENCES " & col.fkTable & "(" & col.fkColumn & ")") - if i < tbl.columns.len - 1: result.add(",") - result.add("\n") + ddl.add(" REFERENCES " & col.fkTable & "(" & col.fkColumn & ")") + if i < tbl.columns.len - 1: ddl.add(",") + ddl.add("\n") # Sample data var kvPairs: seq[(string, seq[byte])] = @[] let rows = execScan(ctx, table) let sampleLimit = min(5, rows.len) if sampleLimit > 0: - result.add(");\n\n-- Sample data:\n") + ddl.add(");\n\n-- Sample data:\n") for i in 0.. 0: - result.add("\n-- Indexes: " & idxList.join(", ")) + ddl.add("\n-- Indexes: " & idxList.join(", ")) # RLS policies if table in ctx.policies and ctx.policies[table].len > 0: - result.add("\n-- RLS Policies:\n") + ddl.add("\n-- RLS Policies:\n") for pol in ctx.policies[table]: - result.add("-- CREATE POLICY " & pol.name & " FOR " & pol.command & "\n") + ddl.add("-- CREATE POLICY " & pol.name & " FOR " & pol.command & "\n") if tbl.foreignKeys.len > 0: - result.add("\n-- Foreign Keys:\n") + ddl.add("\n-- Foreign Keys:\n") for fk in tbl.foreignKeys: - result.add("-- " & fk.refTable & "(" & fk.refColumn & ") ON DELETE " & fk.onDelete & "\n") + ddl.add("-- " & fk.refTable & "(" & fk.refColumn & ") ON DELETE " & fk.onDelete & "\n") - return result + return ddl of "similarity_nodes": if expr.irFuncArgs.len < 1: return "[]" let graphName = evalExpr(expr.irFuncArgs[0], row, ctx) @@ -1508,7 +1553,6 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext = finally: release(ctx.sharedLock.lock) return "0" - return "0" of "snowflake_id": # Snowflake ID: timestamp_ms(41 bits) | node_id(10 bits) | sequence(12 bits) var nodeId: int64 = 0 @@ -1662,20 +1706,30 @@ proc execInsert*(ctx: ExecutionContext, table: string, fields: seq[string], valu kvPairs: var seq[(string, seq[byte])]): int = if not hasPrivilege(ctx, table, "INSERT"): return 0 + let tblDef = if table in ctx.tables: ctx.tables[table] else: TableDef() var count = 0 for rowVals in values: var key = "" var keyFound = false var valParts: seq[string] = @[] + # Build composite PK key from all PK columns + if tblDef.pkColumns.len > 0: + var pkParts: seq[string] = @[] + for pkCol in tblDef.pkColumns: + let pkVal = getValue(rowVals, fields, pkCol) + pkParts.add(pkCol & "=" & escapeRowVal(pkVal)) + key = pkParts.join(":") + keyFound = true for i, f in fields: if i < rowVals.len: if not keyFound: key = f & "=" & escapeRowVal(rowVals[i]) keyFound = true - else: + elif tblDef.pkColumns.len == 0 or f.toLower() notin tblDef.pkColumns.mapIt(it.toLower()): valParts.add(f & "=" & escapeRowVal(rowVals[i])) elif f.len > 0: - valParts.add(f & "=") + if tblDef.pkColumns.len == 0 or f.toLower() notin tblDef.pkColumns.mapIt(it.toLower()): + valParts.add(f & "=") let valStr = valParts.join(",") let fullKey = table & "." & key @@ -2107,32 +2161,24 @@ proc validateConstraints*(ctx: ExecutionContext, tableName: string, if not typeOk: return (false, typeErr) - # FK check + # FK check — uses LSM get which searches memtable + SSTables if col.fkTable.len > 0 and col.fkColumn.len > 0 and not isNull(val): let fkKey = col.fkTable & "." & col.fkColumn & "=" & val let (fkExists, _) = ctx.db.get(fkKey) if not fkExists: - # Also check if value is in any row's first field - var found = false - let prefix = col.fkTable & "." - for entry in ctx.db.scanMemTable(): - if entry.deleted: continue - if entry.key.startsWith(prefix): - let rest = entry.key[prefix.len..^1] - if rest.startsWith(col.fkColumn & "=") and rest[col.fkColumn.len+1..^1] == val: - found = true - break - if not found: - return (false, "FOREIGN KEY violation: '" & val & "' not found in " & col.fkTable & "." & col.fkColumn) + return (false, "FOREIGN KEY violation: '" & val & "' not found in " & col.fkTable & "." & col.fkColumn) # PK uniqueness (skip during UPDATE — PK shouldn't change) if not skipPkCheck and tbl.pkColumns.len > 0: var pkVals: seq[string] = @[] + var pkParts: seq[string] = @[] for pkCol in tbl.pkColumns: - pkVals.add(getValue(rowVals, fields, pkCol)) + let pkVal = getValue(rowVals, fields, pkCol) + pkVals.add(pkVal) + pkParts.add(pkCol & "=" & escapeRowVal(pkVal)) let pkStr = pkVals.join("|") - # Check with field=value format (as stored by execInsert) - var pkKey = tableName & "." & tbl.pkColumns[0] & "=" & escapeRowVal(pkVals[0]) + # Check with composite PK format (as stored by execInsert) + let pkKey = tableName & "." & pkParts.join(":") let (exists, _) = ctx.db.get(pkKey) if exists: return (false, "UNIQUE constraint violated: duplicate key '" & pkStr & "'") @@ -4739,7 +4785,7 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu ctx.views[stmt.cvName] = stmt.cvQuery let viewKey = "_schema:views:" & stmt.cvName let viewSql = selectToSql(stmt.cvQuery) - let viewDdl = "CREATE VIEW " & stmt.cvName & " AS " & viewSql + let viewDdl = "CREATE VIEW \"" & sqlEscapeIdent(stmt.cvName) & "\" AS " & viewSql ctx.db.put(viewKey, cast[seq[byte]](viewDdl)) return okResult(msg="CREATE VIEW " & stmt.cvName) @@ -4762,7 +4808,7 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu ctx.tables[stmt.trigTable].triggers = triggers # Persist trigger to LSM-Tree let trigKey = "_schema:triggers:" & stmt.trigTable & ":" & stmt.trigName - let trigDdl = "CREATE TRIGGER " & stmt.trigName & " ON " & stmt.trigTable & " " & + let trigDdl = "CREATE TRIGGER \"" & sqlEscapeIdent(stmt.trigName) & "\" ON \"" & sqlEscapeIdent(stmt.trigTable) & "\" " & stmt.trigTiming & " " & stmt.trigEvent & " AS " & stmt.trigAction.strVal ctx.db.put(trigKey, cast[seq[byte]](trigDdl)) return okResult(msg="CREATE TRIGGER " & stmt.trigName) @@ -5031,7 +5077,7 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu ctx.users[stmt.cuName] = UserDef(name: stmt.cuName, passwordHash: stmt.cuPassword, isSuperuser: stmt.cuSuperuser, roles: @[]) let userKey = "_schema:users:" & stmt.cuName - let userDdl = "CREATE USER " & stmt.cuName & " WITH PASSWORD '" & stmt.cuPassword & "'" & + let userDdl = "CREATE USER \"" & sqlEscapeIdent(stmt.cuName) & "\" WITH PASSWORD '" & sqlEscapeString(stmt.cuPassword) & "'" & (if stmt.cuSuperuser: " SUPERUSER" else: " NOSUPERUSER") ctx.db.put(userKey, cast[seq[byte]](userDdl)) return okResult(msg="CREATE USER " & stmt.cuName) @@ -5050,7 +5096,7 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu withCheckExpr: stmt.cpWithCheck)) ctx.policies[stmt.cpTable] = pols let polKey = "_schema:policies:" & stmt.cpTable & ":" & stmt.cpName - var polDdl = "CREATE POLICY " & stmt.cpName & " ON " & stmt.cpTable + var polDdl = "CREATE POLICY \"" & sqlEscapeIdent(stmt.cpName) & "\" ON \"" & sqlEscapeIdent(stmt.cpTable) & "\"" if stmt.cpCommand != "ALL": polDdl.add(" FOR " & stmt.cpCommand) if stmt.cpUsing != nil: diff --git a/src/barabadb/query/parser.nim b/src/barabadb/query/parser.nim index 74b1f62..2ce964e 100644 --- a/src/barabadb/query/parser.nim +++ b/src/barabadb/query/parser.nim @@ -181,8 +181,8 @@ proc parsePrimary(p: var Parser): Node = Node(kind: nkCase, caseExpr: caseExpr, caseWhens: whens, caseElse: elseExpr, line: tok.line, col: tok.col) else: - discard p.advance() - Node(kind: nkNullLit, line: tok.line, col: tok.col) + raise newException(ValueError, + "Unexpected token " & $tok.kind & " at line " & $tok.line & ", col " & $tok.col) proc parseOverClause(p: var Parser): Node = ## Parse OVER ( [PARTITION BY expr, ...] [ORDER BY expr [ASC|DESC], ...] [frame] ) @@ -324,7 +324,10 @@ proc parseComparison(p: var Parser): Node = if p.peek().kind == tkNot: discard p.advance() negated = true - discard p.advance() # consume NULL token (assumed) + if p.peek().kind != tkNull: + raise newException(ValueError, + "Expected NULL after IS " & (if negated: "NOT " else: "") & "at line " & $tok.line) + discard p.advance() # consume NULL token return Node(kind: nkIsExpr, isExpr: result, isNegated: negated, line: tok.line, col: tok.col) while p.peek().kind in {tkEq, tkNotEq, tkLt, tkLtEq, tkGt, tkGtEq, tkFtsMatch, tkDistanceOp, tkJsonContains, tkJsonContainedBy, tkJsonHasAny, tkJsonHasAll}: diff --git a/src/barabadb/storage/bloom.nim b/src/barabadb/storage/bloom.nim index 8b534de..0f2b038 100644 --- a/src/barabadb/storage/bloom.nim +++ b/src/barabadb/storage/bloom.nim @@ -81,7 +81,7 @@ proc deserialize*(bf: var BloomFilter, data: seq[byte]) = ) let numBytes = (bf.size + 7) div 8 if data.len < 8 + numBytes: - return + raise newException(ValueError, "Bloom filter data too short: expected " & $(8 + numBytes) & " bytes, got " & $data.len) for i in 0..^DvA*m&k&G^3gIF1|E;~xIrB(Tt@qyV-+M2^ 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 0: + db.flushUnsafe() + db.immutableMem = db.memTable + db.memTable = newMemTable(db.memMaxSize) + discard db.memTable.put(key, value, ts, deleted) + +proc deleteUnsafe*(db: LSMTree, key: string) = + putUnsafe(db, key, @[], deleted = true) + proc getUnsafe(db: LSMTree, key: string): (bool, seq[byte]) = let (found, entry) = db.memTable.get(key) if found: @@ -478,6 +493,9 @@ proc flush*(db: LSMTree) = proc close*(db: LSMTree) = acquire(db.lock) defer: release(db.lock) + # Flush both memtables to avoid data loss + while db.immutableMem.len > 0: + flushUnsafe(db) flushUnsafe(db) for sst in db.sstables.mitems: sst.close() @@ -531,7 +549,8 @@ proc scanAll*(db: LSMTree): seq[(string, seq[byte])] = result.add((e.key, e.value)) # Scan SSTables from newest to oldest - for sst in db.sstables: + for i in countdown(db.sstables.high, db.sstables.low): + let sst = db.sstables[i] for key, offset in sst.index: if key notin seen: seen[key] = true diff --git a/src/barabadb/storage/mmap.nim b/src/barabadb/storage/mmap.nim index 477b7e8..bc16656 100644 --- a/src/barabadb/storage/mmap.nim +++ b/src/barabadb/storage/mmap.nim @@ -54,14 +54,18 @@ proc openMmap*(path: string, mode: MmapMode = mmReadOnly): MmapFile = let mapped = mmap(nil, fileSize, prot, flags, fd, 0) if mapped == MAP_FAILED: - discard close(fd) + discard posix.close(fd) return MmapFile(path: path, regions: @[], totalSize: 0, pageSize: PageSize) + # Kernel keeps the mapping alive independently of the fd; close it now + # to avoid leaking one fd per SSTable. + discard posix.close(fd) + let region = MmapRegion( data: cast[ptr UncheckedArray[byte]](mapped), size: fileSize, offset: 0, - fd: fd, + fd: -1, mode: mode, ) @@ -116,16 +120,21 @@ proc adviseRandom*(mf: MmapFile) = proc adviseWillNeed*(mf: MmapFile, offset: int, size: int) = if mf.regions.len > 0: + if offset < 0 or offset + size > mf.regions[0].size: + return discard madvise(addr mf.regions[0].data[offset], size, MADV_WILLNEED) proc adviseDontNeed*(mf: MmapFile, offset: int, size: int) = if mf.regions.len > 0: + if offset < 0 or offset + size > mf.regions[0].size: + return discard madvise(addr mf.regions[0].data[offset], size, MADV_DONTNEED) proc close*(mf: MmapFile) = for region in mf.regions: discard munmap(region.data, region.size) - discard close(cint(region.fd)) + if region.fd != -1: + discard close(cint(region.fd)) mf.regions.setLen(0) proc size*(mf: MmapFile): int = mf.totalSize diff --git a/src/barabadb/storage/recovery.nim b/src/barabadb/storage/recovery.nim index e74950b..daed440 100644 --- a/src/barabadb/storage/recovery.nim +++ b/src/barabadb/storage/recovery.nim @@ -32,6 +32,7 @@ type dataDir*: string entries*: seq[RecoveredEntry] result*: RecoveryResult + lastTxnId*: uint64 # tracks commits seen in WAL proc newCrashRecovery*(walDir: string, dataDir: string): CrashRecovery = CrashRecovery( @@ -101,6 +102,7 @@ proc scanWAL*(rec: CrashRecovery): seq[RecoveredEntry] = discard stream.close() + rec.lastTxnId = txnId proc analyze*(rec: CrashRecovery): RecoveryResult = rec.entries = rec.scanWAL() @@ -109,10 +111,7 @@ proc analyze*(rec: CrashRecovery): RecoveryResult = rec.result = RecoveryResult(state: recDone, applied: false) return rec.result - var lastCommitted: uint64 = 0 - for entry in rec.entries: - if entry.txnId > lastCommitted: - lastCommitted = entry.txnId + var lastCommitted = rec.lastTxnId var redoCount = 0 var undoCount = 0 @@ -149,11 +148,11 @@ proc recover*(rec: CrashRecovery, db: LSMTree = nil): RecoveryResult = var undoCount = 0 for entry in rec.entries: if entry.txnId < analysis.lastTxn: - # Committed — redo + # Committed — redo (bypass WAL to avoid duplicate entries) if entry.isDelete: - db.delete(entry.key) + db.deleteUnsafe(entry.key) else: - db.put(entry.key, entry.value) + db.putUnsafe(entry.key, entry.value) inc redoCount else: # Uncommitted — skip (undo) diff --git a/src/barabadb/storage/wal.nim b/src/barabadb/storage/wal.nim index 87ea0d5..dbe26bb 100644 --- a/src/barabadb/storage/wal.nim +++ b/src/barabadb/storage/wal.nim @@ -30,10 +30,11 @@ proc newWriteAheadLog*(dir: string, syncOnWrite: bool = true): WriteAheadLog = createDir(dir) let path = dir / "wal.log" let exists = fileExists(path) + let isEmpty = if exists: getFileSize(path) == 0 else: true let stream = if exists: newFileStream(path, fmAppend) else: newFileStream(path, fmWrite) if stream == nil: raise newException(IOError, "Cannot open WAL: " & path) - if not exists: + if not exists or isEmpty: stream.write(WALMagic) stream.write(WALVersion) stream.flush() @@ -78,14 +79,17 @@ proc writeCommit*(wal: var WriteAheadLog, timestamp: uint64) = proc sync*(wal: var WriteAheadLog) = wal.stream.flush() - let fd = posix.open(cstring(wal.path), O_RDONLY) + # Re-open with O_RDWR so fsync operates on a write-capable fd. + # Not ideal (two fds for same file) but avoids accessing private + # FileStream internals that vary across Nim versions. + let fd = posix.open(cstring(wal.path), O_RDWR) if fd != -1: discard posix.fsync(fd) discard posix.close(fd) proc close*(wal: var WriteAheadLog) = wal.stream.flush() - let fd = posix.open(cstring(wal.path), O_RDONLY) + let fd = posix.open(cstring(wal.path), O_RDWR) if fd != -1: discard posix.fsync(fd) discard posix.close(fd) @@ -100,10 +104,10 @@ proc readEntries*(walPath: string, untilTimestamp: uint64 = 0): seq[WalEntry] = let s = newFileStream(walPath, fmRead) if s == nil: return # Skip header - var magic: uint32 - var version: uint32 - discard s.readData(addr magic, 4) - discard s.readData(addr version, 4) + var magic: uint32 = 0 + var version: uint32 = 0 + if s.readData(addr magic, 4) != 4: return + if s.readData(addr version, 4) != 4: return if magic != WALMagic: return while not s.atEnd: var kind: uint8 diff --git a/tests/fuzz_test b/tests/fuzz_test new file mode 100755 index 0000000000000000000000000000000000000000..42145751486f0e3485db00df128c4b869b2a22ea GIT binary patch literal 728680 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