1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * Common methods for use with dell-wmi-sysman |
4 | * |
5 | * Copyright (c) 2020 Dell Inc. |
6 | */ |
7 | |
8 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
9 | |
10 | #include <linux/fs.h> |
11 | #include <linux/dmi.h> |
12 | #include <linux/module.h> |
13 | #include <linux/kernel.h> |
14 | #include <linux/wmi.h> |
15 | #include "dell-wmi-sysman.h" |
16 | #include "../../firmware_attributes_class.h" |
17 | |
18 | #define MAX_TYPES 4 |
19 | #include <linux/nls.h> |
20 | |
21 | struct wmi_sysman_priv wmi_priv = { |
22 | .mutex = __MUTEX_INITIALIZER(wmi_priv.mutex), |
23 | }; |
24 | |
25 | /* reset bios to defaults */ |
26 | static const char * const reset_types[] = {"builtinsafe" , "lastknowngood" , "factory" , "custom" }; |
27 | static int reset_option = -1; |
28 | static const struct class *fw_attr_class; |
29 | |
30 | |
31 | /** |
32 | * populate_string_buffer() - populates a string buffer |
33 | * @buffer: the start of the destination buffer |
34 | * @buffer_len: length of the destination buffer |
35 | * @str: the string to insert into buffer |
36 | */ |
37 | ssize_t populate_string_buffer(char *buffer, size_t buffer_len, const char *str) |
38 | { |
39 | u16 *length = (u16 *)buffer; |
40 | u16 *target = length + 1; |
41 | int ret; |
42 | |
43 | ret = utf8s_to_utf16s(s: str, strlen(str), endian: UTF16_HOST_ENDIAN, |
44 | pwcs: target, maxlen: buffer_len - sizeof(u16)); |
45 | if (ret < 0) { |
46 | dev_err(wmi_priv.class_dev, "UTF16 conversion failed\n" ); |
47 | return ret; |
48 | } |
49 | |
50 | if ((ret * sizeof(u16)) > U16_MAX) { |
51 | dev_err(wmi_priv.class_dev, "Error string too long\n" ); |
52 | return -ERANGE; |
53 | } |
54 | |
55 | *length = ret * sizeof(u16); |
56 | return sizeof(u16) + *length; |
57 | } |
58 | |
59 | /** |
60 | * calculate_string_buffer() - determines size of string buffer for use with BIOS communication |
61 | * @str: the string to calculate based upon |
62 | * |
63 | */ |
64 | size_t calculate_string_buffer(const char *str) |
65 | { |
66 | /* u16 length field + one UTF16 char for each input char */ |
67 | return sizeof(u16) + strlen(str) * sizeof(u16); |
68 | } |
69 | |
70 | /** |
71 | * calculate_security_buffer() - determines size of security buffer for authentication scheme |
72 | * @authentication: the authentication content |
73 | * |
74 | * Currently only supported type is Admin password |
75 | */ |
76 | size_t calculate_security_buffer(char *authentication) |
77 | { |
78 | if (strlen(authentication) > 0) { |
79 | return (sizeof(u32) * 2) + strlen(authentication) + |
80 | strlen(authentication) % 2; |
81 | } |
82 | return sizeof(u32) * 2; |
83 | } |
84 | |
85 | /** |
86 | * populate_security_buffer() - builds a security buffer for authentication scheme |
87 | * @buffer: the buffer to populate |
88 | * @authentication: the authentication content |
89 | * |
90 | * Currently only supported type is PLAIN TEXT |
91 | */ |
92 | void populate_security_buffer(char *buffer, char *authentication) |
93 | { |
94 | char *auth = buffer + sizeof(u32) * 2; |
95 | u32 *sectype = (u32 *) buffer; |
96 | u32 *seclen = sectype + 1; |
97 | |
98 | *sectype = strlen(authentication) > 0 ? 1 : 0; |
99 | *seclen = strlen(authentication); |
100 | |
101 | /* plain text */ |
102 | if (strlen(authentication) > 0) |
103 | memcpy(auth, authentication, *seclen); |
104 | } |
105 | |
106 | /** |
107 | * map_wmi_error() - map errors from WMI methods to kernel error codes |
108 | * @error_code: integer error code returned from Dell's firmware |
109 | */ |
110 | int map_wmi_error(int error_code) |
111 | { |
112 | switch (error_code) { |
113 | case 0: |
114 | /* success */ |
115 | return 0; |
116 | case 1: |
117 | /* failed */ |
118 | return -EIO; |
119 | case 2: |
120 | /* invalid parameter */ |
121 | return -EINVAL; |
122 | case 3: |
123 | /* access denied */ |
124 | return -EACCES; |
125 | case 4: |
126 | /* not supported */ |
127 | return -EOPNOTSUPP; |
128 | case 5: |
129 | /* memory error */ |
130 | return -ENOMEM; |
131 | case 6: |
132 | /* protocol error */ |
133 | return -EPROTO; |
134 | } |
135 | /* unspecified error */ |
136 | return -EIO; |
137 | } |
138 | |
139 | /** |
140 | * reset_bios_show() - sysfs implementaton for read reset_bios |
141 | * @kobj: Kernel object for this attribute |
142 | * @attr: Kernel object attribute |
143 | * @buf: The buffer to display to userspace |
144 | */ |
145 | static ssize_t reset_bios_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) |
146 | { |
147 | char *start = buf; |
148 | int i; |
149 | |
150 | for (i = 0; i < MAX_TYPES; i++) { |
151 | if (i == reset_option) |
152 | buf += sprintf(buf, fmt: "[%s] " , reset_types[i]); |
153 | else |
154 | buf += sprintf(buf, fmt: "%s " , reset_types[i]); |
155 | } |
156 | buf += sprintf(buf, fmt: "\n" ); |
157 | return buf-start; |
158 | } |
159 | |
160 | /** |
161 | * reset_bios_store() - sysfs implementaton for write reset_bios |
162 | * @kobj: Kernel object for this attribute |
163 | * @attr: Kernel object attribute |
164 | * @buf: The buffer from userspace |
165 | * @count: the size of the buffer from userspace |
166 | */ |
167 | static ssize_t reset_bios_store(struct kobject *kobj, |
168 | struct kobj_attribute *attr, const char *buf, size_t count) |
169 | { |
170 | int type = sysfs_match_string(reset_types, buf); |
171 | int ret; |
172 | |
173 | if (type < 0) |
174 | return type; |
175 | |
176 | ret = set_bios_defaults(type); |
177 | pr_debug("reset all attributes request type %d: %d\n" , type, ret); |
178 | if (!ret) { |
179 | reset_option = type; |
180 | ret = count; |
181 | } |
182 | |
183 | return ret; |
184 | } |
185 | |
186 | /** |
187 | * pending_reboot_show() - sysfs implementaton for read pending_reboot |
188 | * @kobj: Kernel object for this attribute |
189 | * @attr: Kernel object attribute |
190 | * @buf: The buffer to display to userspace |
191 | * |
192 | * Stores default value as 0 |
193 | * When current_value is changed this attribute is set to 1 to notify reboot may be required |
194 | */ |
195 | static ssize_t pending_reboot_show(struct kobject *kobj, struct kobj_attribute *attr, |
196 | char *buf) |
197 | { |
198 | return sprintf(buf, fmt: "%d\n" , wmi_priv.pending_changes); |
199 | } |
200 | |
201 | static struct kobj_attribute reset_bios = __ATTR_RW(reset_bios); |
202 | static struct kobj_attribute pending_reboot = __ATTR_RO(pending_reboot); |
203 | |
204 | |
205 | /** |
206 | * create_attributes_level_sysfs_files() - Creates reset_bios and |
207 | * pending_reboot attributes |
208 | */ |
209 | static int create_attributes_level_sysfs_files(void) |
210 | { |
211 | int ret; |
212 | |
213 | ret = sysfs_create_file(kobj: &wmi_priv.main_dir_kset->kobj, attr: &reset_bios.attr); |
214 | if (ret) |
215 | return ret; |
216 | |
217 | ret = sysfs_create_file(kobj: &wmi_priv.main_dir_kset->kobj, attr: &pending_reboot.attr); |
218 | if (ret) |
219 | return ret; |
220 | |
221 | return 0; |
222 | } |
223 | |
224 | static ssize_t wmi_sysman_attr_show(struct kobject *kobj, struct attribute *attr, |
225 | char *buf) |
226 | { |
227 | struct kobj_attribute *kattr; |
228 | ssize_t ret = -EIO; |
229 | |
230 | kattr = container_of(attr, struct kobj_attribute, attr); |
231 | if (kattr->show) |
232 | ret = kattr->show(kobj, kattr, buf); |
233 | return ret; |
234 | } |
235 | |
236 | static ssize_t wmi_sysman_attr_store(struct kobject *kobj, struct attribute *attr, |
237 | const char *buf, size_t count) |
238 | { |
239 | struct kobj_attribute *kattr; |
240 | ssize_t ret = -EIO; |
241 | |
242 | kattr = container_of(attr, struct kobj_attribute, attr); |
243 | if (kattr->store) |
244 | ret = kattr->store(kobj, kattr, buf, count); |
245 | return ret; |
246 | } |
247 | |
248 | static const struct sysfs_ops wmi_sysman_kobj_sysfs_ops = { |
249 | .show = wmi_sysman_attr_show, |
250 | .store = wmi_sysman_attr_store, |
251 | }; |
252 | |
253 | static void attr_name_release(struct kobject *kobj) |
254 | { |
255 | kfree(objp: kobj); |
256 | } |
257 | |
258 | static const struct kobj_type attr_name_ktype = { |
259 | .release = attr_name_release, |
260 | .sysfs_ops = &wmi_sysman_kobj_sysfs_ops, |
261 | }; |
262 | |
263 | /** |
264 | * strlcpy_attr - Copy a length-limited, NULL-terminated string with bound checks |
265 | * @dest: Where to copy the string to |
266 | * @src: Where to copy the string from |
267 | */ |
268 | void strlcpy_attr(char *dest, char *src) |
269 | { |
270 | size_t len = strlen(src) + 1; |
271 | |
272 | if (len > 1 && len <= MAX_BUFF) |
273 | strscpy(dest, src, len); |
274 | |
275 | /*len can be zero because any property not-applicable to attribute can |
276 | * be empty so check only for too long buffers and log error |
277 | */ |
278 | if (len > MAX_BUFF) |
279 | pr_err("Source string returned from BIOS is out of bound!\n" ); |
280 | } |
281 | |
282 | /** |
283 | * get_wmiobj_pointer() - Get Content of WMI block for particular instance |
284 | * @instance_id: WMI instance ID |
285 | * @guid_string: WMI GUID (in str form) |
286 | * |
287 | * Fetches the content for WMI block (instance_id) under GUID (guid_string) |
288 | * Caller must kfree the return |
289 | */ |
290 | union acpi_object *get_wmiobj_pointer(int instance_id, const char *guid_string) |
291 | { |
292 | struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL }; |
293 | acpi_status status; |
294 | |
295 | status = wmi_query_block(guid: guid_string, instance: instance_id, out: &out); |
296 | |
297 | return ACPI_SUCCESS(status) ? (union acpi_object *)out.pointer : NULL; |
298 | } |
299 | |
300 | /** |
301 | * get_instance_count() - Compute total number of instances under guid_string |
302 | * @guid_string: WMI GUID (in string form) |
303 | */ |
304 | int get_instance_count(const char *guid_string) |
305 | { |
306 | int ret; |
307 | |
308 | ret = wmi_instance_count(guid: guid_string); |
309 | if (ret < 0) |
310 | return 0; |
311 | |
312 | return ret; |
313 | } |
314 | |
315 | /** |
316 | * alloc_attributes_data() - Allocate attributes data for a particular type |
317 | * @attr_type: Attribute type to allocate |
318 | */ |
319 | static int alloc_attributes_data(int attr_type) |
320 | { |
321 | int retval = 0; |
322 | |
323 | switch (attr_type) { |
324 | case ENUM: |
325 | retval = alloc_enum_data(); |
326 | break; |
327 | case INT: |
328 | retval = alloc_int_data(); |
329 | break; |
330 | case STR: |
331 | retval = alloc_str_data(); |
332 | break; |
333 | case PO: |
334 | retval = alloc_po_data(); |
335 | break; |
336 | default: |
337 | break; |
338 | } |
339 | |
340 | return retval; |
341 | } |
342 | |
343 | /** |
344 | * destroy_attribute_objs() - Free a kset of kobjects |
345 | * @kset: The kset to destroy |
346 | * |
347 | * Fress kobjects created for each attribute_name under attribute type kset |
348 | */ |
349 | static void destroy_attribute_objs(struct kset *kset) |
350 | { |
351 | struct kobject *pos, *next; |
352 | |
353 | list_for_each_entry_safe(pos, next, &kset->list, entry) { |
354 | kobject_put(kobj: pos); |
355 | } |
356 | } |
357 | |
358 | /** |
359 | * release_attributes_data() - Clean-up all sysfs directories and files created |
360 | */ |
361 | static void release_attributes_data(void) |
362 | { |
363 | mutex_lock(&wmi_priv.mutex); |
364 | exit_enum_attributes(); |
365 | exit_int_attributes(); |
366 | exit_str_attributes(); |
367 | exit_po_attributes(); |
368 | if (wmi_priv.authentication_dir_kset) { |
369 | destroy_attribute_objs(kset: wmi_priv.authentication_dir_kset); |
370 | kset_unregister(kset: wmi_priv.authentication_dir_kset); |
371 | wmi_priv.authentication_dir_kset = NULL; |
372 | } |
373 | if (wmi_priv.main_dir_kset) { |
374 | sysfs_remove_file(kobj: &wmi_priv.main_dir_kset->kobj, attr: &reset_bios.attr); |
375 | sysfs_remove_file(kobj: &wmi_priv.main_dir_kset->kobj, attr: &pending_reboot.attr); |
376 | destroy_attribute_objs(kset: wmi_priv.main_dir_kset); |
377 | kset_unregister(kset: wmi_priv.main_dir_kset); |
378 | wmi_priv.main_dir_kset = NULL; |
379 | } |
380 | mutex_unlock(lock: &wmi_priv.mutex); |
381 | } |
382 | |
383 | /** |
384 | * init_bios_attributes() - Initialize all attributes for a type |
385 | * @attr_type: The attribute type to initialize |
386 | * @guid: The WMI GUID associated with this type to initialize |
387 | * |
388 | * Initialiaze all 4 types of attributes enumeration, integer, string and password object. |
389 | * Populates each attrbute typ's respective properties under sysfs files |
390 | */ |
391 | static int init_bios_attributes(int attr_type, const char *guid) |
392 | { |
393 | struct kobject *attr_name_kobj; //individual attribute names |
394 | union acpi_object *obj = NULL; |
395 | union acpi_object *elements; |
396 | struct kobject *duplicate; |
397 | struct kset *tmp_set; |
398 | int min_elements; |
399 | |
400 | /* instance_id needs to be reset for each type GUID |
401 | * also, instance IDs are unique within GUID but not across |
402 | */ |
403 | int instance_id = 0; |
404 | int retval = 0; |
405 | |
406 | retval = alloc_attributes_data(attr_type); |
407 | if (retval) |
408 | return retval; |
409 | |
410 | switch (attr_type) { |
411 | case ENUM: min_elements = 8; break; |
412 | case INT: min_elements = 9; break; |
413 | case STR: min_elements = 8; break; |
414 | case PO: min_elements = 4; break; |
415 | default: |
416 | pr_err("Error: Unknown attr_type: %d\n" , attr_type); |
417 | return -EINVAL; |
418 | } |
419 | |
420 | /* need to use specific instance_id and guid combination to get right data */ |
421 | obj = get_wmiobj_pointer(instance_id, guid_string: guid); |
422 | if (!obj) |
423 | return -ENODEV; |
424 | |
425 | mutex_lock(&wmi_priv.mutex); |
426 | while (obj) { |
427 | if (obj->type != ACPI_TYPE_PACKAGE) { |
428 | pr_err("Error: Expected ACPI-package type, got: %d\n" , obj->type); |
429 | retval = -EIO; |
430 | goto err_attr_init; |
431 | } |
432 | |
433 | if (obj->package.count < min_elements) { |
434 | pr_err("Error: ACPI-package does not have enough elements: %d < %d\n" , |
435 | obj->package.count, min_elements); |
436 | goto nextobj; |
437 | } |
438 | |
439 | elements = obj->package.elements; |
440 | |
441 | /* sanity checking */ |
442 | if (elements[ATTR_NAME].type != ACPI_TYPE_STRING) { |
443 | pr_debug("incorrect element type\n" ); |
444 | goto nextobj; |
445 | } |
446 | if (strlen(elements[ATTR_NAME].string.pointer) == 0) { |
447 | pr_debug("empty attribute found\n" ); |
448 | goto nextobj; |
449 | } |
450 | if (attr_type == PO) |
451 | tmp_set = wmi_priv.authentication_dir_kset; |
452 | else |
453 | tmp_set = wmi_priv.main_dir_kset; |
454 | |
455 | duplicate = kset_find_obj(tmp_set, elements[ATTR_NAME].string.pointer); |
456 | if (duplicate) { |
457 | pr_debug("Duplicate attribute name found - %s\n" , |
458 | elements[ATTR_NAME].string.pointer); |
459 | kobject_put(kobj: duplicate); |
460 | goto nextobj; |
461 | } |
462 | |
463 | /* build attribute */ |
464 | attr_name_kobj = kzalloc(size: sizeof(*attr_name_kobj), GFP_KERNEL); |
465 | if (!attr_name_kobj) { |
466 | retval = -ENOMEM; |
467 | goto err_attr_init; |
468 | } |
469 | |
470 | attr_name_kobj->kset = tmp_set; |
471 | |
472 | retval = kobject_init_and_add(kobj: attr_name_kobj, ktype: &attr_name_ktype, NULL, fmt: "%s" , |
473 | elements[ATTR_NAME].string.pointer); |
474 | if (retval) { |
475 | kobject_put(kobj: attr_name_kobj); |
476 | goto err_attr_init; |
477 | } |
478 | |
479 | /* enumerate all of this attribute */ |
480 | switch (attr_type) { |
481 | case ENUM: |
482 | retval = populate_enum_data(enumeration_obj: elements, instance_id, attr_name_kobj, |
483 | enum_property_count: obj->package.count); |
484 | break; |
485 | case INT: |
486 | retval = populate_int_data(integer_obj: elements, instance_id, attr_name_kobj); |
487 | break; |
488 | case STR: |
489 | retval = populate_str_data(str_obj: elements, instance_id, attr_name_kobj); |
490 | break; |
491 | case PO: |
492 | retval = populate_po_data(po_obj: elements, instance_id, attr_name_kobj); |
493 | break; |
494 | default: |
495 | break; |
496 | } |
497 | |
498 | if (retval) { |
499 | pr_debug("failed to populate %s\n" , |
500 | elements[ATTR_NAME].string.pointer); |
501 | goto err_attr_init; |
502 | } |
503 | |
504 | nextobj: |
505 | kfree(objp: obj); |
506 | instance_id++; |
507 | obj = get_wmiobj_pointer(instance_id, guid_string: guid); |
508 | } |
509 | |
510 | mutex_unlock(lock: &wmi_priv.mutex); |
511 | return 0; |
512 | |
513 | err_attr_init: |
514 | mutex_unlock(lock: &wmi_priv.mutex); |
515 | kfree(objp: obj); |
516 | return retval; |
517 | } |
518 | |
519 | static int __init sysman_init(void) |
520 | { |
521 | int ret = 0; |
522 | |
523 | if (!dmi_find_device(type: DMI_DEV_TYPE_OEM_STRING, name: "Dell System" , NULL) && |
524 | !dmi_find_device(type: DMI_DEV_TYPE_OEM_STRING, name: "www.dell.com" , NULL)) { |
525 | pr_err("Unable to run on non-Dell system\n" ); |
526 | return -ENODEV; |
527 | } |
528 | |
529 | ret = init_bios_attr_set_interface(); |
530 | if (ret) |
531 | return ret; |
532 | |
533 | ret = init_bios_attr_pass_interface(); |
534 | if (ret) |
535 | goto err_exit_bios_attr_set_interface; |
536 | |
537 | if (!wmi_priv.bios_attr_wdev || !wmi_priv.password_attr_wdev) { |
538 | pr_debug("failed to find set or pass interface\n" ); |
539 | ret = -ENODEV; |
540 | goto err_exit_bios_attr_pass_interface; |
541 | } |
542 | |
543 | ret = fw_attributes_class_get(fw_attr_class: &fw_attr_class); |
544 | if (ret) |
545 | goto err_exit_bios_attr_pass_interface; |
546 | |
547 | wmi_priv.class_dev = device_create(cls: fw_attr_class, NULL, MKDEV(0, 0), |
548 | NULL, fmt: "%s" , DRIVER_NAME); |
549 | if (IS_ERR(ptr: wmi_priv.class_dev)) { |
550 | ret = PTR_ERR(ptr: wmi_priv.class_dev); |
551 | goto err_unregister_class; |
552 | } |
553 | |
554 | wmi_priv.main_dir_kset = kset_create_and_add(name: "attributes" , NULL, |
555 | parent_kobj: &wmi_priv.class_dev->kobj); |
556 | if (!wmi_priv.main_dir_kset) { |
557 | ret = -ENOMEM; |
558 | goto err_destroy_classdev; |
559 | } |
560 | |
561 | wmi_priv.authentication_dir_kset = kset_create_and_add(name: "authentication" , NULL, |
562 | parent_kobj: &wmi_priv.class_dev->kobj); |
563 | if (!wmi_priv.authentication_dir_kset) { |
564 | ret = -ENOMEM; |
565 | goto err_release_attributes_data; |
566 | } |
567 | |
568 | ret = create_attributes_level_sysfs_files(); |
569 | if (ret) { |
570 | pr_debug("could not create reset BIOS attribute\n" ); |
571 | goto err_release_attributes_data; |
572 | } |
573 | |
574 | ret = init_bios_attributes(attr_type: ENUM, DELL_WMI_BIOS_ENUMERATION_ATTRIBUTE_GUID); |
575 | if (ret) { |
576 | pr_debug("failed to populate enumeration type attributes\n" ); |
577 | goto err_release_attributes_data; |
578 | } |
579 | |
580 | ret = init_bios_attributes(attr_type: INT, DELL_WMI_BIOS_INTEGER_ATTRIBUTE_GUID); |
581 | if (ret) { |
582 | pr_debug("failed to populate integer type attributes\n" ); |
583 | goto err_release_attributes_data; |
584 | } |
585 | |
586 | ret = init_bios_attributes(attr_type: STR, DELL_WMI_BIOS_STRING_ATTRIBUTE_GUID); |
587 | if (ret) { |
588 | pr_debug("failed to populate string type attributes\n" ); |
589 | goto err_release_attributes_data; |
590 | } |
591 | |
592 | ret = init_bios_attributes(attr_type: PO, DELL_WMI_BIOS_PASSOBJ_ATTRIBUTE_GUID); |
593 | if (ret) { |
594 | pr_debug("failed to populate pass object type attributes\n" ); |
595 | goto err_release_attributes_data; |
596 | } |
597 | |
598 | return 0; |
599 | |
600 | err_release_attributes_data: |
601 | release_attributes_data(); |
602 | |
603 | err_destroy_classdev: |
604 | device_destroy(cls: fw_attr_class, MKDEV(0, 0)); |
605 | |
606 | err_unregister_class: |
607 | fw_attributes_class_put(); |
608 | |
609 | err_exit_bios_attr_pass_interface: |
610 | exit_bios_attr_pass_interface(); |
611 | |
612 | err_exit_bios_attr_set_interface: |
613 | exit_bios_attr_set_interface(); |
614 | |
615 | return ret; |
616 | } |
617 | |
618 | static void __exit sysman_exit(void) |
619 | { |
620 | release_attributes_data(); |
621 | device_destroy(cls: fw_attr_class, MKDEV(0, 0)); |
622 | fw_attributes_class_put(); |
623 | exit_bios_attr_set_interface(); |
624 | exit_bios_attr_pass_interface(); |
625 | } |
626 | |
627 | module_init(sysman_init); |
628 | module_exit(sysman_exit); |
629 | |
630 | MODULE_AUTHOR("Mario Limonciello <mario.limonciello@outlook.com>" ); |
631 | MODULE_AUTHOR("Prasanth Ksr <prasanth.ksr@dell.com>" ); |
632 | MODULE_AUTHOR("Divya Bharathi <divya.bharathi@dell.com>" ); |
633 | MODULE_DESCRIPTION("Dell platform setting control interface" ); |
634 | MODULE_LICENSE("GPL" ); |
635 | |