You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

670 lines
17 KiB

  1. unit uutlMCF;
  2. { Package: Utils
  3. Prefix: utl - UTiLs
  4. Beschreibung: diese Unit enthält Klassen zum Lesen und Schreiben eines MuoConfgiFiles (kurz MCF)
  5. Lesen/Schreiben in/von Stream über TutlMCFFile
  6. LineEndMode zur Kompatibilität mit MCF-alt und KCF:
  7. leNone - Kein Semikolon erlaubt (KCF)
  8. leAcceptNoWrite - Semikolon wird beim Lesen ignoriert, beim Schreiben weggelassen
  9. leAlways - Beim Lesen erforderlich, immer geschrieben (MCF-alt)
  10. Jeder SectionName und jeder ValueName ist Unique, es kann aber ein Value und eine
  11. Section mit dem gleichen Namen existieren
  12. Zugriff auf Subsections über .Section(), mehrere Stufen auf einmal mit . getrennt:
  13. mcf.Section('foo.bar.baz') === mcf.Section('foo').Section('bar').Section('baz')
  14. Zugriff erstellt automatisch eine Section, falls sie nicht existiert. Prüfung mit
  15. SectionExists (nur direkt, keine Pfade!).
  16. Zugriff auf Werte von der Section aus:
  17. Get/Set[Int,Float,String,Bool](Key, Default)
  18. ValueExists()
  19. UnsetValue()
  20. Strings sind Widestrings, Un/Escaping passiert beim Dateizugriff automatisch
  21. Enumeration: ValueCount/ValueNameAt, SectionCount/SectionNameAt }
  22. interface
  23. uses
  24. SysUtils, Classes, uutlStreamHelper;
  25. type
  26. EConfigException = class(Exception)
  27. end;
  28. TutlMCFSection = class;
  29. TutlMCFFile = class;
  30. TutlMCFLineEndMarkerMode = (leNone, leAcceptNoWrite, leAlways);
  31. { TutlMCFSection }
  32. TutlMCFSection = class
  33. private type
  34. TSectionEnumerator = class(TObject)
  35. private
  36. fList: TStringList;
  37. fPosition: Integer;
  38. function GetCurrent: TutlMCFSection;
  39. public
  40. property Current: TutlMCFSection read GetCurrent;
  41. function MoveNext: Boolean;
  42. constructor Create(const aList: TStringList);
  43. end;
  44. private
  45. FSections,
  46. FValues: TStringList;
  47. function GetSection(aPath: String): TutlMCFSection;
  48. function GetSectionCount: integer;
  49. function GetSectionName(Index: integer): string;
  50. function GetSectionByIndex(aIndex: Integer): TutlMCFSection;
  51. function GetValueCount: integer;
  52. function GetValueName(Index: integer): string;
  53. protected
  54. procedure ClearSections;
  55. procedure ClearValues;
  56. procedure SaveData(Stream: TStream; Indent: string; LineEnds: TutlMCFLineEndMarkerMode);
  57. procedure LoadData(Data: TStream; LineEnds: TutlMCFLineEndMarkerMode; Depth: Integer);
  58. procedure AddValueChecked(Name: String; Val: TObject);
  59. procedure SplitPath(const Path: String; out First, Rest: String);
  60. public
  61. constructor Create;
  62. destructor Destroy; override;
  63. function GetEnumerator: TSectionEnumerator;
  64. procedure Clear;
  65. procedure Assign(Source: TutlMCFSection);
  66. property ValueCount: integer read GetValueCount;
  67. property ValueNameAt[Index: integer]: string read GetValueName;
  68. property SectionCount: integer read GetSectionCount;
  69. property SectionNameAt[Index: integer]: string read GetSectionName;
  70. property Sections[aPath: String]: TutlMCFSection read GetSection; default;
  71. property SectionByIndex[aIndex: Integer]: TutlMCFSection read GetSectionByIndex;
  72. function SectionExists(Path: string): boolean;
  73. function Section(Path: string): TutlMCFSection;
  74. procedure DeleteSection(Name: string);
  75. function ValueExists(Name: string): boolean;
  76. function GetInt(Name: string; Default: Int64 = 0): Int64; overload;
  77. function GetFloat(Name: string; Default: Double = 0): Double; overload;
  78. function GetString(Name: string; Default: AnsiString = ''): AnsiString; overload;
  79. function GetStringW(Name: string; Default: UnicodeString = ''): UnicodeString; overload;
  80. function GetBool(Name: string; Default: Boolean = false): Boolean; overload;
  81. procedure SetInt(Name: string; Value: Int64); overload;
  82. procedure SetFloat(Name: string; Value: Double); overload;
  83. procedure SetString(Name: string; Value: WideString); overload;
  84. procedure SetString(Name: string; Value: AnsiString); overload;
  85. procedure SetBool(Name: string; Value: Boolean); overload;
  86. procedure UnsetValue(Name: string);
  87. end;
  88. { TutlMCFFile }
  89. TutlMCFFile = class(TutlMCFSection)
  90. private
  91. fLineEndMode: TutlMCFLineEndMarkerMode;
  92. public
  93. constructor Create(Data: TStream; LineEndMode: TutlMCFLineEndMarkerMode = leAcceptNoWrite);
  94. procedure LoadFromStream(Stream: TStream);
  95. procedure SaveToStream(Stream: TStream);
  96. end;
  97. implementation
  98. uses Variants, StrUtils;
  99. const
  100. sComment = '#';
  101. sSectionEnd = 'end';
  102. sSectionMarker = ':';
  103. sSectionPathDelim = '.';
  104. sLineEndMarker = ';';
  105. sValueDelim = '=';
  106. sValueQuote = '''';
  107. sValueDecimal = '.';
  108. sIndentOnSave = ' ';
  109. sNameValidChars = [' ' .. #$7F] - [sValueDelim];
  110. sWhitespaceChars = [#0 .. ' '];
  111. type
  112. StoredValue = Variant;
  113. { TutlMCFValue }
  114. TutlMCFValue = class
  115. private
  116. Format: TFormatSettings;
  117. FValue: StoredValue;
  118. procedure SetValue(const Value: StoredValue);
  119. protected
  120. function CheckSpecialChars(Data: WideString): boolean;
  121. procedure LoadData(Data: string);
  122. function SaveData: string;
  123. class function Escape(Value: WideString): AnsiString;
  124. class function Unescape(Value: AnsiString): WideString;
  125. public
  126. constructor Create(Val: StoredValue);
  127. property Value: StoredValue read FValue write SetValue;
  128. end;
  129. { TkcfValue }
  130. constructor TutlMCFValue.Create(Val: StoredValue);
  131. begin
  132. inherited Create;
  133. SetValue(Val);
  134. Format.DecimalSeparator:= sValueDecimal;
  135. end;
  136. procedure TutlMCFValue.SetValue(const Value: StoredValue);
  137. begin
  138. FValue:= Value;
  139. end;
  140. function TutlMCFValue.CheckSpecialChars(Data: WideString): boolean;
  141. var
  142. i: Integer;
  143. begin
  144. result := false;
  145. for i:= 1 to Length(Data) do
  146. if Data[i] in [sSectionMarker, sValueQuote, sValueDelim, sLineEndMarker, ' '] then
  147. exit;
  148. result := true;
  149. end;
  150. procedure TutlMCFValue.LoadData(Data: string);
  151. var
  152. b: boolean;
  153. i: int64;
  154. d: double;
  155. p: PChar;
  156. begin
  157. if TryStrToInt64(Data, i) then
  158. Value:= i
  159. else if TryStrToFloat(Data, d, Format) then
  160. Value:= d
  161. else if TryStrToBool(Data, b) then
  162. Value:= b
  163. else begin
  164. p:= PChar(Data);
  165. if p^ = sValueQuote then
  166. Data := AnsiExtractQuotedStr(p, sValueQuote);
  167. Value:= Unescape(Trim(Data));
  168. end;
  169. end;
  170. function TutlMCFValue.SaveData: string;
  171. begin
  172. if VarIsType(FValue, varBoolean) then
  173. Result:= BoolToStr(FValue, false)
  174. else if VarIsType(FValue, varInt64) then
  175. Result:= IntToStr(FValue)
  176. else if VarIsType(FValue, varDouble) then
  177. Result:= FloatToStr(Double(FValue), Format)
  178. else begin
  179. Result:= Escape(FValue);
  180. if not CheckSpecialChars(WideString(Result)) then
  181. Result:= AnsiQuotedStr(Result, sValueQuote);
  182. end;
  183. end;
  184. class function TutlMCFValue.Escape(Value: WideString): AnsiString;
  185. var
  186. i: integer;
  187. wc: WideChar;
  188. begin
  189. Result:= '';
  190. for i:= 1 to length(Value) do begin
  191. wc:= Value[i];
  192. case Ord(wc) of
  193. Ord('\'),
  194. $007F..$FFFF: Result:= Result + '\'+IntToHex(ord(wc),4);
  195. else
  196. Result:= Result + AnsiChar(wc);
  197. end;
  198. end;
  199. end;
  200. class function TutlMCFValue.Unescape(Value: AnsiString): WideString;
  201. var
  202. i: integer;
  203. c: Char;
  204. begin
  205. Result:= '';
  206. i:= 1;
  207. while i <= length(value) do begin
  208. c:= Value[i];
  209. if c='\' then begin
  210. Result:= Result + WideChar(StrToInt('$'+Copy(Value,i+1,4)));
  211. inc(i, 4);
  212. end else
  213. Result:= Result + WideChar(c);
  214. inc(i);
  215. end;
  216. end;
  217. { TutlMCFSection.TSectionEnumerator }
  218. function TutlMCFSection.TSectionEnumerator.GetCurrent: TutlMCFSection;
  219. begin
  220. result := TutlMCFSection(fList.Objects[fPosition]);
  221. end;
  222. function TutlMCFSection.TSectionEnumerator.MoveNext: Boolean;
  223. begin
  224. inc(fPosition);
  225. result := (fPosition < fList.Count);
  226. end;
  227. constructor TutlMCFSection.TSectionEnumerator.Create(const aList: TStringList);
  228. begin
  229. inherited Create;
  230. fList := aList;
  231. fPosition := -1;
  232. end;
  233. { TkcfCompound }
  234. constructor TutlMCFSection.Create;
  235. begin
  236. inherited;
  237. FSections:= TStringList.Create;
  238. FSections.CaseSensitive:= false;
  239. FSections.Sorted:= true;
  240. FSections.Duplicates:= dupError;
  241. FValues:= TStringList.Create;
  242. FValues.CaseSensitive:= false;
  243. FValues.Sorted:= true;
  244. FValues.Duplicates:= dupError;
  245. end;
  246. destructor TutlMCFSection.Destroy;
  247. begin
  248. ClearSections;
  249. ClearValues;
  250. FreeAndNil(FSections);
  251. FreeAndNil(FValues);
  252. inherited;
  253. end;
  254. function TutlMCFSection.GetEnumerator: TSectionEnumerator;
  255. begin
  256. result := TSectionEnumerator.Create(FSections);
  257. end;
  258. procedure TutlMCFSection.Clear;
  259. begin
  260. ClearSections;
  261. ClearValues;
  262. end;
  263. procedure TutlMCFSection.Assign(Source: TutlMCFSection);
  264. var
  265. ms: TMemoryStream;
  266. begin
  267. Clear;
  268. ms:= TMemoryStream.Create;
  269. try
  270. Source.SaveData(ms, '', leNone);
  271. ms.Position:= 0;
  272. LoadData(ms, leNone, 0);
  273. finally
  274. FreeAndNil(ms);
  275. end;
  276. end;
  277. function TutlMCFSection.GetSectionCount: integer;
  278. begin
  279. Result:= FSections.Count;
  280. end;
  281. function TutlMCFSection.GetSection(aPath: String): TutlMCFSection;
  282. begin
  283. result := Section(aPath);
  284. end;
  285. function TutlMCFSection.GetSectionByIndex(aIndex: Integer): TutlMCFSection;
  286. begin
  287. result := (FSections.Objects[aIndex] as TutlMCFSection);
  288. end;
  289. function TutlMCFSection.GetSectionName(Index: integer): string;
  290. begin
  291. Result:= FSections[Index];
  292. end;
  293. function TutlMCFSection.GetValueCount: integer;
  294. begin
  295. Result:= FValues.Count;
  296. end;
  297. function TutlMCFSection.GetValueName(Index: integer): string;
  298. begin
  299. Result:= FValues[Index];
  300. end;
  301. procedure TutlMCFSection.ClearSections;
  302. var
  303. i: integer;
  304. begin
  305. for i:= FSections.Count - 1 downto 0 do
  306. FSections.Objects[i].Free;
  307. FSections.Clear;
  308. end;
  309. procedure TutlMCFSection.ClearValues;
  310. var
  311. i: integer;
  312. begin
  313. for i:= FValues.Count - 1 downto 0 do
  314. FValues.Objects[i].Free;
  315. FValues.Clear;
  316. end;
  317. procedure TutlMCFSection.SplitPath(const Path: String; out First, Rest: String);
  318. begin
  319. First:= Copy(Path, 1, Pos(sSectionPathDelim, Path)-1);
  320. if First='' then begin
  321. First:= Path;
  322. Rest:= '';
  323. end else begin
  324. Rest:= Copy(Path, Length(First)+2, MaxInt);
  325. end;
  326. end;
  327. function TutlMCFSection.SectionExists(Path: string): boolean;
  328. var
  329. f,r: String;
  330. i: integer;
  331. begin
  332. SplitPath(Path, f, r);
  333. i:= FSections.IndexOf(f);
  334. Result:= (i >= 0) and ((r='') or (TutlMCFSection(FSections.Objects[i]).SectionExists(r)));
  335. end;
  336. function TutlMCFSection.Section(Path: string): TutlMCFSection;
  337. var
  338. f,r: String;
  339. i: integer;
  340. begin
  341. SplitPath(Path, f, r);
  342. i:= FSections.IndexOf(f);
  343. if r <> '' then begin
  344. if (i >= 0) then
  345. Result:= TutlMCFSection(FSections.Objects[i]).Section(r)
  346. else begin
  347. result := TutlMCFSection.Create;
  348. fSections.AddObject(f, result);
  349. result := result.Section(r);
  350. end;
  351. end else begin
  352. if i >= 0 then
  353. Result:= TutlMCFSection(FSections.Objects[i])
  354. else begin
  355. Result:= TutlMCFSection.Create;
  356. FSections.AddObject(f, Result);
  357. end;
  358. end;
  359. end;
  360. procedure TutlMCFSection.DeleteSection(Name: string);
  361. var
  362. i: integer;
  363. begin
  364. i:= FSections.IndexOf(Name);
  365. if i >= 0 then begin
  366. FSections.Objects[i].Free;
  367. FSections.Delete(i);
  368. end;
  369. end;
  370. function TutlMCFSection.ValueExists(Name: string): boolean;
  371. begin
  372. Result:= FValues.IndexOf(Name) >= 0;
  373. end;
  374. function TutlMCFSection.GetInt(Name: string; Default: Int64): Int64;
  375. var
  376. i: integer;
  377. begin
  378. i:= FValues.IndexOf(Name);
  379. if i < 0 then
  380. Result:= Default
  381. else
  382. Result:= TutlMCFValue(FValues.Objects[i]).Value;
  383. end;
  384. function TutlMCFSection.GetFloat(Name: string; Default: Double): Double;
  385. var
  386. i: integer;
  387. begin
  388. i:= FValues.IndexOf(Name);
  389. if i < 0 then
  390. Result:= Default
  391. else
  392. Result:= TutlMCFValue(FValues.Objects[i]).Value;
  393. end;
  394. function TutlMCFSection.GetStringW(Name: string; Default: UnicodeString): UnicodeString;
  395. var
  396. i: integer;
  397. begin
  398. i:= FValues.IndexOf(Name);
  399. if i < 0 then
  400. Result:= Default
  401. else
  402. Result:= TutlMCFValue(FValues.Objects[i]).Value;
  403. end;
  404. function TutlMCFSection.GetString(Name: string; Default: AnsiString): AnsiString;
  405. begin
  406. Result := AnsiString(GetStringW(Name, UnicodeString(Default)));
  407. end;
  408. function TutlMCFSection.GetBool(Name: string; Default: Boolean): Boolean;
  409. var
  410. i: integer;
  411. begin
  412. i:= FValues.IndexOf(Name);
  413. if i < 0 then
  414. Result:= Default
  415. else
  416. Result:= TutlMCFValue(FValues.Objects[i]).Value;
  417. end;
  418. procedure TutlMCFSection.AddValueChecked(Name: String; Val: TObject);
  419. var
  420. i: integer;
  421. begin
  422. if (Length(Name) < 1) or
  423. (Name[1] in sWhitespaceChars) or
  424. (Name[Length(Name)] in sWhitespaceChars) then
  425. raise EConfigException.CreateFmt('Invalid Value Name: "%s"',[Name]);
  426. for i:= 1 to Length(Name) do
  427. if not (Name[i] in sNameValidChars) then
  428. raise EConfigException.CreateFmt('Invalid Value Name: "%s"',[Name]);
  429. FValues.AddObject(Name, Val);
  430. end;
  431. procedure TutlMCFSection.SetInt(Name: string; Value: Int64);
  432. var
  433. i: integer;
  434. begin
  435. i:= FValues.IndexOf(Name);
  436. if i < 0 then
  437. AddValueChecked(Name, TutlMCFValue.Create(Value))
  438. else
  439. TutlMCFValue(FValues.Objects[i]).Value:= Value;
  440. end;
  441. procedure TutlMCFSection.SetFloat(Name: string; Value: Double);
  442. var
  443. i: integer;
  444. begin
  445. i:= FValues.IndexOf(Name);
  446. if i < 0 then
  447. AddValueChecked(Name, TutlMCFValue.Create(Value))
  448. else
  449. TutlMCFValue(FValues.Objects[i]).Value:= Value;
  450. end;
  451. procedure TutlMCFSection.SetString(Name: string; Value: WideString);
  452. var
  453. i: integer;
  454. begin
  455. i:= FValues.IndexOf(Name);
  456. if i < 0 then
  457. AddValueChecked(Name, TutlMCFValue.Create(Value))
  458. else
  459. TutlMCFValue(FValues.Objects[i]).Value:= Value;
  460. end;
  461. procedure TutlMCFSection.SetString(Name: string; Value: AnsiString);
  462. begin
  463. SetString(Name, WideString(Value));
  464. end;
  465. procedure TutlMCFSection.SetBool(Name: string; Value: Boolean);
  466. var
  467. i: integer;
  468. begin
  469. i:= FValues.IndexOf(Name);
  470. if i < 0 then
  471. AddValueChecked(Name, TutlMCFValue.Create(Value))
  472. else
  473. TutlMCFValue(FValues.Objects[i]).Value:= Value;
  474. end;
  475. procedure TutlMCFSection.UnsetValue(Name: string);
  476. var
  477. i: integer;
  478. begin
  479. i:= FValues.IndexOf(Name);
  480. if i >= 0 then begin
  481. FValues.Objects[i].Free;
  482. FValues.Delete(i);
  483. end;
  484. end;
  485. procedure TutlMCFSection.LoadData(Data: TStream; LineEnds: TutlMCFLineEndMarkerMode; Depth: Integer);
  486. var
  487. reader: TutlStreamReader;
  488. l, sn, vn, vs: string;
  489. se: TutlMCFSection;
  490. va: TutlMCFValue;
  491. begin
  492. reader:= TutlStreamReader.Create(Data);
  493. try
  494. repeat
  495. l:= reader.ReadLine;
  496. l:= trim(l);
  497. if (l = '') or AnsiStartsStr(sComment, l) then
  498. continue;
  499. if ((LineEnds in [leNone, leAcceptNoWrite]) and (l = sSectionEnd)) or
  500. ((LineEnds in [leAcceptNoWrite, leAlways]) and (l = sSectionEnd+sLineEndMarker)) then begin
  501. if Depth > 0 then
  502. exit
  503. else
  504. raise EConfigException.Create('Encountered Section End where none was expected.');
  505. end;
  506. if AnsiEndsStr(sSectionMarker, l) then begin
  507. sn:= trim(Copy(l, 1, length(l) - length(sSectionMarker)));
  508. if SectionExists(sn) then
  509. raise EConfigException.Create('Redeclared Section: '+sn);
  510. if Pos(sSectionPathDelim,sn) > 0 then
  511. raise EConfigException.Create('Invalid Section Name: '+sn);
  512. se:= TutlMCFSection.Create;
  513. try
  514. se.LoadData(Data, LineEnds, Depth + 1);
  515. FSections.AddObject(sn, se);
  516. except
  517. FreeAndNil(se);
  518. end;
  519. end else if (Pos(sValueDelim, l) > 0) then begin
  520. if (LineEnds in [leAcceptNoWrite, leAlways]) and AnsiEndsStr(sLineEndMarker, l) then
  521. Delete(l, length(l), 1);
  522. vn:= trim(Copy(l, 1, Pos(sValueDelim, l) - 1));
  523. vs:= trim(Copy(l, Pos(sValueDelim, l) + 1, Maxint));
  524. if ValueExists(vn) then
  525. raise EConfigException.Create('Redeclared Value: '+vn);
  526. va:= TutlMCFValue.Create('');
  527. try
  528. va.LoadData(vs);
  529. AddValueChecked(vn, va);
  530. except
  531. FreeAndNil(va);
  532. end;
  533. end else
  534. raise EConfigException.Create('Cannot Parse Line: '+l);
  535. until reader.IsEOF;
  536. if Depth > 0 then
  537. raise EConfigException.Create('Expected Section End, but reached stream end.');
  538. Depth:= Depth - 1;
  539. finally
  540. FreeAndNil(reader);
  541. end;
  542. end;
  543. procedure TutlMCFSection.SaveData(Stream: TStream; Indent: string;
  544. LineEnds: TutlMCFLineEndMarkerMode);
  545. var
  546. writer: TutlStreamWriter;
  547. i: integer;
  548. ele, s: AnsiString;
  549. begin
  550. if LineEnds in [leAlways] then
  551. ele:= sLineEndMarker
  552. else
  553. ele:= '';
  554. writer:= TutlStreamWriter.Create(Stream);
  555. try
  556. for i:= 0 to FValues.Count - 1 do begin
  557. s:= Indent + FValues[i] + ' ' + sValueDelim + ' ' + TutlMCFValue(FValues.Objects[i]).SaveData + ele;
  558. writer.WriteLine(s);
  559. end;
  560. for i:= 0 to FSections.Count - 1 do begin
  561. s:= Indent + FSections[i] + sSectionMarker;
  562. writer.WriteLine(s);
  563. TutlMCFSection(FSections.Objects[i]).SaveData(Stream, Indent + sIndentOnSave, LineEnds);
  564. s:= Indent + sSectionEnd + ele;
  565. writer.WriteLine(s);
  566. end;
  567. finally
  568. FreeAndNil(writer);
  569. end;
  570. end;
  571. { TutlMCFFile }
  572. constructor TutlMCFFile.Create(Data: TStream; LineEndMode: TutlMCFLineEndMarkerMode);
  573. begin
  574. inherited Create;
  575. fLineEndMode:= LineEndMode;
  576. if Assigned(Data) then
  577. LoadFromStream(Data);
  578. end;
  579. procedure TutlMCFFile.LoadFromStream(Stream: TStream);
  580. begin
  581. ClearSections;
  582. ClearValues;
  583. LoadData(Stream, fLineEndMode, 0);
  584. end;
  585. procedure TutlMCFFile.SaveToStream(Stream: TStream);
  586. begin
  587. SaveData(Stream, '', fLineEndMode);
  588. end;
  589. end.