1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
4 *
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 *
9 * This file incorporates work covered by the following license notice:
10 *
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
18 */
19
20 #include <config_features.h>
21
22 #include <tools/debug.hxx>
23 #include <tools/stream.hxx>
24 #include <basic/sbx.hxx>
25 #include <runtime.hxx>
26
27 #include <boost/optional.hpp>
28
29 using namespace std;
30
31 struct SbxVarEntry
32 {
33 SbxVariableRef mpVar;
34 boost::optional<OUString> maAlias;
35 };
36
37
38 // SbxArray
39
SbxArray(SbxDataType t)40 SbxArray::SbxArray( SbxDataType t ) : SbxBase()
41 {
42 eType = t;
43 if( t != SbxVARIANT )
44 SetFlag( SbxFlagBits::Fixed );
45 }
46
operator =(const SbxArray & rArray)47 SbxArray& SbxArray::operator=( const SbxArray& rArray )
48 {
49 if( &rArray != this )
50 {
51 eType = rArray.eType;
52 Clear();
53 for( const auto& rpSrcRef : rArray.mVarEntries )
54 {
55 SbxVariableRef pSrc_ = rpSrcRef.mpVar;
56 if( !pSrc_.is() )
57 continue;
58
59 if( eType != SbxVARIANT )
60 {
61 // Convert no objects
62 if( eType != SbxOBJECT || pSrc_->GetClass() != SbxClassType::Object )
63 {
64 pSrc_->Convert(eType);
65 }
66 }
67 mVarEntries.push_back( rpSrcRef );
68 }
69 }
70 return *this;
71 }
72
~SbxArray()73 SbxArray::~SbxArray()
74 {
75 }
76
GetType() const77 SbxDataType SbxArray::GetType() const
78 {
79 return static_cast<SbxDataType>( eType | SbxARRAY );
80 }
81
Clear()82 void SbxArray::Clear()
83 {
84 mVarEntries.clear();
85 }
86
Count32() const87 sal_uInt32 SbxArray::Count32() const
88 {
89 return mVarEntries.size();
90 }
91
Count() const92 sal_uInt16 SbxArray::Count() const
93 {
94 sal_uInt32 nCount = mVarEntries.size();
95 DBG_ASSERT( nCount <= SBX_MAXINDEX, "SBX: Array-Index > SBX_MAXINDEX" );
96 return static_cast<sal_uInt16>(nCount);
97 }
98
GetRef32(sal_uInt32 nIdx)99 SbxVariableRef& SbxArray::GetRef32( sal_uInt32 nIdx )
100 {
101 // If necessary extend the array
102 DBG_ASSERT( nIdx <= SBX_MAXINDEX32, "SBX: Array-Index > SBX_MAXINDEX32" );
103 // Very Hot Fix
104 if( nIdx > SBX_MAXINDEX32 )
105 {
106 SetError( ERRCODE_BASIC_OUT_OF_RANGE );
107 nIdx = 0;
108 }
109 if ( mVarEntries.size() <= nIdx )
110 mVarEntries.resize(nIdx+1);
111
112 return mVarEntries[nIdx].mpVar;
113 }
114
GetRef(sal_uInt16 nIdx)115 SbxVariableRef& SbxArray::GetRef( sal_uInt16 nIdx )
116 {
117 // If necessary extend the array
118 DBG_ASSERT( nIdx <= SBX_MAXINDEX, "SBX: Array-Index > SBX_MAXINDEX" );
119 // Very Hot Fix
120 if( nIdx > SBX_MAXINDEX )
121 {
122 SetError( ERRCODE_BASIC_OUT_OF_RANGE );
123 nIdx = 0;
124 }
125 if ( mVarEntries.size() <= nIdx )
126 mVarEntries.resize(nIdx+1);
127
128 return mVarEntries[nIdx].mpVar;
129 }
130
Get32(sal_uInt32 nIdx)131 SbxVariable* SbxArray::Get32( sal_uInt32 nIdx )
132 {
133 if( !CanRead() )
134 {
135 SetError( ERRCODE_BASIC_PROP_WRITEONLY );
136 return nullptr;
137 }
138 SbxVariableRef& rRef = GetRef32( nIdx );
139
140 if ( !rRef.is() )
141 rRef = new SbxVariable( eType );
142
143 return rRef.get();
144 }
145
Get(sal_uInt16 nIdx)146 SbxVariable* SbxArray::Get( sal_uInt16 nIdx )
147 {
148 if( !CanRead() )
149 {
150 SetError( ERRCODE_BASIC_PROP_WRITEONLY );
151 return nullptr;
152 }
153 SbxVariableRef& rRef = GetRef( nIdx );
154
155 if ( !rRef.is() )
156 rRef = new SbxVariable( eType );
157
158 return rRef.get();
159 }
160
Put32(SbxVariable * pVar,sal_uInt32 nIdx)161 void SbxArray::Put32( SbxVariable* pVar, sal_uInt32 nIdx )
162 {
163 if( !CanWrite() )
164 SetError( ERRCODE_BASIC_PROP_READONLY );
165 else
166 {
167 if( pVar )
168 if( eType != SbxVARIANT )
169 // Convert no objects
170 if( eType != SbxOBJECT || pVar->GetClass() != SbxClassType::Object )
171 pVar->Convert( eType );
172 SbxVariableRef& rRef = GetRef32( nIdx );
173 if( rRef.get() != pVar )
174 {
175 rRef = pVar;
176 SetFlag( SbxFlagBits::Modified );
177 }
178 }
179 }
180
Put(SbxVariable * pVar,sal_uInt16 nIdx)181 void SbxArray::Put( SbxVariable* pVar, sal_uInt16 nIdx )
182 {
183 if( !CanWrite() )
184 SetError( ERRCODE_BASIC_PROP_READONLY );
185 else
186 {
187 if( pVar )
188 if( eType != SbxVARIANT )
189 // Convert no objects
190 if( eType != SbxOBJECT || pVar->GetClass() != SbxClassType::Object )
191 pVar->Convert( eType );
192 SbxVariableRef& rRef = GetRef( nIdx );
193 // tdf#122250. It is possible that I hold the last reference to myself, so check, otherwise I might
194 // call SetFlag on myself after I have died.
195 bool removingMyself = rRef.get() && rRef->GetParameters() == this && GetRefCount() == 1;
196 if(rRef.get() != pVar )
197 {
198 rRef = pVar;
199 if (!removingMyself)
200 SetFlag( SbxFlagBits::Modified );
201 }
202 }
203 }
204
GetAlias(sal_uInt16 nIdx)205 OUString SbxArray::GetAlias( sal_uInt16 nIdx )
206 {
207 if( !CanRead() )
208 {
209 SetError( ERRCODE_BASIC_PROP_WRITEONLY );
210 return OUString();
211 }
212 SbxVarEntry& rRef = reinterpret_cast<SbxVarEntry&>(GetRef( nIdx ));
213
214 if (!rRef.maAlias)
215 return OUString();
216
217 return *rRef.maAlias;
218 }
219
PutAlias(const OUString & rAlias,sal_uInt16 nIdx)220 void SbxArray::PutAlias( const OUString& rAlias, sal_uInt16 nIdx )
221 {
222 if( !CanWrite() )
223 {
224 SetError( ERRCODE_BASIC_PROP_READONLY );
225 }
226 else
227 {
228 SbxVarEntry& rRef = reinterpret_cast<SbxVarEntry&>( GetRef( nIdx ) );
229 rRef.maAlias = rAlias;
230 }
231 }
232
Insert32(SbxVariable * pVar,sal_uInt32 nIdx)233 void SbxArray::Insert32( SbxVariable* pVar, sal_uInt32 nIdx )
234 {
235 DBG_ASSERT( mVarEntries.size() <= SBX_MAXINDEX32, "SBX: Array gets too big" );
236 if( mVarEntries.size() > SBX_MAXINDEX32 )
237 {
238 return;
239 }
240 SbxVarEntry p;
241 p.mpVar = pVar;
242 size_t nSize = mVarEntries.size();
243 if( nIdx > nSize )
244 {
245 nIdx = nSize;
246 }
247 if( eType != SbxVARIANT && pVar )
248 {
249 p.mpVar->Convert(eType);
250 }
251 if( nIdx == nSize )
252 {
253 mVarEntries.push_back( p );
254 }
255 else
256 {
257 mVarEntries.insert( mVarEntries.begin() + nIdx, p );
258 }
259 SetFlag( SbxFlagBits::Modified );
260 }
261
Insert(SbxVariable * pVar,sal_uInt16 nIdx)262 void SbxArray::Insert( SbxVariable* pVar, sal_uInt16 nIdx )
263 {
264 DBG_ASSERT( mVarEntries.size() <= 0x3FF0, "SBX: Array gets too big" );
265 if( mVarEntries.size() > 0x3FF0 )
266 {
267 return;
268 }
269 Insert32( pVar, nIdx );
270 }
271
Remove(sal_uInt32 nIdx)272 void SbxArray::Remove( sal_uInt32 nIdx )
273 {
274 if( nIdx < mVarEntries.size() )
275 {
276 mVarEntries.erase( mVarEntries.begin() + nIdx );
277 SetFlag( SbxFlagBits::Modified );
278 }
279 }
280
Remove(SbxVariable const * pVar)281 void SbxArray::Remove( SbxVariable const * pVar )
282 {
283 if( pVar )
284 {
285 for( size_t i = 0; i < mVarEntries.size(); i++ )
286 {
287 if (mVarEntries[i].mpVar.get() == pVar)
288 {
289 Remove( i ); break;
290 }
291 }
292 }
293 }
294
295 // Taking over of the data from the passed array, at which
296 // the variable of the same name will be overwritten.
297
Merge(SbxArray * p)298 void SbxArray::Merge( SbxArray* p )
299 {
300 if (!p)
301 return;
302
303 for (auto& rEntry1: p->mVarEntries)
304 {
305 if (!rEntry1.mpVar.is())
306 continue;
307
308 OUString aName = rEntry1.mpVar->GetName();
309 sal_uInt16 nHash = rEntry1.mpVar->GetHashCode();
310
311 // Is the element by the same name already inside?
312 // Then overwrite!
313 for (auto& rEntry2: mVarEntries)
314 {
315 if (!rEntry2.mpVar.is())
316 continue;
317
318 if (rEntry2.mpVar->GetHashCode() == nHash &&
319 rEntry2.mpVar->GetName().equalsIgnoreAsciiCase(aName))
320 {
321 // Take this element and clear the original.
322 rEntry2.mpVar = rEntry1.mpVar;
323 rEntry1.mpVar.clear();
324 break;
325 }
326 }
327
328 if (rEntry1.mpVar.is())
329 {
330 // We don't have element with the same name. Add a new entry.
331 SbxVarEntry aNewEntry;
332 aNewEntry.mpVar = rEntry1.mpVar;
333 if (rEntry1.maAlias)
334 aNewEntry.maAlias = *rEntry1.maAlias;
335 mVarEntries.push_back(aNewEntry);
336 }
337 }
338 }
339
340 // Search of an element by his name and type. If an element is an object,
341 // it will also be scanned...
342
Find(const OUString & rName,SbxClassType t)343 SbxVariable* SbxArray::Find( const OUString& rName, SbxClassType t )
344 {
345 SbxVariable* p = nullptr;
346 if( mVarEntries.empty() )
347 return nullptr;
348 bool bExtSearch = IsSet( SbxFlagBits::ExtSearch );
349 sal_uInt16 nHash = SbxVariable::MakeHashCode( rName );
350 for (auto& rEntry : mVarEntries)
351 {
352 if (!rEntry.mpVar.is() || !rEntry.mpVar->IsVisible())
353 continue;
354
355 // The very secure search works as well, if there is no hashcode!
356 sal_uInt16 nVarHash = rEntry.mpVar->GetHashCode();
357 if ( (!nVarHash || nVarHash == nHash)
358 && (t == SbxClassType::DontCare || rEntry.mpVar->GetClass() == t)
359 && (rEntry.mpVar->GetName().equalsIgnoreAsciiCase(rName)))
360 {
361 p = rEntry.mpVar.get();
362 p->ResetFlag(SbxFlagBits::ExtFound);
363 break;
364 }
365
366 // Did we have an array/object with extended search?
367 if (bExtSearch && rEntry.mpVar->IsSet(SbxFlagBits::ExtSearch))
368 {
369 switch (rEntry.mpVar->GetClass())
370 {
371 case SbxClassType::Object:
372 {
373 // Objects are not allowed to scan their parent.
374 SbxFlagBits nOld = rEntry.mpVar->GetFlags();
375 rEntry.mpVar->ResetFlag(SbxFlagBits::GlobalSearch);
376 p = static_cast<SbxObject&>(*rEntry.mpVar).Find(rName, t);
377 rEntry.mpVar->SetFlags(nOld);
378 }
379 break;
380 case SbxClassType::Array:
381 // Casting SbxVariable to SbxArray? Really?
382 p = reinterpret_cast<SbxArray&>(*rEntry.mpVar).Find(rName, t);
383 break;
384 default:
385 ;
386 }
387
388 if (p)
389 {
390 p->SetFlag(SbxFlagBits::ExtFound);
391 break;
392 }
393 }
394 }
395 return p;
396 }
397
LoadData(SvStream & rStrm,sal_uInt16)398 bool SbxArray::LoadData( SvStream& rStrm, sal_uInt16 /*nVer*/ )
399 {
400 sal_uInt16 nElem;
401 Clear();
402 bool bRes = true;
403 SbxFlagBits f = nFlags;
404 nFlags |= SbxFlagBits::Write;
405 rStrm.ReadUInt16( nElem );
406 nElem &= 0x7FFF;
407 for( sal_uInt32 n = 0; n < nElem; n++ )
408 {
409 sal_uInt16 nIdx;
410 rStrm.ReadUInt16( nIdx );
411 SbxVariable* pVar = static_cast<SbxVariable*>(Load( rStrm ));
412 if( pVar )
413 {
414 SbxVariableRef& rRef = GetRef( nIdx );
415 rRef = pVar;
416 }
417 else
418 {
419 bRes = false;
420 break;
421 }
422 }
423 nFlags = f;
424 return bRes;
425 }
426
StoreData(SvStream & rStrm) const427 bool SbxArray::StoreData( SvStream& rStrm ) const
428 {
429 sal_uInt32 nElem = 0;
430 // Which elements are even defined?
431 for( auto& rEntry: mVarEntries )
432 {
433 if (rEntry.mpVar.is() && !(rEntry.mpVar->GetFlags() & SbxFlagBits::DontStore))
434 nElem++;
435 }
436 rStrm.WriteUInt16( nElem );
437 for( size_t n = 0; n < mVarEntries.size(); n++ )
438 {
439 const SbxVarEntry& rEntry = mVarEntries[n];
440 if (rEntry.mpVar.is() && !(rEntry.mpVar->GetFlags() & SbxFlagBits::DontStore))
441 {
442 rStrm.WriteUInt16( n );
443 if (!rEntry.mpVar->Store(rStrm))
444 return false;
445 }
446 }
447 return true;
448 }
449
450 // #100883 Method to set method directly to parameter array
PutDirect(SbxVariable * pVar,sal_uInt32 nIdx)451 void SbxArray::PutDirect( SbxVariable* pVar, sal_uInt32 nIdx )
452 {
453 SbxVariableRef& rRef = GetRef32( nIdx );
454 rRef = pVar;
455 }
456
457
458 // SbxArray
459
SbxDimArray(SbxDataType t)460 SbxDimArray::SbxDimArray( SbxDataType t ) : SbxArray( t ), mbHasFixedSize( false )
461 {
462 }
463
operator =(const SbxDimArray & rArray)464 SbxDimArray& SbxDimArray::operator=( const SbxDimArray& rArray )
465 {
466 if( &rArray != this )
467 {
468 SbxArray::operator=( static_cast<const SbxArray&>(rArray) );
469 m_vDimensions = rArray.m_vDimensions;
470 mbHasFixedSize = rArray.mbHasFixedSize;
471 }
472 return *this;
473 }
474
~SbxDimArray()475 SbxDimArray::~SbxDimArray()
476 {
477 }
478
Clear()479 void SbxDimArray::Clear()
480 {
481 m_vDimensions.clear();
482 SbxArray::Clear();
483 }
484
485 // Add a dimension
486
AddDimImpl32(sal_Int32 lb,sal_Int32 ub,bool bAllowSize0)487 void SbxDimArray::AddDimImpl32( sal_Int32 lb, sal_Int32 ub, bool bAllowSize0 )
488 {
489 ErrCode eRes = ERRCODE_NONE;
490 if( ub < lb && !bAllowSize0 )
491 {
492 eRes = ERRCODE_BASIC_OUT_OF_RANGE;
493 ub = lb;
494 }
495 SbxDim d;
496 d.nLbound = lb;
497 d.nUbound = ub;
498 d.nSize = ub - lb + 1;
499 m_vDimensions.push_back(d);
500 if( eRes )
501 SetError( eRes );
502 }
503
504
AddDim(short lb,short ub)505 void SbxDimArray::AddDim( short lb, short ub )
506 {
507 AddDimImpl32( lb, ub, false );
508 }
509
unoAddDim(short lb,short ub)510 void SbxDimArray::unoAddDim( short lb, short ub )
511 {
512 AddDimImpl32( lb, ub, true );
513 }
514
AddDim32(sal_Int32 lb,sal_Int32 ub)515 void SbxDimArray::AddDim32( sal_Int32 lb, sal_Int32 ub )
516 {
517 AddDimImpl32( lb, ub, false );
518 }
519
unoAddDim32(sal_Int32 lb,sal_Int32 ub)520 void SbxDimArray::unoAddDim32( sal_Int32 lb, sal_Int32 ub )
521 {
522 AddDimImpl32( lb, ub, true );
523 }
524
525
526 // Readout dimension data
527
GetDim32(sal_Int32 n,sal_Int32 & rlb,sal_Int32 & rub) const528 bool SbxDimArray::GetDim32( sal_Int32 n, sal_Int32& rlb, sal_Int32& rub ) const
529 {
530 if( n < 1 || n > static_cast<sal_Int32>(m_vDimensions.size()) )
531 {
532 SetError( ERRCODE_BASIC_OUT_OF_RANGE );
533 rub = rlb = 0;
534 return false;
535 }
536 SbxDim d = m_vDimensions[n - 1];
537 rub = d.nUbound;
538 rlb = d.nLbound;
539 return true;
540 }
541
GetDim(short n,short & rlb,short & rub) const542 bool SbxDimArray::GetDim( short n, short& rlb, short& rub ) const
543 {
544 sal_Int32 rlb32, rub32;
545 bool bRet = GetDim32( n, rlb32, rub32 );
546 rub = static_cast<short>(rub32);
547 rlb = static_cast<short>(rlb32);
548 if( bRet )
549 {
550 if( rlb32 < -SBX_MAXINDEX || rub32 > SBX_MAXINDEX )
551 {
552 SetError( ERRCODE_BASIC_OUT_OF_RANGE );
553 return false;
554 }
555 }
556 return bRet;
557 }
558
559 // Element-Ptr with the help of an index list
560
Offset32(const sal_Int32 * pIdx)561 sal_uInt32 SbxDimArray::Offset32( const sal_Int32* pIdx )
562 {
563 sal_uInt32 nPos = 0;
564 for( const auto& rDimension : m_vDimensions )
565 {
566 sal_Int32 nIdx = *pIdx++;
567 if( nIdx < rDimension.nLbound || nIdx > rDimension.nUbound )
568 {
569 nPos = sal_uInt32(SBX_MAXINDEX32) + 1; break;
570 }
571 nPos = nPos * rDimension.nSize + nIdx - rDimension.nLbound;
572 }
573 if( m_vDimensions.empty() || nPos > SBX_MAXINDEX32 )
574 {
575 SetError( ERRCODE_BASIC_OUT_OF_RANGE );
576 nPos = 0;
577 }
578 return nPos;
579 }
580
Offset(const short * pIdx)581 sal_uInt16 SbxDimArray::Offset( const short* pIdx )
582 {
583 long nPos = 0;
584 for (auto const& vDimension : m_vDimensions)
585 {
586 short nIdx = *pIdx++;
587 if( nIdx < vDimension.nLbound || nIdx > vDimension.nUbound )
588 {
589 nPos = SBX_MAXINDEX + 1;
590 break;
591 }
592 nPos = nPos * vDimension.nSize + nIdx - vDimension.nLbound;
593 }
594 if( m_vDimensions.empty() || nPos > SBX_MAXINDEX )
595 {
596 SetError( ERRCODE_BASIC_OUT_OF_RANGE );
597 nPos = 0;
598 }
599 return static_cast<sal_uInt16>(nPos);
600 }
601
Get(const short * pIdx)602 SbxVariable* SbxDimArray::Get( const short* pIdx )
603 {
604 return SbxArray::Get( Offset( pIdx ) );
605 }
606
Put(SbxVariable * p,const short * pIdx)607 void SbxDimArray::Put( SbxVariable* p, const short* pIdx )
608 {
609 SbxArray::Put( p, Offset( pIdx ) );
610 }
611
Get32(const sal_Int32 * pIdx)612 SbxVariable* SbxDimArray::Get32( const sal_Int32* pIdx )
613 {
614 return SbxArray::Get32( Offset32( pIdx ) );
615 }
616
Put32(SbxVariable * p,const sal_Int32 * pIdx)617 void SbxDimArray::Put32( SbxVariable* p, const sal_Int32* pIdx )
618 {
619 SbxArray::Put32( p, Offset32( pIdx ) );
620 }
621
622 // Element-Number with the help of Parameter-Array
Offset32(SbxArray * pPar)623 sal_uInt32 SbxDimArray::Offset32( SbxArray* pPar )
624 {
625 #if HAVE_FEATURE_SCRIPTING
626 if (m_vDimensions.empty() || !pPar ||
627 ((m_vDimensions.size() != sal::static_int_cast<size_t>(pPar->Count() - 1))
628 && SbiRuntime::isVBAEnabled()))
629 {
630 SetError( ERRCODE_BASIC_OUT_OF_RANGE );
631 return 0;
632 }
633 #endif
634 sal_uInt32 nPos = 0;
635 sal_uInt16 nOff = 1; // Non element 0!
636 for (auto const& vDimension : m_vDimensions)
637 {
638 sal_Int32 nIdx = pPar->Get( nOff++ )->GetLong();
639 if( nIdx < vDimension.nLbound || nIdx > vDimension.nUbound )
640 {
641 nPos = sal_uInt32(SBX_MAXINDEX32)+1;
642 break;
643 }
644 nPos = nPos * vDimension.nSize + nIdx - vDimension.nLbound;
645 if (IsError())
646 break;
647 }
648 if( nPos > sal_uInt32(SBX_MAXINDEX32) )
649 {
650 SetError( ERRCODE_BASIC_OUT_OF_RANGE );
651 nPos = 0;
652 }
653 return nPos;
654 }
655
Get(SbxArray * pPar)656 SbxVariable* SbxDimArray::Get( SbxArray* pPar )
657 {
658 return SbxArray::Get32( Offset32( pPar ) );
659 }
660
LoadData(SvStream & rStrm,sal_uInt16 nVer)661 bool SbxDimArray::LoadData( SvStream& rStrm, sal_uInt16 nVer )
662 {
663 short nDimension;
664 rStrm.ReadInt16( nDimension );
665 for( short i = 0; i < nDimension && rStrm.GetError() == ERRCODE_NONE; i++ )
666 {
667 sal_Int16 lb(0), ub(0);
668 rStrm.ReadInt16( lb ).ReadInt16( ub );
669 AddDim( lb, ub );
670 }
671 return SbxArray::LoadData( rStrm, nVer );
672 }
673
StoreData(SvStream & rStrm) const674 bool SbxDimArray::StoreData( SvStream& rStrm ) const
675 {
676 rStrm.WriteInt16( m_vDimensions.size() );
677 for( short i = 0; i < static_cast<short>(m_vDimensions.size()); i++ )
678 {
679 short lb, ub;
680 GetDim( i, lb, ub );
681 rStrm.WriteInt16( lb ).WriteInt16( ub );
682 }
683 return SbxArray::StoreData( rStrm );
684 }
685
686 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
687