najczęstsza przyczyną problemów z bezpieczeństwem są błędy w zarządzaniu pamięcią – znane grupowo pod nazwą buffer overflowów. poza nimi jest kilka innych typów błędów które co prawda nie tworzą zagrożeń bezpieczeństwa systemu, ale np. po wodują krytyczne wyjścia albo zawieszenia z aplikacji.
błędy te mają różną postać, ale w większości sprowadzają się do mniej więcej tego samego.
zauważyli to badacze z uniwersytetu w amherst, którzy następnie wraz z programistami z intela i microsoftu stworzyli interesujące narzędzie: diehard.
diehard jest biblioteką której załadowanie przed uruchomieniem programu zabezpiecza przed większością standardowych problemów w obsłudze pamięci.
diehard nie zapewnia bezpieczeństwa – on po prostu powoduje, że pewne błędy są lepiej obsługiwane – w sposób nie zagrażający ani bezpieczeństwu ani stabilności.
biblioteka ta jest napisana tak by działała zarówno na windows jak i linuksie (są też źródła, więc da się odpalić na innych systemach). z tym, że na windowsach potrafi jak na razie zabezpieczać tylko jedną aplikację – firefoxa. na linuksie może zabezpieczać dowolny soft.
Author: depesz
rozszerzanie funkcjonalności serwisów webowych
pewnie spora część was – moich czytelników – wie co to apache i nie raz stawiała serwisy pod nim. a ilu/ile z was zna i korzysta z mod_rewrite?
mod_rewrite jest najwspanialszym modułem jaki kiedykolwiek powstał dla apache'a. i najgorszym koszmarem.
w dokumentacji do tego modułu można znaleźć dwa motta:
"The great thing about mod_rewrite is it gives you all the configurability and flexibility of Sendmail. The downside to mod_rewrite is that it gives you all the configurability and flexibility of Sendmail."
to jest zabawne, ale tylko dla ludzi znających sendmaila.
drugie motto jest zdecydowanie prostsze w odbiorze:
"Despite the tons of examples and docs, mod_rewrite is voodoo. Damned cool voodoo, but still voodoo."
czy jednak tak faktycznie jest? czy jest to niemożliwe do opanowania?
dziś chciałbym pokazać dwa proste przykłady użycia mod_rewrite.
jest sobie taki program – request tracker. służy on rejestracji i obsługi wszelkiego rodzaju "zgłoszeń". zgłoszenia są numerowane, a dodatkowo każde zgłoszenie jest w określonej "kolejce" – czymś jakby katalogu/kategorii.
system działa nawet fajnie (z dokładnością do wydajności, ale o tym już pisałem). natomiast brakuje mi kilku prostych rzeczy. dokładniej – możliwości podania prostego w urlu, że chcę zobaczyć zgłoszenie numer XXX, czy dane z kolejki YYY.
o ile zgłoszenie numer XXX ma jeszcze prostego urla:
http://rt.domena/Ticket/Display.html?id=XXX
o tyle url do obejrzenia kolejki jest "wygięty", gdyż działa z użyciem mechanizmu search'a, więc wygląda tragicznie:
http://rt.domena/Search/Results.html?Query=Queue%20=%20'YYY'%20AND%20(Status%20=%20'open'%20OR%20Status%20=%20'new')&Rows=50
wkurzające. i niemożliwe do wpisania "z palca".
tak więc stwierdziłem, że dobuduję sobie obsługę odpowiednich urli. ideałem byłoby dorobienie tego bez modyfikowania kodu – aby nie musieć back-portować poprawek po każdy upgrade'dzie.
tak więc, do definicji wirtuala rt.domena dopisałem te 3 linijki:
RewriteEngine on
RewriteRule ^/([0-9]+)$ /Ticket/Display.html?id=$1 [R,L]
RewriteRule ^/([A-Za-z0-9-]+)$ /Search/Results.html?Query=Queue\%20=\%20'$1'\%20AND\%20(Status\%20=\%20'open'\%20OR\%20Status\%20=\%20'new')&Rows=50 [NE,R,L]
co one robią?
pierwsza linijka – po prostu włącza silnik rewrite'ów.
druga – każdy url zaczynający pasujący do regexpa ^/([0-9]+)$ (czyli każdy typu: http://rt.domena/123, gdzie zamiast 123 może być dowolna liczba) zamienia na http://rt.domena/Ticket/Display.html?id=123
oczywiście numer ticketu (123) jest przepisywany.
flagi na końcu – [R,L] oznaczają odpowiednio, że (R) wynikiem rewrite'a ma być redirect (http/302), oraz (L), że przetwarzanie reguł ma się zakończyć na tej – o ile zostanie dopasowana.
druga linijka dopasowuje się do wszystkich urli takich jak: http://rt.domena/COSTAM
gdzie COSTAM jest ciągiem znaków składającym się z dużych i małych liter oraz cyfr.
tym razem do flag dodałem dodatkowo "NE". ‘NE' oznacza, że url ma zostać przesłany do przeglądarki bez dalszej obróbki url'i (escape'owania przy pomocy url-encodingu).
i to wszystko.
dzięki temu teraz mogę używać urli:
http://rt.domena/NUMER_TICKETU
http://rt.domena/NAZWA_KOLEJKI
i działają one zgodnie z przewidywaniami 🙂
tak więc – polecam przyjrzenie się mod_rewrite'owi. pozwala on w trywialny sposób dodać nowe funkcjonalności do istniejących serwisów. i popsuć wszystko w sposób który będzie się wydawał całkowicie magiczny. no cóż – jak voodoo, to voodoo.
mimo tego – polecam poznanie tego modułu. osobiście ratował mi odwłok przynajmniej kilkanaście razy.
drzewa w sql’u – metoda wielu tabel
tak naprawdę to nazwa "metoda wielu tabel" nie oddaje w pełni tego o co chodzi, natomiast jest pewnym przybliżeniem.
zgodnie z tą metodą należy stworzyć dla każdego poziomu zagnieżdżenia tabelę.
w oparciu o nasze dane testowe należy stworzyć system tabel:
CREATE TABLE categories_1 (
id BIGSERIAL PRIMARY KEY,
codename TEXT NOT NULL DEFAULT ''
);
CREATE UNIQUE INDEX ui_categories_1_cn ON categories_1 (codename);
CREATE TABLE categories_2 (
id BIGSERIAL PRIMARY KEY,
parent_id INT8 NOT NULL DEFAULT 0,
codename TEXT NOT NULL DEFAULT ''
);
ALTER TABLE categories_2 ADD FOREIGN KEY (parent_id) REFERENCES categories_1 (id);
CREATE UNIQUE INDEX ui_categories_2_picn ON categories_2 (parent_id, codename);
CREATE TABLE categories_3 (
id BIGSERIAL PRIMARY KEY,
parent_id INT8 NOT NULL DEFAULT 0,
codename TEXT NOT NULL DEFAULT ''
);
ALTER TABLE categories_3 ADD FOREIGN KEY (parent_id) REFERENCES categories_2 (id);
CREATE UNIQUE INDEX ui_categories_3_picn ON categories_3 (parent_id, codename);
CREATE TABLE categories_4 (
id BIGSERIAL PRIMARY KEY,
parent_id INT8 NOT NULL DEFAULT 0,
codename TEXT NOT NULL DEFAULT ''
);
ALTER TABLE categories_4 ADD FOREIGN KEY (parent_id) REFERENCES categories_3 (id);
CREATE UNIQUE INDEX ui_categories_4_picn ON categories_4 (parent_id, codename);
i w nim dane:
# select * from categories_1;
id | codename
----+----------
1 | sql
(1 row)
# select * from categories_2;
id | parent_id | codename
----+-----------+------------
1 | 1 | oracle
2 | 1 | postgresql
(2 rows)
# select * from categories_3;
id | parent_id | codename
----+-----------+----------
1 | 2 | linux
2 | 1 | linux
3 | 1 | solaris
4 | 1 | windows
(4 rows)
# select * from categories_4;
id | parent_id | codename
----+-----------+----------
1 | 2 | glibc1
2 | 2 | glibc2
(2 rows)
jak widać pojawia się pewien problem – nieobecny w innych systemach tabel – aby prawidłowo zidentyfikować element drzewa nie wystarczy nam jego numer, ale musimy też znać poziom zagłębienia.
oczywiście możliwe jest wymuszenie nadawania numerów kolejnych tak aby nie powtarzały się w różnych tabelach, ale wtedy znalezienie który element jest w której tabeli będzie nietrywialne.
teraz pora na zadania testowe dla tego układu tabel:
1. pobranie listy elementów głównych (top-levelowych)
> SELECT * FROM categories_1;
proste i miłe.
2. pobranie elementu bezpośrednio "nad" podanym elementem
dane wejściowe:
- ID : id elementu
- X : poziom zagłębienia.
jeśli X == 1 to:
brak danych
w innym przypadku:
> SELECT p.* FROM categories_[X] c join categories_[X-1] p ON c.parent_id = p.id WHERE c.id = [ID]
pojawia się problem. zapytania są zmienne. nie jest to bardzo duży problem, ale np. utrudnia wykorzystywanie rzeczy typu "prepared statements".
3. pobranie listy elementów bezpośrednio "pod" podanym elementem
dane wejściowe:
- ID : id elementu
- X : poziom zagłębienia.
> SELECT c.* FROM categories_[X] p join categories_[X+1] c ON c.parent_id = p.id WHERE p.id = [ID]
znowu pojawia sie problem z zapytaniami o zmiennej treści (nie parametrach – treści – nazwach tabel!). dodatkowo – musimy gdzieś przechowywać informację o maksymalnym zagnieżdżeniu i sprawdzać ją przed wykonaniem zapytania – aby się nie okazało, że odwołujemy się do nieistniejącej tabeli.
4. pobranie listy wszystkich elementów "nad" danym elementem (wylosowanym)
dane wejściowe:
- ID : id elementu
- X : poziom zagłębienia.
jeśli X == 1 to:
brak danych
jeśli X == 2 to:
> SELECT c1.* FROM categories_2 c2 join categories_1 c1 ON c2.parent_id = c1.id WHERE c2.id = [ID]
jeśli X == 3 to:
> SELECT c1.*, c2.*
FROM categories_3 c3 join categories_2 c2 on c3.parent_id = c2.id join categories_1 c1 ON c2.parent_id = c1.id
WHERE c3.id = [ID]
itd.
jak widać tu problem się multiplikuje. nie tylko zmieniają nam się nazwy tabel, ale wręcz cała konstrukcja zapytania zaczyna przypominać harmonijkę – z każdym ruchem (wgłąb struktury) wyciąga się.
5. pobranie listy wszystkich elementów "pod" danym elementem (wylosowanym)
dane wejściowe:
- ID : id elementu
- X : poziom zagłębienia.
- MaxX : maksymalny poziom zagłębienia w systemie
SELECT c[X+1].* FROM categories_[X+1] c[X+1] WHERE c[X+1].parent_id = [ID]
UNION
SELECT c[X+2].*
FROM categories_[X+1] c[X+1] join categories_[X+2] c[X+2] ON c[X+1].id = c[X+2].parent_id
WHERE c[X+1].parent_id = [ID]
UNION
SELECT c[X+3].*
FROM categories_[X+1] c[X+1]
join categories_[X+2] c[X+2] ON c[X+1].id = c[X+2].parent_id
join categories_[X+3] c[X+3] ON c[X+2].id = c[X+3].parent_id
WHERE c[X+1].parent_id = [ID]
...
łaaaał. robi się coraz gorzej. napisanie tego dla np. 16 poziomów zagnieżdżenia przerasta moje chęci.
6. sprawdzenie czy dany element jest "liściem" (czy ma pod-elementy)
dane wejściowe:
- ID : id elementu
- X : poziom zagłębienia.
SELECT count(*) from categories_[X+1] WHERE parent_id = [ID]
zasadniczo proste, ale trzeba pamiętać o wcześniejszym sprawdzeniu czy przypadkiem X+1 nie jest większe od maksymalnie obsługiwanego limitu – aby uniknąć błędów w bazie.
7. pobranie głównego elementu w tej gałęzi drzewa w której znajduje się dany (wylosowany) element
dane wejściowe:
- ID : id elementu
- X : poziom zagłębienia.
jeśli X == 1 to:
> SELECT c1.* FROM categories_1 c1 WHERE id = [ID]
jeśli X == 2 to:
> SELECT c1.* FROM categories_2 c2 join categories_1 c1 ON c2.parent_id = c1.id WHERE c2.id = [ID]
jeśli X == 3 to:
> SELECT c1.*
FROM categories_3 c3 join categories_2 c2 on c3.parent_id = c2.id join categories_1 c1 ON c2.parent_id = c1.id
WHERE c3.id = [ID]
itd.
eh. chyba widzicie do czego to zmierza.
jak widać – zapisywanie tak drzew ma swoje problemy. i raczej niewiele rzeczy można w tym zrobić ładnie szybko i porządnie.
zasadniczo – nie opisywałbym tej metody gdyby nie fakt iż z nieznanych powodów jest ona bardzo często sugerowana przez ludzi których odpytuję w ramach rozmowy kwalifikacyjnej. no – a skoro się pojawia, to trzeba ją omówić. choćby pobieżnie.
tapeta na dziś
drzewa w sql’u – wstęp
jakiś czas temu zainteresowała mnie metoda przechowywania wszelkiego rodzaju struktur drzewiastych w bazach danych.
dla wyjaśnienia – struktura drzewiasta jest to taki uporządkowany zbiór danych gdzie każdy element ma swój element nadrzędny, chyba, że jest elementem najwyższego rzędu.
dając przykłady z życia – drzewami są opisane wszelkiego rodzaju hierarchie – służbowe, kategorie (np. w sklepie internetowym), różnego rodzaju podziały. drzewami są też wiadomości na forach dyskusyjnych – a dokładniej – w drzewa są poukładane wątki z tych wiadomości.
ogólnie – struktury drzewiaste mają sporo zastosowań.
z tego powodu stały się one obiektem mojego zainteresowania, testów i badań. o struktury drzewiaste pytam potencjalnych pracowników na rozmowie kwalifikacyjnej.
w czasie swojej pracy z "drzewami" wyodrębniłem kilka metod które kiedyś już opisałem (link do http://www.dbf.pl/faq/tresc.html?rozdzial=1#o1_9), ale tu postaram się opisać je dokładniej i podając przy okazji wyniki pewnych testów na wydajność.
ze względu na obfitość materiału tekst ten podzielę na kilka wpisów: wstęp (to co czytacie), następnie po jednym wpisie na każdą z opisywanych metod, potem jakieś podsumowanie i na koniec kilka uwag praktycznych do wybranej przeze mnie metody.
ponieważ testowanie wydajności powinno odbywać się na sporych zestawach danych, a jednocześnie pokazywanie "o co mi chodzi" powinno być na możliwie małych ilościach – na potrzeby tego tematu przygotowałem 2 zupełnie różne zestawy danych:
1. małe drzewko w celach dydaktycznych:
to drzewko, zgodnie z konwencjami stosowanymi w tym dokumencie, ma następujące elementy:
- sql
- sql.oracle
- sql.oracle.linux
- sql.oracle.linux.glibc2
- sql.oracle.linux.glibc1
- sql.oracle.solaris
- sql.oracle.windows
- sql.postgresql
- sql.postgresql.linux
2. spory zestaw danych pobrany z katalogu dmoz – lista kategorii
dane z dmoza poddałem obróbce usuwając z nich znaki spoza zestawu A-Za-z0-9_, i ustawiając separator elementów drzewa na ".".
powstała lista elementów ma 478,870 elementów.
ścieżki z dmoz zawierają od 3 do 247 znaków, największe zagłębienie w drzewo jest w elemencie Regional.North_America.United_States.New_York.Localities.N.New_York_City.Brooklyn.Society_and_Culture.Religion.Christianity.Catholicism.Eastern_Rites.Maronite
w celu porównania wydajności różnych metod zapisu drzew będę testował następujące scenariusze testowe:
- pobranie listy elementów głównych (top-levelowych)
- pobranie elementu bezpośrednio "nad" podanym elementem
- pobranie listy elementów bezpośrednio "pod" podanym elementem
- pobranie listy wszystkich elementów "nad" danym elementem (wylosowanym)
- pobranie listy wszystkich elementów "pod" danym elementem (wylosowanym)
- sprawdzenie czy dany element jest "liściem" (czy ma pod-elementy)
- pobranie głównego elementu w tej gałęzi drzewa w której znajduje się dany (wylosowany) element
dodatkowo zostanie sprawdzona wielkość tabel i indeksów na założonej strukturze danych.
dla porównania – zapis w postaci takiej jak w pliku źródłowym zajmuje:
– tabela dmoz: 57,319,424 bajtów
– index pkey: 8,609,792 bajtów
– unikalny indeks ścieżki: 51,888,128 bajtów
tak więc – w ciągu najbliższego czasu będę pokazywał kolejne metody oraz wyniki ich testów.
two for the money / podwójna gra
obejrzałem sobie tę (dosyć nową (2005) w sumie) produkcję.
ocena ogólna – około 7/10. film nie jest zły. jest nawet dobry. ogląda sie go przyjemnie.
jedynym problemem tego filmu jest to, że spodziewałem się więcej. w roli głównej al pacino. człowiek legenda. heat (gorączka) oglądam co jakiś czas by zobaczyć kawał wspaniałego kina. ojciec chrzestny, adwokat diabła (film zebrał takie sobie oceny, ale rola ala w nim jest niesamowita).
tu – oczywiście nadal jest tym samym diabłem. świetnym. tyle, że trochę jednak starszym. mającym swoje problemy.
na dokładkę do ala w filmie jest rene russo – z mojego punktu widzenia znana głównie z zabójczej broni 3 i 4, ale zdarzyło jej się zagrać też w innych, nie najgorszych, filmach.
no i trzeci aktor pierwszoplanowy – matthew mcconaughey. ciekawe ile z was kojarzy nazwisko. mnie sie on głównie kojarzy z rolą w świetnym kontakcie (contact) z jodie foster.
i rene i matthew zagrali poprawnie. w/g mnie przemiana brandona w john'a jest mało przekonująca (spokojnie, nie zdradzam tu nic tajnego). russo jest zdecydowanie bardziej przekonująca – może dlatego, że ta rola powstała dla niej i z myślą o niej jako odtwórczyni 🙂
kończąc – co by się nie rozpisywać i nie zdradzać nic z fabuły – film jest przyjemny. nie jest to arcydzieło kina światowego, ale spędziłem miłe 2 godzinki oglądając go. drugi raz nie zamierzam, ale żałować, nie żałuję.
tapeta na dziś
tapeta na dziś
eksperymenty kuchenne – kurczak w porach
od poprzedniego wpisu kuchennego minęło już trochę czasu (w międzyczasie udoskonaliłem przepis na zupę, ale o tym opow iem innym razem).
tym razem inspiracją był post na newsach.
opis jest zasadniczo jasny, ale jeśli ktoś z czytających nie wie jak do tego podejść – poniżej opis ze zdjęciami co i jak.
najpierw kilka uwag ogólnych:
- używamy piersi kurczaka, ale one nie będą "suche" jak to zazwyczaj bywa. zdecydowanie nie.
- w żarciu jest sporo porów, ale smakują one rewelacyjnie – zupełnie inaczej niż się spodziewałem. naprawdę
- przygotowanie żarcia w/g tego co poniżej to około doby, z tym, że to czas oczekiwania. samej roboty jest około 15 minut.
- podane ilości są orientacyjne. jak zamiast 4 piersi zrobicie 2, to trzeba też zmniejszyć ilość porów.
ok. jak już podstawy są jasne, to lecimy.
zaczynamy od porów:
na zdjęciu są 2, ale finalnie dałem 3 sztuki.
pory kroimy w tzw. talarki:
taka uwaga – z pora zdejmuje sie zewnetrzna warstwe, potem odkrawa korzonki i tnie na talarki. tniemy pora na takiej długości by talarki wychodziły "spójne". potem znowu zdzieramy warstwę, i dalej kroimy. ze standardowego pora wykorzystuje około 60% jego długości na talarki.
pory wrzucam do garnka:
i solę. ilość soli – w/g uznania. na 3 pory wsypałem do środka mniej więcej 1 łyżeczkę soli.
garnek (czy cokolwiek tak naprawdę) wstawiam do lodówki. na kilka godzin.
realnie – pory "robię" wieczorem – i na następny wieczór je używam. czyli w lodówce spędzają około 20 godzin.
następnego dnia rano wyciągam z lodówki kurczaki – piersi, i rozmrażam. jeśli masz rozmrożone, to ten etap można pominąć 🙂
po rozmrożeniu (w moim przypadku to koło 12:00 jest – co utrudnia robienie tego dania w czasie chodzenia do pracy), wykładamy kurczaki na deskę:
jak widać – oddzieliłem z piersi mniejsze kawałki tak aby były "luzem". nie jest to konieczne – tak mi po prostu było wygodniej przygotować.
nasŧępnie – biorę czosnek
wyłuskuję z niego ząbki – mniej więcej po dwa ząbki na pierś.
tu uwaga – w oryginalnym przepisie czosnku nie było. ja dodałem i sobie chwalę. można dodać więcej jak ktoś lubi, jak nie – bez czosnku też smakuje super.
czosnek zgniatam:
i wcieram w kurczaki:
następnie, biorę "warzywko" (inne nazwy: wegeta, kucharek, pewnie są też inne):
i lekko obsypuję nim kurczaki:
nastepnie, biorę sporą miskę i układam w niej kurczaki:
w międzyczasie podsypuję warzywko, tak aby każdy kurczak był z obu stron obsypany.
po wrzuceniu wszystkich kurczaków do miski wstawiam ją do lodówki:
i mam znowu kilka godzin wolnego. ciąg dalszy następuje wieczorem – można o 16, można później. im później tym zasadniczo lepiej dla smaku, gorzej dla zdrowia (bo kto późno je i potem idzie spać …). jak już mnie najdzie głód, nastawiam piekarnik na 180 stopni, i biorę brytfannę:
tu uwaga – ja mam taką szklaną, ale każda może być. w szczególności – pierwsze 2 "iteracje" robienia kurczaka robiłem w takich jednorazowych foremkach "jana niezbędnego" z supermarketu.
nastepnie, biorę masło:
i smaruję dno i boki, aż całość będzie wysmarowana:
tu kolejna uwaga – tych jednorazowych nie trzeba smarować. a przynajmniej – ja nie smarowałem i wyszło ok.
następnie – do brytfanny wrzucam pory z lodówki. jak coś z nich "wyciekło" – to też wlewam:
pory układam równo i na nich kurczaka. jeśli jest możliwość – dobrze jest zostawić trochę miejsca między kurczakami – ja nie miałem takiej możliwości:
następnie, biorę majonez – sam używam kieleckiego, ale pewnie na każdym wyjdzie dobrze:
i układam grubą warstwę na kurczakach (jeśli są przerwy między kurczakami, to w nich nie dajemy majonezu):
następnie na to – tzn. na majonez, czyli w moim przypadku na całą powierzchnię, układamy plasterki (nie za grube, takie standardowe, kanapkowe) żółtego sera. ser musi być bez-dziurkowy. innych specjalnych życzeń nie ma – sam użyłem morskiego:
gotowe. w międzyczasie piekarnik powinien się już dobrze rozgrzać. więc przykrywam brytfannę (ale np. tych jednorazowych nie przykrywałem, a jak raz przykryłem to wyszło nie za dobrze) i wsadzam do piekarnika – tak po środku:
i czekam godzinę. po godzinie całość wygląda tak:
po wyłożeniu na talerz wygląda w ten sposób – może mało apetycznie, ale to naprawdę jest fenomenalnie dobre:
i to wszystko. całość przygotowań trwała krócej niż pisanie tego wpisu 🙂
smacznego.
grafika komputerowa
rendering. raytracing. cg. wiele nazw, ta sama (mniej więcej) technologia.
technologia używana od dawna służyła w założeniach do tworzenia foto-realistycznych grafik w komputerze. rozmieszczamy kilka obiektów na scenie, nadajemy im tekstury, światło, umieszczamy kamerę i robimy "zdjęcie".
jak to wyglądało widać było w filmach – terminator 2, jurrasic park. są filmy gdzie ta grafika z definicji ma wyglądać nierealistycznie (toy story i inne filmy dla dzieci), są takie gdzie ma wyglądać jak najlepiej (final fantasy). są też takie – i jest ich najwięcej, gdzie grafika komputerowa daje tylko efekty. coś dodatkowego, trudnego, bądź nierealnego do zrobienia – day after tomorrow, harry potter.
zawsze jednak przy dokładniejszym przyjrzeniu można się dopatrzyć gdzie jest granica między rzeczywistością, a wykreowanym sztucznie światem 3d.
czy jednak zawsze? autodesk pokazał krótki test – 10 zdjęć. trzeba tylko wybrać które z nich są zdjęciami, a które grafiką komputerową. mnie poszło podobno nie źle – 8 trafień. ale nie było to zbyt łatwe