chrome-tools 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -246
- package/dist/bin/cli.d.ts +8 -0
- package/dist/bin/cli.js +2 -0
- package/dist/src/core/chrome.d.ts +77 -0
- package/dist/src/core/chrome.js +1 -0
- package/dist/src/core/crypto.d.ts +26 -0
- package/dist/src/core/crypto.js +1 -0
- package/dist/src/core/db.d.ts +19 -0
- package/dist/src/core/db.js +1 -0
- package/dist/src/core/keychain.d.ts +20 -0
- package/dist/src/core/keychain.js +1 -0
- package/{src/index.js → dist/src/index.d.ts} +5 -29
- package/dist/src/index.js +1 -0
- package/dist/src/modules/autofill.d.ts +72 -0
- package/dist/src/modules/autofill.js +1 -0
- package/dist/src/modules/bookmarks.d.ts +23 -0
- package/dist/src/modules/bookmarks.js +1 -0
- package/dist/src/modules/cookies.d.ts +40 -0
- package/dist/src/modules/cookies.js +1 -0
- package/dist/src/modules/downloads.d.ts +28 -0
- package/dist/src/modules/downloads.js +1 -0
- package/dist/src/modules/extensions.d.ts +25 -0
- package/dist/src/modules/extensions.js +1 -0
- package/dist/src/modules/history.d.ts +37 -0
- package/dist/src/modules/history.js +1 -0
- package/dist/src/modules/passwords.d.ts +20 -0
- package/dist/src/modules/passwords.js +1 -0
- package/dist/src/modules/search-engines.d.ts +22 -0
- package/dist/src/modules/search-engines.js +1 -0
- package/dist/src/modules/top-sites.d.ts +18 -0
- package/dist/src/modules/top-sites.js +1 -0
- package/package.json +26 -19
- package/bin/cli.js +0 -629
- package/src/core/chrome.js +0 -178
- package/src/core/crypto.js +0 -75
- package/src/core/db.js +0 -69
- package/src/core/keychain.js +0 -68
- package/src/modules/autofill.js +0 -213
- package/src/modules/bookmarks.js +0 -109
- package/src/modules/cookies.js +0 -178
- package/src/modules/downloads.js +0 -91
- package/src/modules/extensions.js +0 -80
- package/src/modules/history.js +0 -102
- package/src/modules/passwords.js +0 -61
- package/src/modules/search-engines.js +0 -48
- package/src/modules/top-sites.js +0 -43
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{join as t}from"node:path";import{createHash as e}from"node:crypto";import{resolveTargetProfiles as n}from"../core/chrome.js";import{deriveEncryptionKey as o}from"../core/keychain.js";import{decryptValueBuffer as s,chromeTimeToDate as r}from"../core/crypto.js";import{queryDatabase as c,tableExists as i}from"../core/db.js";function a(t,n,o){const r=s(t,n);if(!Buffer.isBuffer(r))return r;const c=e("sha256").update(o).digest();return(r.length>=c.length&&r.subarray(0,c.length).equals(c)?r.subarray(c.length):r).toString("utf-8")}function u(t){if(!t)return null;try{return new URL(t)}catch{try{if(!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(t))return new URL("https://"+t)}catch{}}return null}function h(t){const e=new Set;e.add(t),e.add("."+t);const n=t.split(".");for(let t=1;t<n.length;t++){const o=n.slice(t).join(".");e.add(o),e.add("."+o)}return Array.from(e)}function p(t,e){return t===e||!(!e.startsWith(".")||t!==e.slice(1)&&!t.endsWith(e))}function l(t,e){const n=t||"/",o=e||"/";return"/"===o||(n===o||!(!n.startsWith(o)||!o.endsWith("/")&&"/"!==n[o.length]))}export function getCookies({chromeDir:e,profile:s,domain:f,url:m,decrypt:d=!0,limit:y=100}={}){const _=n({chromeDir:e,profile:s}),k=d?o():null,g=[],j=u(m);for(const e of _){const n=t(e.path,"Cookies");try{if(!i(n,"cookies"))continue;let t="SELECT host_key, name, value, encrypted_value, path,\n expires_utc, is_secure, is_httponly, samesite,\n creation_utc, last_access_utc\n FROM cookies";const o=[],s=[];if(f)o.push("host_key LIKE ?"),s.push(`%${f}%`);else if(j){const t=h(j.hostname),e=t.map(()=>"?").join(", ");o.push(`host_key IN (${e})`),s.push(...t)}o.length>0&&(t+=` WHERE ${o.join(" AND ")}`),t+=" ORDER BY host_key, name",y&&!m&&(t+=` LIMIT ${parseInt(y,10)}`);const u=c(n,t,s);for(const t of u){if(j){if(!p(j.hostname,t.host_key||""))continue;if(!l(j.pathname||"/",t.path||"/"))continue;if(t.is_secure&&"https:"!==j.protocol)continue}let n=t.value||"";!n&&t.encrypted_value&&k&&(n=a(t.encrypted_value,k,t.host_key||"")),g.push({profile:e.name,domain:t.host_key||"",name:t.name||"",value:n,path:t.path||"/",expires:r(t.expires_utc||0),secure:Boolean(t.is_secure),httpOnly:Boolean(t.is_httponly),sameSite:["unspecified","lax","strict","none"][t.samesite]||"unspecified",created:r(t.creation_utc||0),lastAccessed:r(t.last_access_utc||0)})}}catch{}}return y&&m?g.slice(0,parseInt(y,10)):g}export const _test={parseURL:u,getPossibleHostKeys:h,domainMatch:p,pathMatch:l};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface DownloadOptions {
|
|
2
|
+
chromeDir?: string;
|
|
3
|
+
profile?: string;
|
|
4
|
+
search?: string;
|
|
5
|
+
limit?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface DownloadEntry {
|
|
8
|
+
profile: string;
|
|
9
|
+
targetPath: string;
|
|
10
|
+
url: string;
|
|
11
|
+
siteUrl: string;
|
|
12
|
+
referrer: string;
|
|
13
|
+
totalBytes: number;
|
|
14
|
+
receivedBytes: number;
|
|
15
|
+
mimeType: string;
|
|
16
|
+
state: string;
|
|
17
|
+
dangerType: string;
|
|
18
|
+
opened: boolean;
|
|
19
|
+
startTime: string;
|
|
20
|
+
endTime: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Retrieves download history from Chrome's History database.
|
|
24
|
+
*
|
|
25
|
+
* @param options - Targeting, filtering, and limit options.
|
|
26
|
+
* @returns Array of download entries.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getDownloads({ chromeDir, profile, search, limit, }?: DownloadOptions): DownloadEntry[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{join as e}from"node:path";import{resolveTargetProfiles as t}from"../core/chrome.js";import{chromeTimeToDate as r}from"../core/crypto.js";import{queryDatabase as o,tableExists as n}from"../core/db.js";const a={0:"in_progress",1:"complete",2:"cancelled",3:"interrupted"},s={0:"not_dangerous",1:"dangerous_file",2:"dangerous_url",3:"dangerous_content",4:"maybe_dangerous_content",5:"uncommon_content",6:"user_validated",7:"dangerous_host",8:"potentially_unwanted",9:"allowlisted_by_policy"};export function getDownloads({chromeDir:i,profile:_,search:l,limit:d=100}={}){const c=t({chromeDir:i,profile:_}),m=[];for(const t of c){const i=e(t.path,"History");try{if(!n(i,"downloads"))continue;let e="SELECT id, target_path, tab_url, total_bytes, received_bytes,\n start_time, end_time, mime_type, original_mime_type,\n state, danger_type, opened, referrer, site_url\n FROM downloads ORDER BY start_time DESC";d&&(e+=` LIMIT ${parseInt(d,10)}`);const _=o(i,e);for(const e of _){const o={profile:t.name,targetPath:e.target_path||"",url:e.tab_url||"",siteUrl:e.site_url||"",referrer:e.referrer||"",totalBytes:e.total_bytes||0,receivedBytes:e.received_bytes||0,mimeType:e.mime_type||e.original_mime_type||"",state:a[e.state]||`unknown(${e.state})`,dangerType:s[e.danger_type]||`unknown(${e.danger_type})`,opened:Boolean(e.opened),startTime:r(e.start_time||0),endTime:r(e.end_time||0)};if(l){const e=l.toLowerCase();if(!(o.targetPath&&o.targetPath.toLowerCase().includes(e)||o.url&&o.url.toLowerCase().includes(e)))continue}m.push(o)}}catch{}}return m}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ExtensionOptions {
|
|
2
|
+
chromeDir?: string;
|
|
3
|
+
profile?: string;
|
|
4
|
+
search?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ExtensionEntry {
|
|
7
|
+
profile: string;
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
description: string;
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
homepageUrl: string;
|
|
14
|
+
updateUrl: string;
|
|
15
|
+
installTime: string;
|
|
16
|
+
fromWebstore: boolean;
|
|
17
|
+
location: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Retrieves installed Chrome extensions from the Preferences JSON file.
|
|
21
|
+
*
|
|
22
|
+
* @param options - Targeting and filtering options.
|
|
23
|
+
* @returns Array of extension entries.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getExtensions({ chromeDir, profile, search }?: ExtensionOptions): ExtensionEntry[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{existsSync as e,readFileSync as o}from"node:fs";import{join as t}from"node:path";import{resolveTargetProfiles as n}from"../core/chrome.js";export function getExtensions({chromeDir:r,profile:i,search:s}={}){const c=n({chromeDir:r,profile:i}),a=[];for(const n of c){let r=null;for(const i of["Secure Preferences","Preferences"]){const s=t(n.path,i);if(e(s))try{r=JSON.parse(o(s,"utf-8"));break}catch{continue}}if(!r)continue;const i=r?.extensions?.settings;if(i&&"object"==typeof i)for(const[e,o]of Object.entries(i)){const t=o;if(5===t.location||10===t.location)continue;const r=t.manifest||{},i={profile:n.name,id:e,name:r.name||t.name||e,version:r.version||t.version||"unknown",description:r.description||"",enabled:1===t.state,homepageUrl:r.homepage_url||"",updateUrl:r.update_url||"",installTime:t.install_time||"",fromWebstore:t.from_webstore||!1,location:t.location||0};if(i.name.startsWith("__MSG_")&&(i.name=e),i.description.startsWith("__MSG_")&&(i.description=""),s){const e=s.toLowerCase();if(!(i.name&&i.name.toLowerCase().includes(e)||i.id&&i.id.toLowerCase().includes(e)))continue}a.push(i)}}return a}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface HistoryOptions {
|
|
2
|
+
chromeDir?: string;
|
|
3
|
+
profile?: string;
|
|
4
|
+
search?: string;
|
|
5
|
+
limit?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface HistoryEntry {
|
|
8
|
+
profile: string;
|
|
9
|
+
url: string;
|
|
10
|
+
title: string;
|
|
11
|
+
visitCount: number;
|
|
12
|
+
lastVisit: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SearchTermOptions {
|
|
15
|
+
chromeDir?: string;
|
|
16
|
+
profile?: string;
|
|
17
|
+
limit?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface SearchTermEntry {
|
|
20
|
+
profile: string;
|
|
21
|
+
term: string;
|
|
22
|
+
url: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Retrieves browsing history from Chrome's History database.
|
|
26
|
+
*
|
|
27
|
+
* @param options - Filtering and targeting options.
|
|
28
|
+
* @returns Array of history entries.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getHistory({ chromeDir, profile, search, limit }?: HistoryOptions): HistoryEntry[];
|
|
31
|
+
/**
|
|
32
|
+
* Retrieves search terms from Chrome's History database.
|
|
33
|
+
*
|
|
34
|
+
* @param options - Targeting and limit options.
|
|
35
|
+
* @returns Array of search term entries.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getSearchTerms({ chromeDir, profile, limit }?: SearchTermOptions): SearchTermEntry[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{join as t}from"node:path";import{resolveTargetProfiles as r}from"../core/chrome.js";import{chromeTimeToDate as o}from"../core/crypto.js";import{queryDatabase as e,tableExists as i}from"../core/db.js";export function getHistory({chromeDir:s,profile:c,search:n,limit:l=100}={}){const u=r({chromeDir:s,profile:c}),m=[];for(const r of u){const s=t(r.path,"History");try{if(!i(s,"urls"))continue;let t="SELECT url, title, visit_count, last_visit_time\n FROM urls ORDER BY last_visit_time DESC";l&&(t+=` LIMIT ${parseInt(l,10)}`);const c=e(s,t);for(const t of c){const e={profile:r.name,url:t.url,title:t.title||"",visitCount:t.visit_count||0,lastVisit:o(t.last_visit_time||0)};if(n){const t=n.toLowerCase();if(!(e.url&&e.url.toLowerCase().includes(t)||e.title&&e.title.toLowerCase().includes(t)))continue}m.push(e)}}catch{}}return m}export function getSearchTerms({chromeDir:o,profile:s,limit:c=100}={}){const n=r({chromeDir:o,profile:s}),l=[];for(const r of n){const o=t(r.path,"History");try{if(!i(o,"keyword_search_terms"))continue;let t="SELECT kst.term, u.url\n FROM keyword_search_terms kst\n JOIN urls u ON kst.url_id = u.id\n ORDER BY u.last_visit_time DESC";c&&(t+=` LIMIT ${parseInt(c,10)}`);const s=e(o,t);for(const t of s)l.push({profile:r.name,term:t.term||"",url:t.url||""})}catch{}}return l}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface PasswordOptions {
|
|
2
|
+
chromeDir?: string;
|
|
3
|
+
profile?: string;
|
|
4
|
+
search?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PasswordEntry {
|
|
7
|
+
profile: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
username?: string;
|
|
10
|
+
password?: string;
|
|
11
|
+
lastUsed?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Extracts and decrypts saved passwords from Chrome's Login Data database.
|
|
16
|
+
*
|
|
17
|
+
* @param options - Options for profile selection and filtering.
|
|
18
|
+
* @returns Array of decrypted password entries.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getPasswords({ chromeDir, profile, search }?: PasswordOptions): PasswordEntry[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{join as r}from"node:path";import{resolveTargetProfiles as o}from"../core/chrome.js";import{deriveEncryptionKey as e}from"../core/keychain.js";import{decryptValue as s,chromeTimeToDate as t}from"../core/crypto.js";import{queryDatabase as n,tableExists as a}from"../core/db.js";export function getPasswords({chromeDir:i,profile:u,search:c}={}){const l=o({chromeDir:i,profile:u}),m=e(),f=[];for(const o of l){const e=r(o.path,"Login Data");try{if(!a(e,"logins"))continue;const r=n(e,"SELECT origin_url, username_value, password_value, date_last_used\n FROM logins ORDER BY origin_url");for(const e of r){const r={profile:o.name,url:e.origin_url,username:e.username_value,password:s(e.password_value,m),lastUsed:t(e.date_last_used||0)};if(c){const o=c.toLowerCase();if(!(r.url&&r.url.toLowerCase().includes(o)||r.username&&r.username.toLowerCase().includes(o)))continue}f.push(r)}}catch(r){f.push({profile:o.name,error:r.message})}}return f.filter(r=>!r.error)}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface SearchEngineOptions {
|
|
2
|
+
chromeDir?: string;
|
|
3
|
+
profile?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface SearchEngineEntry {
|
|
6
|
+
profile: string;
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
keyword: string;
|
|
10
|
+
url: string;
|
|
11
|
+
suggestionsUrl: string;
|
|
12
|
+
faviconUrl: string;
|
|
13
|
+
isDefault: boolean;
|
|
14
|
+
isActive: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Retrieves search engines configured in Chrome from the Web Data database.
|
|
18
|
+
*
|
|
19
|
+
* @param options - Targeting options.
|
|
20
|
+
* @returns Array of search engine entries.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getSearchEngines({ chromeDir, profile }?: SearchEngineOptions): SearchEngineEntry[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{join as o}from"node:path";import{resolveTargetProfiles as r}from"../core/chrome.js";import{queryDatabase as e,tableExists as i}from"../core/db.js";export function getSearchEngines({chromeDir:t,profile:s}={}){const n=r({chromeDir:t,profile:s}),c=[];for(const r of n){const t=o(r.path,"Web Data");try{if(!i(t,"keywords"))continue;const o=e(t,"SELECT id, short_name, keyword, url, suggestions_url,\n favicon_url, prepopulate_id, is_active\n FROM keywords ORDER BY short_name");for(const e of o)c.push({profile:r.name,id:e.id,name:e.short_name||"",keyword:e.keyword||"",url:e.url||"",suggestionsUrl:e.suggestions_url||"",faviconUrl:e.favicon_url||"",isDefault:1===e.prepopulate_id,isActive:1===e.is_active})}catch{}}return c}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface TopSiteOptions {
|
|
2
|
+
chromeDir?: string;
|
|
3
|
+
profile?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface TopSiteEntry {
|
|
6
|
+
profile: string;
|
|
7
|
+
url: string;
|
|
8
|
+
title: string;
|
|
9
|
+
rank: number;
|
|
10
|
+
redirects: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Retrieves top sites from Chrome's Top Sites database.
|
|
14
|
+
*
|
|
15
|
+
* @param options - Targeting options.
|
|
16
|
+
* @returns Array of top site entries.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getTopSites({ chromeDir, profile }?: TopSiteOptions): TopSiteEntry[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{join as r}from"node:path";import{resolveTargetProfiles as t}from"../core/chrome.js";import{queryDatabase as o,tableExists as e}from"../core/db.js";export function getTopSites({chromeDir:i,profile:c}={}){const n=t({chromeDir:i,profile:c}),s=[];for(const t of n){const i=r(t.path,"Top Sites");try{if(!e(i,"top_sites"))continue;const r=o(i,"SELECT url, url_rank, title, redirects\n FROM top_sites ORDER BY url_rank ASC");for(const o of r)s.push({profile:t.name,url:o.url||"",title:o.title||"",rank:o.url_rank??0,redirects:o.redirects||""})}catch{}}return s}
|
package/package.json
CHANGED
|
@@ -1,34 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-tools",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "CLI & Node.js library
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"description": "CLI & Node.js library for chrome",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./src/index.js",
|
|
6
|
+
"main": "./dist/src/index.js",
|
|
7
7
|
"exports": {
|
|
8
|
-
".": "./src/index.js",
|
|
9
|
-
"./passwords": "./src/modules/passwords.js",
|
|
10
|
-
"./bookmarks": "./src/modules/bookmarks.js",
|
|
11
|
-
"./history": "./src/modules/history.js",
|
|
12
|
-
"./cookies": "./src/modules/cookies.js",
|
|
13
|
-
"./autofill": "./src/modules/autofill.js",
|
|
14
|
-
"./downloads": "./src/modules/downloads.js",
|
|
15
|
-
"./extensions": "./src/modules/extensions.js",
|
|
16
|
-
"./search-engines": "./src/modules/search-engines.js",
|
|
17
|
-
"./top-sites": "./src/modules/top-sites.js"
|
|
8
|
+
".": "./dist/src/index.js",
|
|
9
|
+
"./passwords": "./dist/src/modules/passwords.js",
|
|
10
|
+
"./bookmarks": "./dist/src/modules/bookmarks.js",
|
|
11
|
+
"./history": "./dist/src/modules/history.js",
|
|
12
|
+
"./cookies": "./dist/src/modules/cookies.js",
|
|
13
|
+
"./autofill": "./dist/src/modules/autofill.js",
|
|
14
|
+
"./downloads": "./dist/src/modules/downloads.js",
|
|
15
|
+
"./extensions": "./dist/src/modules/extensions.js",
|
|
16
|
+
"./search-engines": "./dist/src/modules/search-engines.js",
|
|
17
|
+
"./top-sites": "./dist/src/modules/top-sites.js"
|
|
18
18
|
},
|
|
19
19
|
"bin": {
|
|
20
|
-
"chrome-tools": "./bin/cli.js"
|
|
20
|
+
"chrome-tools": "./dist/bin/cli.js"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
|
-
"
|
|
24
|
-
"
|
|
23
|
+
"dist/src",
|
|
24
|
+
"dist/bin",
|
|
25
25
|
"README.md",
|
|
26
26
|
"LICENSE"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
29
|
+
"build": "node scripts/clean.mjs && tsc && node scripts/minify.mjs",
|
|
30
|
+
"start": "node dist/bin/cli.js",
|
|
31
|
+
"test": "npm run build && node --test dist/test/*.test.js dist/test/**/*.test.js && node dist/bin/cli.js --help",
|
|
32
|
+
"release": "npm run build && npm version patch --no-git-tag-version && npm publish"
|
|
32
33
|
},
|
|
33
34
|
"keywords": [
|
|
34
35
|
"chrome",
|
|
@@ -51,5 +52,11 @@
|
|
|
51
52
|
},
|
|
52
53
|
"engines": {
|
|
53
54
|
"node": ">=18.0.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
58
|
+
"@types/node": "^25.9.1",
|
|
59
|
+
"terser": "^5.44.0",
|
|
60
|
+
"typescript": "^6.0.3"
|
|
54
61
|
}
|
|
55
62
|
}
|