mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-23 21:03:08 +09:00
632 lines
16 KiB
C++
632 lines
16 KiB
C++
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#ifndef _MDB_
|
|
#include "mdb.h"
|
|
#endif
|
|
|
|
#ifndef _MORK_
|
|
#include "mork.h"
|
|
#endif
|
|
|
|
#ifndef _MORKNODE_
|
|
#include "morkNode.h"
|
|
#endif
|
|
|
|
#ifndef _MORKMAP_
|
|
#include "morkMap.h"
|
|
#endif
|
|
|
|
#ifndef _MORKSPACE_
|
|
#include "morkSpace.h"
|
|
#endif
|
|
|
|
#ifndef _MORKNODEMAP_
|
|
#include "morkNodeMap.h"
|
|
#endif
|
|
|
|
#ifndef _MORKROWMAP_
|
|
#include "morkRowMap.h"
|
|
#endif
|
|
|
|
#ifndef _MORKENV_
|
|
#include "morkEnv.h"
|
|
#endif
|
|
|
|
#ifndef _MORKROWSPACE_
|
|
#include "morkRowSpace.h"
|
|
#endif
|
|
|
|
#ifndef _MORKPOOL_
|
|
#include "morkPool.h"
|
|
#endif
|
|
|
|
#ifndef _MORKSTORE_
|
|
#include "morkStore.h"
|
|
#endif
|
|
|
|
#ifndef _MORKTABLE_
|
|
#include "morkTable.h"
|
|
#endif
|
|
|
|
#ifndef _MORKROW_
|
|
#include "morkRow.h"
|
|
#endif
|
|
|
|
#ifndef _MORKATOMMAP_
|
|
#include "morkAtomMap.h"
|
|
#endif
|
|
|
|
#ifndef _MORKROWOBJECT_
|
|
#include "morkRowObject.h"
|
|
#endif
|
|
|
|
//3456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
|
|
|
|
// ````` ````` ````` ````` `````
|
|
// { ===== begin morkNode interface =====
|
|
|
|
/*public virtual*/ void
|
|
morkRowSpace::CloseMorkNode(morkEnv* ev) // CloseRowSpace() only if open
|
|
{
|
|
if ( this->IsOpenNode() )
|
|
{
|
|
this->MarkClosing();
|
|
this->CloseRowSpace(ev);
|
|
this->MarkShut();
|
|
}
|
|
}
|
|
|
|
/*public virtual*/
|
|
morkRowSpace::~morkRowSpace() // assert CloseRowSpace() executed earlier
|
|
{
|
|
MORK_ASSERT(this->IsShutNode());
|
|
}
|
|
|
|
/*public non-poly*/
|
|
morkRowSpace::morkRowSpace(morkEnv* ev,
|
|
const morkUsage& inUsage, mork_scope inScope, morkStore* ioStore,
|
|
nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap)
|
|
: morkSpace(ev, inUsage, inScope, ioStore, ioHeap, ioSlotHeap)
|
|
, mRowSpace_SlotHeap( ioSlotHeap )
|
|
, mRowSpace_Rows(ev, morkUsage::kMember, (nsIMdbHeap*) 0, ioSlotHeap,
|
|
morkRowSpace_kStartRowMapSlotCount)
|
|
, mRowSpace_Tables(ev, morkUsage::kMember, (nsIMdbHeap*) 0, ioSlotHeap)
|
|
, mRowSpace_NextTableId( 1 )
|
|
, mRowSpace_NextRowId( 1 )
|
|
|
|
, mRowSpace_IndexCount( 0 )
|
|
{
|
|
morkAtomRowMap** cache = mRowSpace_IndexCache;
|
|
morkAtomRowMap** cacheEnd = cache + morkRowSpace_kPrimeCacheSize;
|
|
while ( cache < cacheEnd )
|
|
*cache++ = 0; // put nil into every slot of cache table
|
|
|
|
if ( ev->Good() )
|
|
{
|
|
if ( ioSlotHeap )
|
|
{
|
|
mNode_Derived = morkDerived_kRowSpace;
|
|
|
|
// the morkSpace base constructor handles any dirty propagation
|
|
}
|
|
else
|
|
ev->NilPointerError();
|
|
}
|
|
}
|
|
|
|
/*public non-poly*/ void
|
|
morkRowSpace::CloseRowSpace(morkEnv* ev) // called by CloseMorkNode();
|
|
{
|
|
if ( this->IsNode() )
|
|
{
|
|
morkAtomRowMap** cache = mRowSpace_IndexCache;
|
|
morkAtomRowMap** cacheEnd = cache + morkRowSpace_kPrimeCacheSize;
|
|
--cache; // prepare for preincrement:
|
|
while ( ++cache < cacheEnd )
|
|
{
|
|
if ( *cache )
|
|
morkAtomRowMap::SlotStrongAtomRowMap(0, ev, cache);
|
|
}
|
|
|
|
mRowSpace_Tables.CloseMorkNode(ev);
|
|
|
|
morkStore* store = mSpace_Store;
|
|
if ( store )
|
|
this->CutAllRows(ev, &store->mStore_Pool);
|
|
|
|
mRowSpace_Rows.CloseMorkNode(ev);
|
|
this->CloseSpace(ev);
|
|
}
|
|
else
|
|
this->NonNodeError(ev);
|
|
}
|
|
|
|
// } ===== end morkNode methods =====
|
|
// ````` ````` ````` ````` `````
|
|
|
|
/*static*/ void
|
|
morkRowSpace::NonRowSpaceTypeError(morkEnv* ev)
|
|
{
|
|
ev->NewError("non morkRowSpace");
|
|
}
|
|
|
|
/*static*/ void
|
|
morkRowSpace::ZeroKindError(morkEnv* ev)
|
|
{
|
|
ev->NewError("zero table kind");
|
|
}
|
|
|
|
/*static*/ void
|
|
morkRowSpace::ZeroScopeError(morkEnv* ev)
|
|
{
|
|
ev->NewError("zero row scope");
|
|
}
|
|
|
|
/*static*/ void
|
|
morkRowSpace::ZeroTidError(morkEnv* ev)
|
|
{
|
|
ev->NewError("zero table ID");
|
|
}
|
|
|
|
/*static*/ void
|
|
morkRowSpace::MinusOneRidError(morkEnv* ev)
|
|
{
|
|
ev->NewError("row ID is -1");
|
|
}
|
|
|
|
///*static*/ void
|
|
//morkRowSpace::ExpectAutoIdOnlyError(morkEnv* ev)
|
|
//{
|
|
// ev->NewError("zero row ID");
|
|
//}
|
|
|
|
///*static*/ void
|
|
//morkRowSpace::ExpectAutoIdNeverError(morkEnv* ev)
|
|
//{
|
|
//}
|
|
|
|
mork_num
|
|
morkRowSpace::CutAllRows(morkEnv* ev, morkPool* ioPool)
|
|
{
|
|
if ( this->IsRowSpaceClean() )
|
|
this->MaybeDirtyStoreAndSpace();
|
|
|
|
mork_num outSlots = mRowSpace_Rows.MapFill();
|
|
|
|
#ifdef MORK_ENABLE_ZONE_ARENAS
|
|
MORK_USED_2(ev, ioPool);
|
|
return 0;
|
|
#else /*MORK_ENABLE_ZONE_ARENAS*/
|
|
morkZone* zone = &mSpace_Store->mStore_Zone;
|
|
morkRow* r = 0; // old key row in the map
|
|
mork_change* c = 0;
|
|
|
|
#ifdef MORK_ENABLE_PROBE_MAPS
|
|
morkRowProbeMapIter i(ev, &mRowSpace_Rows);
|
|
#else /*MORK_ENABLE_PROBE_MAPS*/
|
|
morkRowMapIter i(ev, &mRowSpace_Rows);
|
|
#endif /*MORK_ENABLE_PROBE_MAPS*/
|
|
|
|
for ( c = i.FirstRow(ev, &r); c && ev->Good();
|
|
c = i.NextRow(ev, &r) )
|
|
{
|
|
if ( r )
|
|
{
|
|
if ( r->IsRow() )
|
|
{
|
|
if ( r->mRow_Object )
|
|
{
|
|
morkRowObject::SlotWeakRowObject((morkRowObject*) 0, ev,
|
|
&r->mRow_Object);
|
|
}
|
|
ioPool->ZapRow(ev, r, zone);
|
|
}
|
|
else
|
|
r->NonRowTypeWarning(ev);
|
|
}
|
|
else
|
|
ev->NilPointerError();
|
|
|
|
#ifdef MORK_ENABLE_PROBE_MAPS
|
|
// cut nothing from the map
|
|
#else /*MORK_ENABLE_PROBE_MAPS*/
|
|
i.CutHereRow(ev, /*key*/ (morkRow**) 0);
|
|
#endif /*MORK_ENABLE_PROBE_MAPS*/
|
|
}
|
|
#endif /*MORK_ENABLE_ZONE_ARENAS*/
|
|
|
|
|
|
return outSlots;
|
|
}
|
|
|
|
morkTable*
|
|
morkRowSpace::FindTableByKind(morkEnv* ev, mork_kind inTableKind)
|
|
{
|
|
if ( inTableKind )
|
|
{
|
|
#ifdef MORK_BEAD_OVER_NODE_MAPS
|
|
|
|
morkTableMapIter i(ev, &mRowSpace_Tables);
|
|
morkTable* table = i.FirstTable(ev);
|
|
for ( ; table && ev->Good(); table = i.NextTable(ev) )
|
|
|
|
#else /*MORK_BEAD_OVER_NODE_MAPS*/
|
|
mork_tid* key = 0; // nil pointer to suppress key access
|
|
morkTable* table = 0; // old table in the map
|
|
|
|
mork_change* c = 0;
|
|
morkTableMapIter i(ev, &mRowSpace_Tables);
|
|
for ( c = i.FirstTable(ev, key, &table); c && ev->Good();
|
|
c = i.NextTable(ev, key, &table) )
|
|
#endif /*MORK_BEAD_OVER_NODE_MAPS*/
|
|
{
|
|
if ( table->mTable_Kind == inTableKind )
|
|
return table;
|
|
}
|
|
}
|
|
else
|
|
this->ZeroKindError(ev);
|
|
|
|
return (morkTable*) 0;
|
|
}
|
|
|
|
morkTable*
|
|
morkRowSpace::NewTableWithTid(morkEnv* ev, mork_tid inTid,
|
|
mork_kind inTableKind,
|
|
const mdbOid* inOptionalMetaRowOid) // can be nil to avoid specifying
|
|
{
|
|
morkTable* outTable = 0;
|
|
morkStore* store = mSpace_Store;
|
|
|
|
if ( inTableKind && store )
|
|
{
|
|
mdb_bool mustBeUnique = morkBool_kFalse;
|
|
nsIMdbHeap* heap = store->mPort_Heap;
|
|
morkTable* table = new(*heap, ev)
|
|
morkTable(ev, morkUsage::kHeap, heap, store, heap, this,
|
|
inOptionalMetaRowOid, inTid, inTableKind, mustBeUnique);
|
|
if ( table )
|
|
{
|
|
if ( mRowSpace_Tables.AddTable(ev, table) )
|
|
{
|
|
outTable = table;
|
|
if ( mRowSpace_NextTableId <= inTid )
|
|
mRowSpace_NextTableId = inTid + 1;
|
|
}
|
|
|
|
if ( this->IsRowSpaceClean() && store->mStore_CanDirty )
|
|
this->MaybeDirtyStoreAndSpace(); // morkTable does already
|
|
|
|
}
|
|
}
|
|
else if ( store )
|
|
this->ZeroKindError(ev);
|
|
else
|
|
this->NilSpaceStoreError(ev);
|
|
|
|
return outTable;
|
|
}
|
|
|
|
morkTable*
|
|
morkRowSpace::NewTable(morkEnv* ev, mork_kind inTableKind,
|
|
mdb_bool inMustBeUnique,
|
|
const mdbOid* inOptionalMetaRowOid) // can be nil to avoid specifying
|
|
{
|
|
morkTable* outTable = 0;
|
|
morkStore* store = mSpace_Store;
|
|
|
|
if ( inTableKind && store )
|
|
{
|
|
if ( inMustBeUnique ) // need to look for existing table first?
|
|
outTable = this->FindTableByKind(ev, inTableKind);
|
|
|
|
if ( !outTable && ev->Good() )
|
|
{
|
|
mork_tid id = this->MakeNewTableId(ev);
|
|
if ( id )
|
|
{
|
|
nsIMdbHeap* heap = mSpace_Store->mPort_Heap;
|
|
morkTable* table = new(*heap, ev)
|
|
morkTable(ev, morkUsage::kHeap, heap, mSpace_Store, heap, this,
|
|
inOptionalMetaRowOid, id, inTableKind, inMustBeUnique);
|
|
if ( table )
|
|
{
|
|
if ( mRowSpace_Tables.AddTable(ev, table) )
|
|
outTable = table;
|
|
else
|
|
table->Release();
|
|
|
|
if ( this->IsRowSpaceClean() && store->mStore_CanDirty )
|
|
this->MaybeDirtyStoreAndSpace(); // morkTable does already
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if ( store )
|
|
this->ZeroKindError(ev);
|
|
else
|
|
this->NilSpaceStoreError(ev);
|
|
|
|
return outTable;
|
|
}
|
|
|
|
mork_tid
|
|
morkRowSpace::MakeNewTableId(morkEnv* ev)
|
|
{
|
|
mork_tid outTid = 0;
|
|
mork_tid id = mRowSpace_NextTableId;
|
|
mork_num count = 9; // try up to eight times
|
|
|
|
while ( !outTid && --count ) // still trying to find an unused table ID?
|
|
{
|
|
if ( !mRowSpace_Tables.GetTable(ev, id) )
|
|
outTid = id;
|
|
else
|
|
{
|
|
MORK_ASSERT(morkBool_kFalse); // alert developer about ID problems
|
|
++id;
|
|
}
|
|
}
|
|
|
|
mRowSpace_NextTableId = id + 1;
|
|
return outTid;
|
|
}
|
|
|
|
#define MAX_AUTO_ID (morkRow_kMinusOneRid - 2)
|
|
mork_rid
|
|
morkRowSpace::MakeNewRowId(morkEnv* ev)
|
|
{
|
|
mork_rid outRid = 0;
|
|
mork_rid id = mRowSpace_NextRowId;
|
|
mork_num count = 9; // try up to eight times
|
|
mdbOid oid;
|
|
oid.mOid_Scope = this->SpaceScope();
|
|
|
|
// still trying to find an unused row ID?
|
|
while (!outRid && --count && id <= MAX_AUTO_ID)
|
|
{
|
|
oid.mOid_Id = id;
|
|
if ( !mRowSpace_Rows.GetOid(ev, &oid) )
|
|
outRid = id;
|
|
else
|
|
{
|
|
MORK_ASSERT(morkBool_kFalse); // alert developer about ID problems
|
|
++id;
|
|
}
|
|
}
|
|
|
|
if (id < MAX_AUTO_ID)
|
|
mRowSpace_NextRowId = id + 1;
|
|
return outRid;
|
|
}
|
|
|
|
morkAtomRowMap*
|
|
morkRowSpace::make_index(morkEnv* ev, mork_column inCol)
|
|
{
|
|
morkAtomRowMap* outMap = 0;
|
|
nsIMdbHeap* heap = mRowSpace_SlotHeap;
|
|
if ( heap ) // have expected heap for allocations?
|
|
{
|
|
morkAtomRowMap* map = new(*heap, ev)
|
|
morkAtomRowMap(ev, morkUsage::kHeap, heap, heap, inCol);
|
|
|
|
if ( map ) // able to create new map index?
|
|
{
|
|
if ( ev->Good() ) // no errors during construction?
|
|
{
|
|
#ifdef MORK_ENABLE_PROBE_MAPS
|
|
morkRowProbeMapIter i(ev, &mRowSpace_Rows);
|
|
#else /*MORK_ENABLE_PROBE_MAPS*/
|
|
morkRowMapIter i(ev, &mRowSpace_Rows);
|
|
#endif /*MORK_ENABLE_PROBE_MAPS*/
|
|
mork_change* c = 0;
|
|
morkRow* row = 0;
|
|
mork_aid aidKey = 0;
|
|
|
|
for ( c = i.FirstRow(ev, &row); c && ev->Good();
|
|
c = i.NextRow(ev, &row) ) // another row in space?
|
|
{
|
|
aidKey = row->GetCellAtomAid(ev, inCol);
|
|
if ( aidKey ) // row has indexed attribute?
|
|
map->AddAid(ev, aidKey, row); // include in map
|
|
}
|
|
}
|
|
if ( ev->Good() ) // no errors constructing index?
|
|
outMap = map; // return from function
|
|
else
|
|
map->CutStrongRef(ev); // discard map on error
|
|
}
|
|
}
|
|
else
|
|
ev->NilPointerError();
|
|
|
|
return outMap;
|
|
}
|
|
|
|
morkAtomRowMap*
|
|
morkRowSpace::ForceMap(morkEnv* ev, mork_column inCol)
|
|
{
|
|
morkAtomRowMap* outMap = this->FindMap(ev, inCol);
|
|
|
|
if ( !outMap && ev->Good() ) // no such existing index?
|
|
{
|
|
if ( mRowSpace_IndexCount < morkRowSpace_kMaxIndexCount )
|
|
{
|
|
morkAtomRowMap* map = this->make_index(ev, inCol);
|
|
if ( map ) // created a new index for col?
|
|
{
|
|
mork_count wrap = 0; // count times wrap-around occurs
|
|
morkAtomRowMap** slot = mRowSpace_IndexCache; // table
|
|
morkAtomRowMap** end = slot + morkRowSpace_kPrimeCacheSize;
|
|
slot += ( inCol % morkRowSpace_kPrimeCacheSize ); // hash
|
|
while ( *slot ) // empty slot not yet found?
|
|
{
|
|
if ( ++slot >= end ) // wrap around?
|
|
{
|
|
slot = mRowSpace_IndexCache; // back to table start
|
|
if ( ++wrap > 1 ) // wrapped more than once?
|
|
{
|
|
ev->NewError("no free cache slots"); // disaster
|
|
break; // end while loop
|
|
}
|
|
}
|
|
}
|
|
if ( ev->Good() ) // everything went just fine?
|
|
{
|
|
++mRowSpace_IndexCount; // note another new map
|
|
*slot = map; // install map in the hash table
|
|
outMap = map; // return the new map from function
|
|
}
|
|
else
|
|
map->CutStrongRef(ev); // discard map on error
|
|
}
|
|
}
|
|
else
|
|
ev->NewError("too many indexes"); // why so many indexes?
|
|
}
|
|
return outMap;
|
|
}
|
|
|
|
morkAtomRowMap*
|
|
morkRowSpace::FindMap(morkEnv* ev, mork_column inCol)
|
|
{
|
|
if ( mRowSpace_IndexCount && ev->Good() )
|
|
{
|
|
mork_count wrap = 0; // count times wrap-around occurs
|
|
morkAtomRowMap** slot = mRowSpace_IndexCache; // table
|
|
morkAtomRowMap** end = slot + morkRowSpace_kPrimeCacheSize;
|
|
slot += ( inCol % morkRowSpace_kPrimeCacheSize ); // hash
|
|
morkAtomRowMap* map = *slot;
|
|
while ( map ) // another used slot to examine?
|
|
{
|
|
if ( inCol == map->mAtomRowMap_IndexColumn ) // found col?
|
|
return map;
|
|
if ( ++slot >= end ) // wrap around?
|
|
{
|
|
slot = mRowSpace_IndexCache;
|
|
if ( ++wrap > 1 ) // wrapped more than once?
|
|
return (morkAtomRowMap*) 0; // stop searching
|
|
}
|
|
map = *slot;
|
|
}
|
|
}
|
|
return (morkAtomRowMap*) 0;
|
|
}
|
|
|
|
morkRow*
|
|
morkRowSpace::FindRow(morkEnv* ev, mork_column inCol, const mdbYarn* inYarn)
|
|
{
|
|
morkRow* outRow = 0;
|
|
|
|
// if yarn hasn't been atomized, there can't be a corresponding row,
|
|
// so pass in false to not create the row - should help history bloat
|
|
morkAtom* atom = mSpace_Store->YarnToAtom(ev, inYarn, false);
|
|
if ( atom ) // have or created an atom corresponding to input yarn?
|
|
{
|
|
mork_aid atomAid = atom->GetBookAtomAid();
|
|
if ( atomAid ) // atom has an identity for use in hash table?
|
|
{
|
|
morkAtomRowMap* map = this->ForceMap(ev, inCol);
|
|
if ( map ) // able to find or create index for col?
|
|
{
|
|
outRow = map->GetAid(ev, atomAid); // search for row
|
|
}
|
|
}
|
|
}
|
|
|
|
return outRow;
|
|
}
|
|
|
|
morkRow*
|
|
morkRowSpace::NewRowWithOid(morkEnv* ev, const mdbOid* inOid)
|
|
{
|
|
morkRow* outRow = mRowSpace_Rows.GetOid(ev, inOid);
|
|
MORK_ASSERT(outRow==0);
|
|
if ( !outRow && ev->Good() )
|
|
{
|
|
morkStore* store = mSpace_Store;
|
|
if ( store )
|
|
{
|
|
morkPool* pool = this->GetSpaceStorePool();
|
|
morkRow* row = pool->NewRow(ev, &store->mStore_Zone);
|
|
if ( row )
|
|
{
|
|
row->InitRow(ev, inOid, this, /*length*/ 0, pool);
|
|
|
|
if ( ev->Good() && mRowSpace_Rows.AddRow(ev, row) )
|
|
{
|
|
outRow = row;
|
|
mork_rid rid = inOid->mOid_Id;
|
|
if ( mRowSpace_NextRowId <= rid )
|
|
mRowSpace_NextRowId = rid + 1;
|
|
}
|
|
else
|
|
pool->ZapRow(ev, row, &store->mStore_Zone);
|
|
|
|
if ( this->IsRowSpaceClean() && store->mStore_CanDirty )
|
|
this->MaybeDirtyStoreAndSpace(); // InitRow() does already
|
|
}
|
|
}
|
|
else
|
|
this->NilSpaceStoreError(ev);
|
|
}
|
|
return outRow;
|
|
}
|
|
|
|
morkRow*
|
|
morkRowSpace::NewRow(morkEnv* ev)
|
|
{
|
|
morkRow* outRow = 0;
|
|
if ( ev->Good() )
|
|
{
|
|
mork_rid id = this->MakeNewRowId(ev);
|
|
if ( id )
|
|
{
|
|
morkStore* store = mSpace_Store;
|
|
if ( store )
|
|
{
|
|
mdbOid oid;
|
|
oid.mOid_Scope = this->SpaceScope();
|
|
oid.mOid_Id = id;
|
|
morkPool* pool = this->GetSpaceStorePool();
|
|
morkRow* row = pool->NewRow(ev, &store->mStore_Zone);
|
|
if ( row )
|
|
{
|
|
row->InitRow(ev, &oid, this, /*length*/ 0, pool);
|
|
|
|
if ( ev->Good() && mRowSpace_Rows.AddRow(ev, row) )
|
|
outRow = row;
|
|
else
|
|
pool->ZapRow(ev, row, &store->mStore_Zone);
|
|
|
|
if ( this->IsRowSpaceClean() && store->mStore_CanDirty )
|
|
this->MaybeDirtyStoreAndSpace(); // InitRow() does already
|
|
}
|
|
}
|
|
else
|
|
this->NilSpaceStoreError(ev);
|
|
}
|
|
}
|
|
return outRow;
|
|
}
|
|
|
|
|
|
//3456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
|
|
|
|
|
|
morkRowSpaceMap::~morkRowSpaceMap()
|
|
{
|
|
}
|
|
|
|
morkRowSpaceMap::morkRowSpaceMap(morkEnv* ev, const morkUsage& inUsage,
|
|
nsIMdbHeap* ioHeap, nsIMdbHeap* ioSlotHeap)
|
|
: morkNodeMap(ev, inUsage, ioHeap, ioSlotHeap)
|
|
{
|
|
if ( ev->Good() )
|
|
mNode_Derived = morkDerived_kRowSpaceMap;
|
|
}
|
|
|
|
//3456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789
|