FOSSology  4.7.1
Open Source License Compliance by Open Source Software
kotoba.c
1 /*
2  SPDX-FileCopyrightText: © 2025 Harshit Gandhi <gandhiharshit716@gmail.com>
3  SPDX-FileCopyrightText: © Fossology contributors
4 
5  SPDX-License-Identifier: GPL-2.0-only
6 */
7 
8 #include <stdlib.h>
9 
10 #include "libfossology.h"
11 
12 #include "kotoba.h"
13 #include "database.h"
14 #include "license.h"
15 #include "match.h"
16 #include "common.h"
17 #include "monk.h"
18 #include "string_operations.h"
19 #include "highlight.h"
20 
21 // Global hash table to map License refId (cpId) back to Phrase*
22 static GHashTable* phraseByCpId = NULL;
23 
24 int phrase_onAllMatches(MonkState* state, const File* file, const GArray* matches);
25 
26  MatchCallbacks phraseCallbacks = {.onAll = phrase_onAllMatches};
27 
28 /* Parse comma-separated delimiter string; essential delimiters always included.
29  * Caller must free result with g_free. */
30 char* parseDelimiters(const char* input) {
31  if (input == NULL || strlen(input) == 0) {
32  return g_strdup(" ,\t\n\r\f");
33  }
34 
35  GString* result = g_string_new(" ,\t\n\r\f");
36 
37  gchar** tokens = g_strsplit(input, ",", -1);
38 
39  for (int i = 0; tokens[i] != NULL; i++) {
40  gchar* token = g_strstrip(tokens[i]);
41 
42  if (strlen(token) > 0) {
43  for (int j = 0; token[j] != '\0'; j++) {
44  char c = token[j];
45  if (strchr(result->str, c) == NULL) {
46  g_string_append_c(result, c);
47  }
48  }
49  }
50  }
51 
52  g_strfreev(tokens);
53 
54  /* Ensure essential delimiters are always present. */
55  const char* essentialDelims = " ,\t\n\r\f";
56  for (const char* p = essentialDelims; *p != '\0'; p++) {
57  if (strchr(result->str, *p) == NULL)
58  g_string_append_c(result, *p);
59  }
60 
61  return g_string_free(result, FALSE);
62 }
63 
64 /* Build a Licenses* index from phrases for matching. Caller must free. */
65 Licenses* buildLicenseIndexFromPhrases(GArray* phrases, const char* delimiters) {
66  GArray* licenseArray = g_array_new(FALSE, FALSE, sizeof(License));
67 
68  // Initialize global phrase mapping
69  if (phraseByCpId) {
70  g_hash_table_destroy(phraseByCpId);
71  }
72  phraseByCpId = g_hash_table_new(g_direct_hash, g_direct_equal);
73 
74  for (guint i = 0; i < phrases->len; i++) {
75  Phrase* phrase = g_array_index(phrases, Phrase*, i);
76 
77  // Skip phrases with no mapped licenses
78  if (!phrase->licenseMappings || phrase->licenseMappings->len == 0) {
79  continue;
80  }
81 
82  License license = {0};
83  license.refId = phrase->cpId; // Use cpId as refId for lookup
84  license.shortname = g_strdup_printf("phrase_%ld", phrase->cpId);
85  license.tokens = tokenize(phrase->text, delimiters);
86 
87  g_array_append_val(licenseArray, license);
88 
89  // Store phrase mapping for callback lookup
90  g_hash_table_insert(phraseByCpId, GSIZE_TO_POINTER(phrase->cpId), phrase);
91  }
92 
93  return buildLicenseIndexes(licenseArray, MIN_ADJACENT_MATCHES, MAX_LEADING_DIFF);
94 }
95 
96 /* Save highlights for full matches to highlight_kotoba. Returns 1 on success. */
97 int saveKotobaHighlights(MonkState* state, const File* file, const GArray* matches,
98  long clearingEventId, long phraseId) {
99  for (guint j = 0; j < matches->len; j++) {
100  Match* match = match_array_index(matches, j);
101 
102  // Only save highlights for full matches (exact phrase matches)
103  if (match->type != MATCH_TYPE_FULL)
104  continue;
105 
106  // Calculate byte positions from token indices
107  DiffPoint* highlightTokens = match->ptr.full;
108  DiffPoint highlight = getFullHighlightFor(file->tokens,
109  highlightTokens->start,
110  highlightTokens->length);
111 
112  // Insert into highlight_kotoba table
113  PGresult* highlightResult = fo_dbManager_ExecPrepared(
114  fo_dbManager_PrepareStamement(
115  state->dbManager,
116  "saveKotobaHighlight",
117  "INSERT INTO highlight_kotoba(clearing_event_fk, cp_fk, start, len) VALUES($1,$2,$3,$4)",
118  long, long, size_t, size_t
119  ),
120  clearingEventId,
121  phraseId,
122  highlight.start,
123  highlight.length
124  );
125 
126  if (!highlightResult) {
127  return 0;
128  }
129 
130  PQclear(highlightResult);
131  }
132 
133  return 1;
134 }
135 
136 /* Callback for phrase matches; writes clearing decisions. */
137 int phrase_onAllMatches(MonkState* state, const File* file, const GArray* matches) {
138  int haveAFullMatch = 0;
139  for (guint j = 0; j < matches->len; j++) {
140  Match* match = match_array_index(matches, j);
141  if (match->type == MATCH_TYPE_FULL) {
142  haveAFullMatch = 1;
143  break;
144  }
145  }
146 
147  if (!haveAFullMatch)
148  return 1;
149 
150  PhraseModeArgs* args = (PhraseModeArgs*)state->ptr;
151 
152  if (!fo_dbManager_begin(state->dbManager))
153  return 0;
154 
155  for (guint j = 0; j < matches->len; j++) {
156  Match* match = match_array_index(matches, j);
157  if (match->type != MATCH_TYPE_FULL)
158  continue;
159 
160  Phrase* phrase = g_hash_table_lookup(phraseByCpId, GSIZE_TO_POINTER(match->license->refId));
161  if (!phrase)
162  continue;
163 
164  for (guint k = 0; k < phrase->licenseMappings->len; k++) {
165  LicenseMapping mapping = g_array_index(phrase->licenseMappings, LicenseMapping, k);
166 
167  /* Per-mapping fields win, the phrase-level columns are the compat
168  * fallback. reportinfo has no phrase-level column to fall back to. */
169  const char* comment = mapping.comment ? mapping.comment :
170  (phrase->comments ? phrase->comments : "");
171  const char* reportinfo = mapping.reportinfo ? mapping.reportinfo : "";
172  const char* acknowledgement = mapping.acknowledgement ? mapping.acknowledgement :
173  (phrase->acknowledgement ? phrase->acknowledgement : "");
174 
175  PGresult* result = fo_dbManager_ExecPrepared(
176  fo_dbManager_PrepareStamement(
177  state->dbManager,
178  phrase->stmtName,
179  args->insertSql,
180  long, int, int, int, int, long, int, char*, char*, char*, int
181  ),
182  file->id,
183  args->userId,
184  args->groupId,
185  args->jobId,
186  BULK_DECISION_TYPE_KOTOBA,
187  mapping.rfPk,
188  mapping.removing ? 1 : 0,
189  comment,
190  reportinfo,
191  acknowledgement,
192  args->uploadId
193  );
194 
195  if (!result) {
196  fo_dbManager_rollback(state->dbManager);
197  return 0;
198  }
199 
200  long clearingEventId = -1;
201  if (PQntuples(result) == 1)
202  clearingEventId = atol(PQgetvalue(result, 0, 0));
203  PQclear(result);
204 
205  if (clearingEventId <= 0)
206  continue;
207 
208  if (k == 0) {
209  if (!saveKotobaHighlights(state, file, matches, clearingEventId, phrase->cpId)) {
210  fo_dbManager_rollback(state->dbManager);
211  return 0;
212  }
213  }
214  }
215  }
216 
217  return fo_dbManager_commit(state->dbManager);
218 }
219 
220 /* Process a single upload with phrase-mode scanning. */
221 int processUploadWithPhrases(MonkState* state, int uploadId) {
222  int userId = fo_scheduler_userID();
223  int groupId = fo_scheduler_groupID();
224  int jobId = fo_scheduler_jobId();
225 
226  GArray* phrases = queryActiveCustomPhrases(state->dbManager);
227  if (!phrases || phrases->len == 0) {
228  if (phrases)
229  phrases_free(phrases);
230  return 1;
231  }
232 
233  char* configDelimiters = NULL;
234  PGresult* delimitersResult = fo_dbManager_Exec_printf(state->dbManager,
235  "SELECT conf_value FROM sysconfig WHERE variablename='KotobaDelimiters'");
236  if (delimitersResult && PQntuples(delimitersResult) > 0 && !PQgetisnull(delimitersResult, 0, 0))
237  configDelimiters = g_strdup(PQgetvalue(delimitersResult, 0, 0));
238  if (delimitersResult)
239  PQclear(delimitersResult);
240 
241  char* delimiters = parseDelimiters(configDelimiters);
242  g_free(configDelimiters);
243 
244  /* Cache upload tree table name and pre-build the INSERT SQL once. */
245  char* uploadTreeTableName = getUploadTreeTableName(state->dbManager, uploadId);
246  gchar* insertSql = g_strdup_printf(
247  "INSERT INTO clearing_event(uploadtree_fk, user_fk, group_fk, job_fk, type_fk, rf_fk, removed, comment, reportinfo, acknowledgement) "
248  "SELECT uploadtree_pk, $2, $3, $4, $5, $6, $7, $8, $9, $10 "
249  "FROM %s WHERE upload_fk = $11 AND pfile_fk = $1 "
250  "RETURNING clearing_event_pk",
251  uploadTreeTableName);
252 
253  /* Pre-build per-phrase statement names (read-only in the parallel loop). */
254  for (guint i = 0; i < phrases->len; i++) {
255  Phrase* p = g_array_index(phrases, Phrase*, i);
256  g_free(p->stmtName);
257  p->stmtName = g_strdup_printf("phrase_decision.%s.%ld", uploadTreeTableName, p->cpId);
258  }
259  g_free(uploadTreeTableName);
260 
261  PhraseModeArgs args = {
262  .uploadId = uploadId,
263  .userId = userId,
264  .groupId = groupId,
265  .jobId = jobId,
266  .phrases = phrases,
267  .delimiters = delimiters,
268  .uploadTreeTableName = NULL, /* not needed by callback; SQL is in insertSql */
269  .insertSql = insertSql
270  };
271 
272  state->ptr = &args;
273 
274  Licenses* licenses = buildLicenseIndexFromPhrases(phrases, args.delimiters);
275  if (!licenses) {
276  phrases_free(phrases);
277  g_free(args.delimiters);
278  g_free(args.insertSql);
279  return 0;
280  }
281 
282  PGresult* fileIdResult = queryFileIdsForUpload(state->dbManager, uploadId, false);
283  if (!fileIdResult) {
284  licenses_free(licenses);
285  phrases_free(phrases);
286  g_free(args.delimiters);
287  g_free(args.insertSql);
288  return 0;
289  }
290 
291  if (PQntuples(fileIdResult) == 0) {
292  PQclear(fileIdResult);
293  licenses_free(licenses);
294  phrases_free(phrases);
295  g_free(args.delimiters);
296  g_free(args.insertSql);
298  return 1;
299  }
300 
301  int haveError = 0;
302  int resultsCount = PQntuples(fileIdResult);
303 
304 #ifdef MONK_MULTI_THREAD
305  #pragma omp parallel
306 #endif
307  {
308  MonkState threadLocalStateStore = *state;
309  MonkState* threadLocalState = &threadLocalStateStore;
310  threadLocalState->ptr = &args;
311 
312  threadLocalState->dbManager = fo_dbManager_fork(state->dbManager);
313  if (threadLocalState->dbManager) {
314 #ifdef MONK_MULTI_THREAD
315  #pragma omp for schedule(dynamic)
316 #endif
317  for (int i = 0; i < resultsCount; i++) {
318  if (haveError)
319  continue;
320 
321  long fileId = atol(PQgetvalue(fileIdResult, i, 0));
322 
323  if (matchPFileWithLicenses(threadLocalState, fileId, licenses,
324  &phraseCallbacks, args.delimiters)) {
326  } else {
328  haveError = 1;
329  }
330  }
331  fo_dbManager_finish(threadLocalState->dbManager);
332  } else {
333  haveError = 1;
334  }
335  }
336 
337  PQclear(fileIdResult);
338  licenses_free(licenses);
339  phrases_free(phrases);
340  g_free(args.delimiters);
341  g_free(args.insertSql);
342 
343  return !haveError;
344 }
345 
346 /* Main function - phrase-driven bulk scanning. */
347 int main(int argc, char** argv) {
348  MonkState stateStore;
349  MonkState* state = &stateStore;
350 
351  fo_scheduler_connect_dbMan(&argc, argv, &(state->dbManager));
352 
353  queryAgentId(state, AGENT_NAME, AGENT_DESC);
354 
355  state->scanMode = MODE_BULK;
356 
357  while (fo_scheduler_next() != NULL) {
358  const char* schedulerCurrent = fo_scheduler_current();
359 
360  int uploadId = atoi(schedulerCurrent);
361 
362  if (uploadId == 0) continue;
363 
364  int arsId = fo_WriteARS(fo_dbManager_getWrappedConnection(state->dbManager),
365  0, uploadId, state->agentId, AGENT_ARS, NULL, 0);
366 
367  if (arsId <= 0)
368  bail(state, 2);
369 
370  if (!processUploadWithPhrases(state, uploadId))
371  bail(state, 3);
372 
373  fo_WriteARS(fo_dbManager_getWrappedConnection(state->dbManager),
374  arsId, uploadId, state->agentId, AGENT_ARS, NULL, 1);
375 
377  }
378 
379  // Clean up global hash table
380  if (phraseByCpId) {
381  g_hash_table_destroy(phraseByCpId);
382  phraseByCpId = NULL;
383  }
384 
385  scheduler_disconnect(state, 0);
386  return 0;
387 }
int queryAgentId(DbManager &dbManager)
Query and register the agent id. Bails on failure.
void bail(int exitval)
Disconnect scheduler and exit.
void matchPFileWithLicenses(CopyrightState const &state, int agentId, unsigned long pFileId, CopyrightDatabaseHandler &databaseHandler, int uploadId, const string &uploadTreeTableName)
Get the file contents, scan for statements and save findings to database.
char * getUploadTreeTableName(fo_dbManager *dbManager, int uploadId)
Get the upload tree table name for a given upload.
Definition: libfossagent.c:25
PGresult * queryFileIdsForUpload(fo_dbManager *dbManager, int uploadId, bool ignoreFilesWithMimeType)
Get all file IDs (pfile_fk) for a given upload.
Definition: libfossagent.c:61
FUNCTION int fo_WriteARS(PGconn *pgConn, int ars_pk, int upload_pk, int agent_pk, const char *tableName, const char *ars_status, int ars_success)
Write ars record.
Definition: libfossagent.c:214
The main FOSSology C library.
void fo_scheduler_heart(int i)
This function must be called by agents to let the scheduler know they are alive and how many items th...
int jobId
The id of the job.
void fo_scheduler_connect_dbMan(int *argc, char **argv, fo_dbManager **dbManager)
Make a connection from an agent to the scheduler and create a DB manager as well.
int fo_scheduler_userID()
Gets the id of the user that created the job that the agent is running.
char * fo_scheduler_current()
Get the last read string from the scheduler.
int fo_scheduler_jobId()
Gets the id of the job that the agent is running.
char * fo_scheduler_next()
Get the next data to process from the scheduler.
int fo_scheduler_groupID()
Gets the id of the group that created the job that the agent is running.
if(!preg_match("/\s$projectGroup\s/", $groups) &&(posix_getgid() !=$gInfo[ 'gid']))
get monk license list of one specified uploadtree_id
Definition: migratetest.php:33
PGresult * fo_dbManager_ExecPrepared(fo_dbManager_PreparedStatement *preparedStatement,...)
Execute a prepared statement.
Definition: standalone.c:37
Definition: diff.h:14
Definition: monk.h:61
License mapping entry with per-mapping report metadata.
Definition: database.h:22
int removing
0 = add license, 1 = remove license
Definition: database.h:24
char * acknowledgement
nullable
Definition: database.h:27
long rfPk
rf_pk from license_ref table
Definition: database.h:23
char * comment
nullable
Definition: database.h:25
char * reportinfo
nullable
Definition: database.h:26
Definition: monk.h:55
Definition: monk.h:67
Definition: match.h:20
Definition: monk.h:44
Structure to hold a custom phrase and its mapped licenses.
Definition: database.h:33
Definition: nomos.h:426
Store the results of a regex match.
Definition: scanners.hpp:28