КАКОДА користим Property List датотеке

Упутство за програмере. [верзија 0.1, 5. јануар 2024.]

Страхиња Радић

Садржај

  1. Увод
  2. Формат Property List датотека
  3. Читање датотеке
    1. Испитивање типа plist-а
    2. Приступ вредностима у речнику и низовима
    3. Приступ подацима и нискама знакова
  4. Писање у plist датотеку

1. Увод

Property List или plist датотеке су датотеке са подешавањима у текстуалном или бинарном формату. Новија текстуална верзија plist формата је у ствари XML, али ни она, ни бинарни формат нису предмет овог текста. Уместо тога, овде ће реч бити о првој, текстуалној, верзији тог формата, која је коришћена у оперативном систему NeXTSTEP, па је зато усвојена и у менаџеру прозора Window Maker. Једино „упутство“ које сам за сада могао да пронађем је оно из туторијала на сајту WINGs библиотеке, a оно се састоји од простог набрајања једног дела свих функција, међу којима су и оне које се баве plist‑ом. Ту лежи основни разлог за писање овог упутства.

2. Формат Property List датотека

Пример самог plist формата су датотеке са подешавањима, обично у директоријуму $HOME/GNUstep/Defaults, рецимо $HOME/GNUstep/Defaults/WMState:

{
  Dock = {
    AutoRaiseLower = No;
    Applications = (
      {
        Forced = No;
        Name = Logo.WMDock;
        BuggyApplication = No;
        AutoLaunch = No;
        Position = "0,0";
        Lock = No;
        Command = "-";
      },
      {
        Forced = No;
        Name = wmclock.WMClock;
        DropCommand = "wmclock %d";
        BuggyApplication = No;
        AutoLaunch = Yes;
        Position = "0,2";
        Lock = Yes;
        PasteCommand = "wmclock %s";
        Command = wmclock;
      },
//...
  };
//...
}

или $HOME/GNUstep/Defaults/WMGLOBAL:

{
  BoldSystemFont = "Liberation Sans:slant=0:weight=200:width=100:pixelsize=12";
  SystemFont = "Liberation Sans:slant=0:weight=80:width=100:pixelsize=12";
}

или, рецимо, /usr/local/share/WindowMaker/plmenu:

("Applications",
 ("Info",
  ("Info Panel", INFO_PANEL),
  ("Legal", LEGAL_PANEL),
  ("System Console", EXEC, "xconsole"),
  ("System Load", SHEXEC, "xosview || xload"),
  ("Process List", EXEC, "xterm -e top"),
  ("Manual Browser", EXEC, "xman")
 ),
//...
)

итд. Одавде можемо већ да закључимо неколико ствари:

Рецимо, plist

{ Applications = ({ }, { }); }

је речник са једним кључем, Applications, чија вредност је низ од два (празна) речника.

3. Читање датотеке

plist датотеке се, наравно, могу налазити било где у систему датотека, али WINGs садржи и згодну функцију wusergnusteppath, која враћа путању корисничког директоријума $HOME/GNUstep. На пример:

#include <WINGs/WINGs.h>
#include <stdio.h>
#include <stdlib.h>

int
main()
{
        char prefsfile[PATH_MAX];
        WMPropList* settings;

        snprintf(prefsfile, PATH_MAX, "%s/%s", wusergnusteppath(),
                "Defaults/wpltest");
        if (!(settings = WMReadPropListFromFile(prefsfile)))
        {
                fprintf(stderr, "Cannot read plist file %sn",
                        prefsfile);
                exit(1);
        }

	// ... Манипулација добијеним plist-ом у променљивој settings ...

        WMReleasePropList(settings);

        return 0;
}

пошто датотека $HOME/GNUstep/Defaults/wpltest (највероватније) не постоји, програм ће исписати поруку о грешци. Међутим, и ако извршимо:

$ touch $HOME/GNUstep/Defaults/wpltest
$ ./wpltest
Cannot read plist file /home/strajder/GNUstep/Defaults/wpltest

Празна датотека није валидан plist, па WMReadPropListFromFile и даље враћа NULL. Убацимо зато у њу празан речник:

$ printf "{}\n" > $HOME/GNUstep/Defaults/wpltest
$ ./wpltest
$

3.1. Испитивање типа plist-а

Видели смо да је сваки plist или низ, или речник, ниска знакова или податак. На срећу, WINGs садржи низ функција за испитивање типа plist‑а. Ако програму додамо функцију PLToString која враћа тип plist‑а на основу испитивања овим функцијама:

const char*
PLToString(WMPropList* pl)
{
        if (WMIsPLString(pl))
                return "String";
        else if (WMIsPLData(pl))
                return "Data";
        else if (WMIsPLArray(pl))
                return "Array";
        else if (WMIsPLDictionary(pl))
                return "Dictionary";
        else
                return "Error";
}

и уместо коментара у нашем тест‑програму убацимо:

	printf("type(settings) = %s\n", PLToString(settings));

добићемо:

$ cat $HOME/GNUstep/Defaults/wpltest
{}
$ ./wpltest
type(settings) = Dictionary

3.2. Приступ вредностима у речнику и низовима

Изменимо сада датотеку wpltest:

$ printf "{
Sequence = < 1b A0B1c2 >;
Amount = 42;
}\n" > $HOME/GNUstep/Defaults/wpltest

и додајмо после позива функције printf у нашем програму:

	if (WMIsPLDictionary(settings))
	{
		WMPropList* keys;
		keys = WMGetPLDictionaryKeys(settings);
		printf("type(keys) = %sn", PLToString(keys));
		for (int i = 0; i < WMGetPropListItemCount(keys); i++)
		{
			WMPropList* key;
			WMPropList* val;
			char* skey;
			char* sval;

			key = WMGetFromPLArray(keys, i);
			printf("type(key = keys[%d]) = %s\n",
				i, PLToString(key));
			skey = WMGetFromPLString(key);
			printf("key = %s\n", skey);

			val = WMGetFromPLDictionary(settings, key);
			printf("type(val = settings{%s}) = %s\n",
				skey,
				PLToString(val));

			// ... наставак следи ...
		}
		WMReleasePropList(keys);
	}

овде прво испитујемо да ли је settings речник, и уколико јесте, прослеђујемо ту променљиву функцији WMGetPLDictionaryKeys, која враћа „plist‑низ“ свих кључева тог речника. Даље се врши итерација тог низа, где је горња граница итератора одређена позивом функције WMGetPropListItemCount над низом keys.

Пошто функција WMGetPLDictionaryKeys алоцира меморију, добра је пракса да на крају блока у ком смо је позвали позовемо и функцију WMReleasePropList, како бисмо ослободили ту алоцирану меморију.

За сваку вредност итератора, i‑ти члан низа keys добијамо као резултат позива функције WMGetFromPLArray(keys, i). Ово можемо да схватимо као еквивалент

	key = keys[i];

До конкретног назива кључа key се не може доћи директно, већ се мора позвати функција WMGetFromPLString. Она се може схватити као cast‑овање:

	skey = (String)key;

Слично са вредношћу која је индексирана кључем key. Њу добијамо позивом функције WMGetFromPLDictionary(settings, key), што се може схватити као када бисмо у Perl‑у написали

$settings{$key}

где је %settings хеш.

Ако сада покренемо програм, добићемо:

$ cat $HOME/GNUstep/Defaults/wpltest
{
Sequence = < 1b A0B1c2 >;
Amount = 42;
}
$ ./wpltest
type(settings) = Dictionary
type(keys) = Array
type(key = keys[0]) = String
key = Amount
type(val = settings{Amount}) = String
type(key = keys[1]) = String
key = Sequence
type(val = settings{Sequence}) = Data

Приметимо да вредности у оквиру keys не морају бити у истом поретку као у датотеци. Такође је, сасвим у складу са оним што је речено, тип вредности 42, индексиране са Amount, ниска знакова, а не број. За даље коришћење те вредности бисмо по свој прилици морали да позовемо функцију типа strtol. Тај начин претварања ниске знакова у број ћемо видети у практичном примеру у следећој тачки.

3.3. Приступ подацима и нискама знакова

Технички, управо смо већ обрадили приступ нискама знакова позивом функције WMGetFromPLString. Погледајмо сада како да приступимо и plist типу „податак“, и да конвертујемо ниске знакова у бројеве.

Додајмо на почетак нашег програма

#include <errno.h>

а уместо коментара:

			if (WMIsPLData(val))
			{
				const unsigned char* data;
				const unsigned char* pdata;
				data = WMGetPLDataBytes(val);
				pdata = data;
				printf("data(val) = ");
				while (*pdata)
				{
					printf("0x%02X ", *pdata);
					pdata++;
				}
				printf("\n");
			}
			else if (WMIsPLString(val))
			{
				long lval = 0;
				sval = WMGetFromPLString(val);
				errno = 0;
				lval  = strtol(sval, NULL, 10);
				if (!lval
					&& (errno == EINVAL || errno == ERANGE))
					printf("string(val) = %sn", sval);
				else
					printf("number(val) = %ld\n", lval);
			}

у суштини, позивом функције WMGetPLDataBytes добијамо показивач који показује на секвенцу const unsigned char‑ова, завршену NUL бајтом. Простом итерацијом тог низа приступамо појединачним бајтовима.

Вредност индексирану кључем Amount покушавамо да конвертујемо функцијом strtol, и у случају неуспеха закључујемо да се ради о знаковној нисци, а не броју.

Сада наш програм исписује додатне информације:

$ ./wpltest
type(settings) = Dictionary
type(keys) = Array
type(key = keys[0]) = String
key = Amount
type(val = settings{Amount}) = String
number(val) = 42
type(key = keys[1]) = String
key = Sequence
type(val = settings{Sequence}) = Data
data(val) = 0x1B 0xA0 0xB1 0xC2

4. Писање у plist датотеку

Писање у датотеку може се извести директно, позивом функције WMWritePropListToFile, којој се проследи plist променљива (у случају да је већ имамо спремну) и назив датотеке. Међутим, за практичне примене вероватно ће интересантнији бити случај када се та променљива конструише „у лету“, од „изворног кода“. Ово се постиже функцијом WMCreatePropListFromDescription:

#define MAX_PL_BUF 512

int
save_prefs(const long amount, const unsigned char* sequence)
{
	char desc[MAX_PL_BUF];
	char sfile[PATH_MAX];
	WMPropList* settings = NULL;
	char* ssequence = wmalloc(strlen((const char*)sequence) * 2 + 1);
	char* pssequence = ssequence;
	const unsigned char* psequence = sequence;

	while (*psequence)
	{
		snprintf(pssequence, 3, "%02X", *psequence);
		pssequence += 2;
		psequence++;
	}
	
	snprintf(desc, MAX_PL_BUF,
		"{ Sequence = < %s >; "
		"  Amount = %ld; }",
		ssequence,
		amount);
	snprintf(sfile, PATH_MAX, "%s/%s", wusergnusteppath(),
		"Defaults/wpltest-new");

	if (!(settings = WMCreatePropListFromDescription(desc)))
	{
		fprintf(stderr, "WMCreatePropListFromDescription failed\n");
		wfree(ssequence);
		return 1;
	}
	if (!WMWritePropListToFile(settings, sfile))
	{
		fprintf(stderr, "WMWritePropListToFile failed\n");
		wfree(ssequence);
		WMReleasePropList(settings);
		return 1;
	}
	wfree(ssequence);
	WMReleasePropList(settings);
	return 0;
}

(наравно, сада нам на почетку кода треба и string.h).

Функције wmalloc/wfree су помоћне WINGs функције, које по потреби користе Boehm GC, a wmalloc и тестира да ли је меморија успешно алоцирана, па због тога то не морамо да радимо у функцији save_prefs.

Уз пар малих измена (додавање декларације long amount и постављање вредности те променљиве из lval када се наиђе на !strcmp(skey, "Amount"), програму се може додати и позив

	save_prefs(amount, data);

па сада имамо:

$ cat $HOME/GNUstep/Defaults/wpltest
{
Sequence = < 1b A0B1c2 >;
Amount = 42;
}
$ ./wpltest >/dev/null
$ cat $HOME/GNUstep/Defaults/wpltest-new
{
  Amount = 42;
  Sequence = <1ba0b1c2>;
}

коначна верзија кода је дата испод.

Датотека wpltest.c (дугачак листинг)
#include <WINGs/WINGs.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

const char*
PLToString(WMPropList* pl)
{
	if (WMIsPLString(pl))
		return "String";
	else if (WMIsPLData(pl))
		return "Data";
	else if (WMIsPLArray(pl))
		return "Array";
	else if (WMIsPLDictionary(pl))
		return "Dictionary";
	else
		return "Error";
}

#define MAX_PL_BUF 512

int
save_prefs(const long amount, const unsigned char* sequence)
{
	char desc[MAX_PL_BUF];
	char sfile[PATH_MAX];
	WMPropList* settings = NULL;
	char* ssequence = wmalloc(strlen((const char*)sequence) * 2 + 1);
	char* pssequence = ssequence;
	const unsigned char* psequence = sequence;

	while (*psequence)
	{
		snprintf(pssequence, 3, "%02X", *psequence);
		pssequence += 2;
		psequence++;
	}
	
	snprintf(desc, MAX_PL_BUF,
		"{ Sequence = < %s >; "
		"  Amount = %ld; }",
		ssequence,
		amount);
	snprintf(sfile, PATH_MAX, "%s/%s", wusergnusteppath(),
		"Defaults/wpltest-new");

	if (!(settings = WMCreatePropListFromDescription(desc)))
	{
		fprintf(stderr, "WMCreatePropListFromDescription failed\n");
		wfree(ssequence);
		return 1;
	}
	if (!WMWritePropListToFile(settings, sfile))
	{
		fprintf(stderr, "WMWritePropListToFile failed\n");
		wfree(ssequence);
		WMReleasePropList(settings);
		return 1;
	}
	wfree(ssequence);
	WMReleasePropList(settings);
	return 0;
}

int
main()
{
	char prefsfile[PATH_MAX];
	WMPropList* settings;
	long amount = 0;
	const unsigned char* sequence = NULL;

	snprintf(prefsfile, PATH_MAX - 1, "%s/%s", wusergnusteppath(),
		"Defaults/wpltest");
	if (!(settings = WMReadPropListFromFile(prefsfile)))
	{
		fprintf(stderr, "Cannot read plist file %s\n", prefsfile);
		exit(1);
	}

	printf("type(settings) = %s\n", PLToString(settings));
	if (WMIsPLDictionary(settings))
	{
		WMPropList* keys;
		keys = WMGetPLDictionaryKeys(settings);
		printf("type(keys) = %s\n", PLToString(keys));
		for (int i = 0; i < WMGetPropListItemCount(keys); i++)
		{
			WMPropList* key;
			WMPropList* val;
			char* skey;
			char* sval;

			key = WMGetFromPLArray(keys, i);
			printf("type(key = keys[%d]) = %s\n", i,
				PLToString(key));
			skey = WMGetFromPLString(key);
			printf("key = %s\n", skey);

			val = WMGetFromPLDictionary(settings, key);
			printf("type(val = settings{%s}) = %s\n", skey,
				PLToString(val));
			if (WMIsPLData(val))
			{
				const unsigned char* data;
				const unsigned char* pdata;
				data  = WMGetPLDataBytes(val);
				pdata = data;
				printf("data(val) = ");
				while (*pdata)
				{
					printf("0x%02X ", *pdata);
					pdata++;
				}
				printf("\n");
				if (!strcmp(skey, "Sequence"))
					sequence = data;
			}
			else if (WMIsPLString(val))
			{
				long lval = 0;
				sval = WMGetFromPLString(val);
				errno = 0;
				lval  = strtol(sval, NULL, 10);
				if (!lval
					&& (errno == EINVAL || errno == ERANGE))
					printf("string(val) = %s\n", sval);
				else
					printf("number(val) = %ld\n", lval);
				if (!strcmp(skey, "Amount"))
					amount = lval;
			}
		}
		WMReleasePropList(keys);
		if (save_prefs(amount, sequence))
			fprintf(stderr, "save_prefs failed\n");

	}

	WMReleasePropList(settings);

	return 0;
}

Generated by slweb © 2020-2023 Strahinya Radich.