From d61bed2b2dcca27d8baecc2b63255f9df456ba2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Fri, 27 Jul 2018 01:16:00 +0200 Subject: [PATCH] Split refNamesFromImageReference from Runtime.getPullListFromRef MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Again, that makes the core logic independent from Runtime == containers-storage, and easier to test independently. So, this also adds tests. Should not change behavior. Signed-off-by: Miloslav Trmač Closes: #1176 Approved by: rhatdan --- libpod/image/pull.go | 15 ++- libpod/image/pull_test.go | 109 ++++++++++++++++++ libpod/image/testdata/docker-name-only.tar.xz | Bin 0 -> 1024 bytes .../testdata/docker-registry-name.tar.xz | Bin 0 -> 1028 bytes .../image/testdata/docker-two-images.tar.xz | Bin 0 -> 1416 bytes libpod/image/testdata/docker-two-names.tar.xz | Bin 0 -> 1040 bytes libpod/image/testdata/docker-unnamed.tar.xz | Bin 0 -> 968 bytes libpod/image/testdata/oci-name-only.tar.gz | Bin 0 -> 975 bytes .../image/testdata/oci-non-docker-name.tar.gz | Bin 0 -> 991 bytes .../image/testdata/oci-registry-name.tar.gz | Bin 0 -> 979 bytes libpod/image/testdata/oci-unnamed.tar.gz | Bin 0 -> 928 bytes 11 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 libpod/image/testdata/docker-name-only.tar.xz create mode 100644 libpod/image/testdata/docker-registry-name.tar.xz create mode 100644 libpod/image/testdata/docker-two-images.tar.xz create mode 100644 libpod/image/testdata/docker-two-names.tar.xz create mode 100644 libpod/image/testdata/docker-unnamed.tar.xz create mode 100644 libpod/image/testdata/oci-name-only.tar.gz create mode 100644 libpod/image/testdata/oci-non-docker-name.tar.gz create mode 100644 libpod/image/testdata/oci-registry-name.tar.gz create mode 100644 libpod/image/testdata/oci-unnamed.tar.gz diff --git a/libpod/image/pull.go b/libpod/image/pull.go index f7131f11e9..bbabba7b9f 100644 --- a/libpod/image/pull.go +++ b/libpod/image/pull.go @@ -84,8 +84,8 @@ func getPullRefName(srcRef types.ImageReference, destName string) (*pullRefName, }, nil } -// returns a list of pullRefPair with the srcRef and DstRef based on the transport being used -func (ir *Runtime) getPullListFromRef(ctx context.Context, srcRef types.ImageReference, imgName string, sc *types.SystemContext) ([]*pullRefPair, error) { +// refNamesFromImageReference returns a list of pullRefName for a single ImageReference, depending on the used transport. +func refNamesFromImageReference(ctx context.Context, srcRef types.ImageReference, imgName string, sc *types.SystemContext) ([]*pullRefName, error) { var pullNames []*pullRefName splitArr := strings.Split(imgName, ":") archFile := splitArr[len(splitArr)-1] @@ -178,8 +178,17 @@ func (ir *Runtime) getPullListFromRef(ctx context.Context, srcRef types.ImageRef } pullNames = append(pullNames, pullInfo) } + return pullNames, nil +} - return ir.pullRefPairsFromRefNames(pullNames) +// getPullListFromRef returns a list of pullRefPair for a single ImageReference, depending on the used transport. +func (ir *Runtime) getPullListFromRef(ctx context.Context, srcRef types.ImageReference, imgName string, sc *types.SystemContext) ([]*pullRefPair, error) { + refNames, err := refNamesFromImageReference(ctx, srcRef, imgName, sc) + if err != nil { + return nil, err + } + + return ir.pullRefPairsFromRefNames(refNames) } // pullImage pulls an image from configured registries diff --git a/libpod/image/pull_test.go b/libpod/image/pull_test.go index e5e9f6b91d..9305527d47 100644 --- a/libpod/image/pull_test.go +++ b/libpod/image/pull_test.go @@ -1,6 +1,7 @@ package image import ( + "context" "fmt" "io/ioutil" "os" @@ -78,6 +79,114 @@ func TestGetPullRefName(t *testing.T) { } } +func TestRefNamesFromImageReference(t *testing.T) { + type expected struct{ image, dstName string } + for _, c := range []struct { + srcName string + expected []expected + }{ + // == docker-archive: + {"docker-archive:/dev/this-does-not-exist", nil}, // Input does not exist. + {"docker-archive:/dev/null", nil}, // Input exists but does not contain a manifest. + // FIXME: The implementation has extra code for len(manifest) == 0?! That will fail in getImageDigest anyway. + { // RepoTags is empty + "docker-archive:testdata/docker-unnamed.tar.xz", + []expected{{"@ec9293436c2e66da44edb9efb8d41f6b13baf62283ebe846468bc992d76d7951", "@ec9293436c2e66da44edb9efb8d41f6b13baf62283ebe846468bc992d76d7951"}}, + }, + { // RepoTags is a [docker.io/library/]name:latest, normalized to the short format. + "docker-archive:testdata/docker-name-only.tar.xz", + []expected{{"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}}, + }, + { // RepoTags is a registry/name:latest + "docker-archive:testdata/docker-registry-name.tar.xz", + []expected{{"example.com/empty:latest", "example.com/empty:latest"}}, + }, + { // RepoTags has multiple items for a single image + "docker-archive:testdata/docker-two-names.tar.xz", + []expected{ + {"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}, + {"example.com/empty:latest", "example.com/empty:latest"}, + }, + }, + { // FIXME: Two images in a single archive - only the "first" one (whichever it is) is returned + // (and docker-archive: then refuses to read anything when the manifest has more than 1 item) + "docker-archive:testdata/docker-two-images.tar.xz", + []expected{{"example.com/empty:latest", "example.com/empty:latest"}}, + // "example.com/empty/but:different" exists but is ignored + }, + + // == oci-archive: + {"oci-archive:/dev/this-does-not-exist", nil}, // Input does not exist. + {"oci-archive:/dev/null", nil}, // Input exists but does not contain a manifest. + // FIXME: The remaining tests are commented out for now, because oci-archive: does not work unprivileged. + // { // No name annotation + // "oci-archive:testdata/oci-unnamed.tar.gz", + // []expected{{"@5c8aca8137ac47e84c69ae93ce650ce967917cc001ba7aad5494073fac75b8b6", "@5c8aca8137ac47e84c69ae93ce650ce967917cc001ba7aad5494073fac75b8b6"}}, + // }, + // { // Name is a name:latest (no normalization is defined). + // "oci-archive:testdata/oci-name-only.tar.gz", + // []expected{{"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}}, + // }, + // { // Name is a registry/name:latest + // "oci-archive:testdata/oci-registry-name.tar.gz", + // []expected{{"example.com/empty:latest", "example.com/empty:latest"}}, + // }, + // { // Name exists, but is an invalid Docker reference; such names are passed through, and will fail when intepreting dstName. + // "oci-archive:testdata/oci-non-docker-name.tar.gz", + // []expected{{"UPPERCASE-IS-INVALID", "UPPERCASE-IS-INVALID"}}, + // }, + // Maybe test support of two images in a single archive? It should be transparently handled by adding a reference to srcRef. + + // == dir: + { // Absolute path + "dir:/dev/this-does-not-exist", + []expected{{"localhost/dev/this-does-not-exist", "localhost/dev/this-does-not-exist"}}, + }, + { // Relative path, single element. + // FIXME? Note the :latest difference in .image. (In .dstName as well, but it has the same semantics in there.) + "dir:this-does-not-exist", + []expected{{"localhost/this-does-not-exist:latest", "localhost/this-does-not-exist:latest"}}, + }, + { // Relative path, multiple elements. + // FIXME: This does not add localhost/, and dstName is parsed as docker.io/testdata. + "dir:testdata/this-does-not-exist", + []expected{{"testdata/this-does-not-exist", "testdata/this-does-not-exist"}}, + }, + + // == Others, notably: + // === docker:// (has ImageReference.DockerReference) + { // Fully-specified input + "docker://docker.io/library/busybox:latest", + []expected{{"docker://docker.io/library/busybox:latest", "docker.io/library/busybox:latest"}}, + }, + { // Minimal form of the input + "docker://busybox", + []expected{{"docker://busybox", "docker.io/library/busybox:latest"}}, + }, + + // === tarball: (as an example of what happens when ImageReference.DockerReference is nil). + { // FIXME? The dstName value is invalid, and will fail. + // (This is NOT an API promise that the results will continue to be this way.) + "tarball:/dev/null", + []expected{{"tarball:/dev/null", "tarball:/dev/null"}}, + }, + } { + srcRef, err := alltransports.ParseImageName(c.srcName) + require.NoError(t, err, c.srcName) + + res, err := refNamesFromImageReference(context.Background(), srcRef, c.srcName, nil) + if len(c.expected) == 0 { + assert.Error(t, err, c.srcName) + } else { + require.NoError(t, err, c.srcName) + require.Len(t, res, len(c.expected), c.srcName) + for i, e := range c.expected { + assert.Equal(t, &pullRefName{image: e.image, srcRef: srcRef, dstName: e.dstName}, res[i], fmt.Sprintf("%s #%d", c.srcName, i)) + } + } + } +} + const registriesConfWithSearch = `[registries.search] registries = ['example.com', 'docker.io'] ` diff --git a/libpod/image/testdata/docker-name-only.tar.xz b/libpod/image/testdata/docker-name-only.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..0cad9f108396df4ef725c489dab16b6c3ecc94a0 GIT binary patch literal 1024 zcmV+b1poW}H+ooF000E$*0e?f03iVu0001VFXf})DgOh#T>u=JLgNtLTxM;WQ4u$=d`wd?7@|5dG7-5d{TtQ+sES;Skl4yH16G7Fk@|#r8h6}v3&nwKq zeob^=<#_nKw04qneXu$!j$EosUzAX3OhH_Hq8k|+?ZuJ`nX*?eZ0`UIz1gYQWE}Y;+CS%Abcg6OzzbJdUL4b0wHuM@+y_7yhht z?;jccSC?fASauI{<>k!1g3N?MW^SSYJ+sxSw^g&`1$DS?*IZqlD5MhTEsmBOSn~mB zHjxf4a#OO8H5UT{V3ZP8=YvB0_;>5pNMLT3YjR^bQBdMklum#!3>8aq3_-&_Lg*ZM4DZ_7-GGCKzr{!!7TOxTKgEP~ zWlT7{hYhp>ff2WN#}!grZQBUrBp0|<*Z;NncgtDh@M1~6JB!7|qrT1BVhyu;4$TV`L0eRv9`uM8AX}0%*8ZAB^*13= u5zG)i^R(6g00007x6c|YW9q8_0oezDQ~&^db*RFz#Ao{g000001X)_|^YV27 literal 0 HcmV?d00001 diff --git a/libpod/image/testdata/docker-registry-name.tar.xz b/libpod/image/testdata/docker-registry-name.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..181816c2e569f57098020166140f397b4bb2ccc7 GIT binary patch literal 1028 zcmV+f1pE8_H+ooF000E$*0e?f03iVu0001VFXf})DgOh(T>u=JLgNtLTxM;WQ4u$=d`wd?7@|5dG7-5d{TtQ+sES;Skl4yH16G7Fk@|#r8h6}v3&nwKq zeob^=<#_nKw04qneXu$!j$EosUzAX3OhH_Hq8k|+?ZuJ`nX*?eZ0`UIz1gYQWE}Y;+CS%Abcg6OzzbJdUL4b0wHuM@+y_7yhht z?;jccSC?fASauI{<>k!1g3N?MW^SSYJ+sxSw^g&`1$DS?*IZqlD5MhTEsmBOSn~mB zHjxf4a#OO8H5UT{V3ZP8=YvB0_;>5pNMLT3YjR^bQBdMklum#!3>8aq3_-&_Lg*ZM4DZ_7-GGCKzr{!!7TOxTKgEP~ zWlT7{hYhp>ff2WN#}!grZQBUrBp0|<*Z;NncgtDh@M1~6JB!7|qrT1BVhyu;4$TV`L0h-y2JTLq4o7jXuwJw#a_MjOsf2ZKQ&@ap|yi;Y|}Aa;ifzUS|Hawb7* y_4u)GLgsN~w9b^4e;~T5 zci!}2p$e^qo2Z_>iB8f9s5$z=_r8VW_|3bj7Bg0L!W*)1qUq_|^dnxknZ`9bJ6dO% zg1b2^8&Oc!F@GRyI=F$DXmR@h(>zOaPGWfqWpGV1(FSQ;Ae&pydAJB9ud>qLh`>aF zU+J8Ynra46(#(GB1VT={3n<=J@cwQ6)yec_U0E^w6?fs;47AJMFqdFvgm3KmBm!hS zOx1(SX+}1LD}W{Mr*IgzEI34aJ4;o5ZmQw@q_7{y5ImshWI`~zvMkDL6!73(@LDld zDf(wfi`q!C7#~{hR9uG4(OKG;1sBkxHzQ^;cEmCuLOl0kS&zkG>uFAWKHwhe?R4~d z%@OrFggABKew(DNgIFYX+Y+SKVUeQ|Bc`2oaCrUZl3xdz2%Ceu7ao1i^9F8!m)U9N zlvdym-RDST-fV^b1LH6Rm;h`{TN#@tmn!9(&0tS_=J~6-+ z3g{VK*X)AN91EWLk}D#8sGa^TVW>-?bCO^;GejRW=@!5&R#4jK6*>7~g`>3zwTPgg z=@A0qPHJnM({)c4GPjbgitlfZG;J#bpd{Ek%5-&OqaG%#`gzra5C7%~v8O7~@Zl1KD&$)@JA32>Qa`(Jy8bur|PjMeP# z4z^o|9lOf4Jf%)8QjWHBemUqB-4KWPLBw+L$eD7JcAzPs!ql8%pyoZx%!c~*+B*uI z_9_J*ZzcTZ6fl-d7~}8XJ~*|X89b8T0X#5RjbN)rmT%eixyw-e)y8CgG4%#hDi7iR zKD)QCmPt?ky?X?1VB%HsphjXMgJxL^K8)iV(qwy;IfKQ3@5! zn8~8m>Y|0<$Y3m*bW1SR-ij7S8$SQ6r(KKBori|W-RcP0l>-7*sp>Q?ifXd8(D{iW z{-8|9&iGUVS^)ivog~S?tVi{ly6ABUdmly=Fl=R`vdz(`E@S1mPQ@^O>M^HyR_eNISQmGU)D^vX z)3jdoUnK+fmfvZA20-#j4m8-1WFQjeC@Kr;0~&&LCaIUcf7`GR|E?5+E4Qq=k6BR_ z?m~HpMO2{|uN~mWG`28aUzDm&qtU-eWYutcqV!dbQH;c`gup)l-#A-=5=qB>l0zr5 zNphZX>d7t%v@>`jl4K9@s}bA2GIJ5D%LA?}GEKN!H9a!;Js5U`;tz=v*>^&q);s!l zV|bv;C2o|Z4ENFt;p3Vaz@+ZDZ#v*$(%%l~3e1+NdLWor)#Ao{g000001X)@$IL(H+ooF000E$*0e?f03iVu0001VFXf})DgOh`T>u=JLgNtLTxM;WQ4u$=d`wd?7@|5dG7-5d{TtQ+sES;Skl4yH16G7Fk@|#r8h6}v3&nwKq zeob^=<#_nKw04qneXu$!j$EosUzAX3OhH_Hq8k|+?ZuJ`nX*?eZ0`UIz1gYQWE}Y;+CS%Abcg6OzzbJdUL4b0wHuM@+y_7yhht z?;jccSC?fASauI{<>k!1g3N?MW^SSYJ+sxSw^g&`1$DS?*IZqlD5MhTEsmBOSn~mB zHjxf4a#OO8H5UT{V3ZP8=YvB0_;>5pNMLT3YjR^bQBdMklum#!3>8aq3_-&_Lg*ZM4DZ_7-GGCKzr{!!7TOxTKgEP~ zWlT7{hYhp>ff2WN#}!grZQBUrBp0|<*Z;NncgtDh@M1~6JB!7|qrT1BVhyu;4$TV`L0oZW^DaibAdrU0W01Q#HenW);3p)4Zj9r9xU0qO^UQ~&^e07gKu#Ao{g K000001X)_w(DX+D literal 0 HcmV?d00001 diff --git a/libpod/image/testdata/docker-unnamed.tar.xz b/libpod/image/testdata/docker-unnamed.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..ba6ea1bae5cc1d1cdb94b1191aac70edb844d90c GIT binary patch literal 968 zcmV;(12_ErH+ooF000E$*0e?f03iVu0001VFXf})CI174T>u=JLgNtLTxM;WQ4u$=d`wd?7@|5dG7-5d{TtQ+sES;Skl4yH16G7Fk@|#r8h6}v3&nwKq zeob^=<#_nKw04qneXu$!j$EosUzAX3OhH_Hq8k|+?ZuJ`nX*?eZ0`UIz1gYQWE}Y;+CS%Abcg6OzzbJdUL4b0wHuM@+y_7yhht z?;jccSC?fASauI{<>k!1g3N?MW^SSYJ+sxSw^g&`1$DS?*IZqlD5MhTEsmBOSn~mB zHjxf4a#OO8H5UT{V3ZP8=YvB0_;>5pNMLT3YjR^bQBdMklum#!3>8aq3_-&_Lg*ZM4DZ_7-GGCKzr{!!7TOxTKgEP~ zWlT7{hYhp>ff2WN#}!grZQBUrBp0|<*Z;NncgtDh@M1~6JB!7|qrT1BVhyu;4$TV`L01eBGNSB#nGd#RmHqjAMDz9B$;8 q9u?QeL~v+5 ztR&M@-yaP*1QH42VGoN3C4~B%Et%;ax_g#P!a~4Hz<*+|IRQi`@JqZt!y(MoGTDwNOBHGge6BOk4G3_N{L{CPzL=lA&3r! ze6Rs5n%2}nWtQ6db^T7ee)J$m#|r|x(!bd?gp2WiMt@3hum9%t=S~@C6sFQRCWB&5 zXcIJbL2%@P7E)vB9LQ&lG{$qLL0KA&liVsRUiE4)LV~eZ>MxP}tNw&x3Hqa6|8vLv z%+3xf!QWK_jcF7ds*>)wtao)*0u4XsmIgGnd!K(sVq*kkDGs9Se>k!@RQx%znaxm z)yAe9wq4G<$){!+>INqCDaJd=H{D$8bzFF7jZq9)r4&+u5*-ljmb1p1dXGJmf@=*c zRxoOf4J3NPf{+qh?+jVZxSN``s+TbA8@;2$u6HclnRWERT*HwYe!f*!^Dl?jZ`>Hh zGz~?F(o^rKCl=IVMaP$NVq7B*HTeR-COz}%KeM} zX9D&9-vmDU>BIHO^RIs!Z9Vb_qb{uxr3O71w4eM6nt&*nQz}1rBn9@{=-~0wUk;x9 z{Keyk&mP#Pzh9Fdef4bX^7}u2I(QF;dG6)*$Ae3Q9v3hFW5iGlG1n1k!(;F&dTj+$ z#!`;8u$*8`pdcXaBN0Y3uU$k!5^Mqw>@EGz$xHc9Q1AZ@<$vRL!_0gOVVbHkE6B+R zp9P=H&N^_-d_GOiw9sq3SNgniDNi#q3HiEzzK5^;(7+H~Bu_%q!aaw1eAe$u7_v$C zfCS?llKC+DyI}rj1XnizzZ|5#GP#%Lf5Y*t*>}uQqqF4krjbe??&h6t=eYkr#s5t3 z7x#Y{iQfP3%KtM{rU>SE6N??)^O`Df)^s^`7-bapB4t^%D<-R5K~+!kY930cMXgCC zJk24oI>fv*vyT0|4sE*}t-hLzDSV{^J18e}IE__3>xMafhqHf?+@4J9YL~i%A{sY`mJsGso*?at-Ia+G41rz>{U?0QG0;OHm*(C= z|D*J`lI=Qa6E|@!*Ja;9yCcoV+kNwXPh%#fsfWFD1wsr-&f$u%^z$c=et7c0JpTQfeE0JwYwy4L{fE6bV49~+j^69N)$4Hi@;?L&#Sn89 zkkUMOmj$PcV3{_QV`)NV!0SR?I0IkdOpx&pmrh|8w$}{3odM|BCXzwmW{R zZ$g;HvPg1rGQy|cMZLWYTs@mjqSX!b8txV@Ep1HWRF8eS?4R!9D|c#`qK)LSuNxS1 zn8#=RUkOvz@g9(1oI^4nMt>X3|BT>b0Psse>MP@WarQSH&zgP592GiC9UNIj|5N-A5%c`{55tk5^Zy0;f2xZZz#Ok)v7<3BsRCzBmt%)fM&T?{6lJquvN#o# z)i^C@zJOZP=vcth91^R1NDDn}*?%({ZGQdP`i;%O){Vi|S2x$MZ*3${Pi7O{gtD5p z^PGOf*}qKoJ{6$g0Lo)IZ!XUNh|13Y7v}$kO|CDtz;O}iCpbkZO8!S1y+el%9XdRN NKLAfL8)X0-002cF?+yR} literal 0 HcmV?d00001 diff --git a/libpod/image/testdata/oci-registry-name.tar.gz b/libpod/image/testdata/oci-registry-name.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..e6df87339e1093fa5cbe511ffcfbdf25ba61e730 GIT binary patch literal 979 zcmV;^11$U>iwFqoWLjGS18-w#Eo5(PVQFqHZeeX@E_7jX0PR;xYur{C9SVKWO}C{K zI;dH=B%`_ShaihUT1x4{3)4kQ2={5|5wRNr=v>+LJUdH;fS#0==AXzBTOj~Oc2WH2w_4H z8IAd94Oq0DX@E+Zy5_8Y=UqR3kmKVOfnDm~?ij+w^gp9NC3w(({rYpKj57*TX&jS5 zF(f7u_W*WDXy5PD+6QI8-yL2OTwTSqofY@9&#T?6_%7SLwe{uA>7s3>v)Y;2w6&=!PPI>(e_dFfteWtX-8s0C zHFe#^wjZ|JpZAl`%zkKEn9!#f?_fqnePP5J&8Pc}Y$`-hK5Z^1B6z1;j@^v-C&)yw}FF%(10bwt|m7`%#J zTfvmElw&O{Cs-3G2uS-#gwf1v7m<(zo4^BmP5*Q9g8U!+zoz_e-A*XYHxQ<&uCjuh zj_@-0WVTj;Yv%J=a;Ae`)7{GFwM%&_&34FF{qtRX{reV%=p%VMv>n`Yl*bqSu7n}m zbPq@{&LNqPqQ4L3e@1Y51Mtg1>g(J0()_w`o?v_lJyMnse&g*%opcZu| zRq!-N#F`NE%9K6(uxHBotglz)bouFA%-~ZUC_+t{!)dIWvLEaGZO*|}viO+*1t(A* z>qYnG{Ew)7-v46o|9bqtw8_`YC2&#%#u?60jeI;ZK*Cvp)bF001UX B|JMKj literal 0 HcmV?d00001 diff --git a/libpod/image/testdata/oci-unnamed.tar.gz b/libpod/image/testdata/oci-unnamed.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..de445fdf8efdee767281770d4d2fa91f499f0218 GIT binary patch literal 928 zcmV;R17G|fiwFq2WLjGS18-w#Ep={gVQpn(E_7jX0PR;ziyTE59t1z|;#EYjX+3Pn zPEUP*G~^J762!wE77t1Y^|@Oz(>-+eEZKyGKrVuRVy`&{au9MccLo1Ze0yehT@#Zv zS$E<}KZmKV>9?xhsi&UznX=Q`wv*9?0wIPZ=g=Z7IXc}QV}vOsf(b$-8zD>xVxx?Y z)__IZnFdmsrLK8Z+*$9Bf5>rrNnn@!x4VXLG5OE%PYE9QUqAocDdUX7R2s)*P|OK! zf~GDAj$F_}YAl@t`mB-0c+NC1OQUg;TV=(|wgw|47<(!I63M^vPZ)+KLj(U8j(fRl zg6V?K%bXyr#z>Bd*e0}Os-&{eLI8PKs_*j6+%#@Cb-{IuCP2KIdBNc8x`oa(Rf{{& zadj2bPF~*2zpVE1^80-2`u5FhlSSK1rnNKENo!M8p6Z`;|9r80Sasnezjd&gH+9{` zwx71!pZAMT&376x3F=UleNCirCK(s&38f)r3_Dl+{HJn(% zs5Lf_=m`r#N^HF|WVzyQYP-7GhiPAHkB&lbTNs)5>4UhkvCBT+sOtHZ?D~xxSxnPV zhA2Jtj(TEYUFsmSz%^{Bh|X{(JQkoWL!$81g6<+$6bDU+*TgW(vQ%YZHt+Vc{NUEX zzoY-?l(*V(p<+PFS{on_ZgC{c3lT9x&M_BxbPhAp=Y@oe!gwterPRHl{-NAIuYV@+ zp#OE?v!6a#Jv{sRxADe9e>m>l8c}M{{ZaSHufT~Q6mv@D`wykSehVEwdh*NR